FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

test: shave loopback timing overhead from remaining slow tests (#1760) · python-zeroconf/python-zeroconf@343dc7a · GitHub

Commit 343dc7a

Browse files
authored
test: shave loopback timing overhead from remaining slow tests (#1760)
1 parent e95297f commit 343dc7a

6 files changed

Lines changed: 40 additions & 21 deletions

File tree

‎src/zeroconf/_core.py‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,13 @@
104104

105105
_REGISTER_BROADCASTS = 3
106106

107+
# RFC 6762 §8.1 thundering-herd avoidance: wait a random
108+
# 0-250ms before the first probe so simultaneously-started
109+
# responders don't collide. We default to 150-250ms to
110+
# preserve existing timing assumptions; tests on loopback
111+
# may patch this lower via the `quick_timing` fixture.
112+
_PROBE_RANDOM_DELAY_INTERVAL = (150, 250) # ms
113+
107114

108115
def async_send_with_transport(
109116
log_debug: bool,
@@ -561,7 +568,7 @@ async def async_check_service(
561568

562569
# Wait a random amount of time up avoid collisions and avoid
563570
# a thundering herd when multiple services are started on the network
564-
await self.async_wait(random.randint(150, 250)) # noqa: S311
571+
await self.async_wait(random.randint(*_PROBE_RANDOM_DELAY_INTERVAL)) # noqa: S311
565572

566573
next_instance_number = 2
567574
next_time = now = current_time_millis()

‎tests/conftest.py‎

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -82,17 +82,22 @@ def quick_timing() -> Generator[None]:
8282
"""Shorten the probe/announce/goodbye/first-query intervals for tests on loopback.
8383
8484
The production values (_CHECK_TIME=500ms, _REGISTER_TIME=225ms,
85-
_UNREGISTER_TIME=125ms, _FIRST_QUERY_DELAY_RANDOM_INTERVAL=20-120ms)
86-
exist for RFC 6762 interop on real networks (§8.1 thundering-herd
87-
avoidance for probing, §5.2 for the initial-query delay). Tests on
88-
127.0.0.1 do not need them and pay 1-2s per register/unregister
89-
cycle and 20-120ms per ServiceBrowser startup without this fixture.
90-
Opt in by adding `quick_timing` to a test's argument list.
85+
_UNREGISTER_TIME=125ms, _PROBE_RANDOM_DELAY_INTERVAL=150-250ms,
86+
_FIRST_QUERY_DELAY_RANDOM_INTERVAL=20-120ms) exist for RFC 6762
87+
interop on real networks (§8.1 thundering-herd avoidance for
88+
probing, §5.2 for the initial-query delay). Tests on 127.0.0.1
89+
do not need them and pay 1-2s per register/unregister cycle,
90+
150-250ms per probe, and 20-120ms per ServiceBrowser startup
91+
without this fixture. Opt in either by adding `quick_timing`
92+
to a test's argument list or via
93+
`@pytest.mark.usefixtures("quick_timing")` on the test or
94+
its class.
9195
"""
9296
with (
9397
patch.object(_core, "_CHECK_TIME", 10),
9498
patch.object(_core, "_REGISTER_TIME", 10),
9599
patch.object(_core, "_UNREGISTER_TIME", 10),
100+
patch.object(_core, "_PROBE_RANDOM_DELAY_INTERVAL", (1, 5)),
96101
patch.object(service_browser, "_FIRST_QUERY_DELAY_RANDOM_INTERVAL", (1, 5)),
97102
):
98103
yield
@@ -105,9 +110,11 @@ def quick_request_timing() -> Generator[None]:
105110
The 200ms `_LISTENER_TIME` and 20-120ms random jitter (RFC 6762
106111
§5.2) help spread queries from multiple clients on real networks.
107112
On loopback they're pure overhead — get_service_info-style tests
108-
wait ~250ms before the first query even fires. Opt in by adding
109-
`quick_request_timing` to a test's argument list, then drop the
110-
test's own timeouts (which had to accommodate that delay).
113+
wait ~250ms before the first query even fires. Opt in either by
114+
adding `quick_request_timing` to a test's argument list or via
115+
`@pytest.mark.usefixtures("quick_request_timing")` on the test
116+
or its class, then drop the test's own timeouts (which had to
117+
accommodate that delay).
111118
"""
112119
with (
113120
patch.object(service_info, "_LISTENER_TIME", 10),

‎tests/services/test_info.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ def test_service_info_rejects_expired_records(self):
251251

252252
@unittest.skipIf(not has_working_ipv6(), "Requires IPv6")
253253
@unittest.skipIf(os.environ.get("SKIP_IPV6"), "IPv6 tests disabled")
254+
@pytest.mark.usefixtures("quick_request_timing")
254255
def test_get_info_partial(self):
255256
zc = r.Zeroconf(interfaces=["127.0.0.1"])
256257

‎tests/test_asyncio.py‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -608,7 +608,7 @@ async def test_async_wait_unblocks_on_update(quick_timing: None) -> None:
608608

609609

610610
@pytest.mark.asyncio
611-
async def test_service_info_async_request(quick_timing: None) -> None:
611+
async def test_service_info_async_request(quick_timing: None, quick_request_timing: None) -> None:
612612
"""Test registering services broadcasts and query with AsyncServceInfo.async_request."""
613613
if not has_working_ipv6() or os.environ.get("SKIP_IPV6"):
614614
pytest.skip("Requires IPv6")
@@ -710,13 +710,13 @@ async def test_service_info_async_request(quick_timing: None) -> None:
710710
aiozc.zeroconf.out_delay_queue.queue.clear()
711711
aiosinfo = AsyncServiceInfo(type_, registration_name)
712712
_clear_cache(aiozc.zeroconf)
713-
# Generating the race condition is almost impossible
714-
# without patching since its a TOCTOU race. 1500ms covers
715-
# the initial _LISTENER_TIME + random delay (200-320ms) and
716-
# leaves plenty of margin for the loopback response to land
713+
# Generating the race condition is almost impossible without
714+
# patching since it's a TOCTOU race. Under `quick_request_timing`
715+
# the first QU query fires at ~10ms and the QM follow-up at ~15ms;
716+
# 300ms leaves plenty of margin for the loopback response to land
717717
# before the loop times out.
718718
with patch("zeroconf.asyncio.AsyncServiceInfo._is_complete", False):
719-
await aiosinfo.async_request(aiozc.zeroconf, 1500)
719+
await aiosinfo.async_request(aiozc.zeroconf, 300)
720720
assert aiosinfo is not None
721721
assert aiosinfo.addresses == [socket.inet_aton("10.0.1.3")]
722722

‎tests/test_services.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def teardown_module():
3333

3434

3535
class ListenerTest(unittest.TestCase):
36-
@pytest.mark.usefixtures("quick_timing")
36+
@pytest.mark.usefixtures("quick_timing", "quick_request_timing")
3737
def test_integration_with_listener_class(self):
3838
sub_service_added = Event()
3939
service_added = Event()

‎tests/utils/test_asyncio.py‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,18 +86,22 @@ async def _still_running():
8686
await asyncio.sleep(5)
8787

8888
def _run_coro() -> None:
89-
runcoro_thread_ready.set()
9089
assert loop is not None
90+
future = asyncio.run_coroutine_threadsafe(_still_running(), loop)
91+
runcoro_thread_ready.set()
9192
with contextlib.suppress(concurrent.futures.TimeoutError):
92-
asyncio.run_coroutine_threadsafe(_still_running(), loop).result(1)
93+
future.result(0.1)
9394

9495
runcoro_thread = threading.Thread(target=_run_coro, daemon=True)
9596
runcoro_thread.start()
9697
runcoro_thread_ready.wait()
9798

98-
time.sleep(0.1)
9999
assert loop is not None
100-
aioutils.shutdown_loop(loop)
100+
# Patch _TASK_AWAIT_TIMEOUT so the inner `asyncio.wait` returns
101+
# within 50ms instead of blocking the full 1s on the deliberately
102+
# never-completing _still_running() task.
103+
with patch.object(aioutils, "_TASK_AWAIT_TIMEOUT", 0.05):
104+
aioutils.shutdown_loop(loop)
101105
for _ in range(5):
102106
if not loop.is_running():
103107
break

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL