Skip to content

Commit a22d068

Browse files
committed
test: shave register/shutdown/TC waits on loopback
`async_check_service` paid an inline `random.randint(150, 250)` ms wait on every `register_service` — RFC 6762 §8.1 thundering-herd avoidance for real networks, pure overhead on 127.0.0.1. Extracted as a module constant `_REGISTER_RANDOM_INTERVAL` and patched in the `quick_timing` fixture to (1, 5)ms alongside the existing _CHECK_TIME / _REGISTER_TIME / _UNREGISTER_TIME shaves. Also: - `test_shutdown_loop`: drop `.result(1)` → `.result(0.1)` and patch `_TASK_AWAIT_TIMEOUT` to 0.1s. The 1s waits were `shutdown_loop`'s outer timeout on a never-completing `asyncio.sleep(5)` — the test only needs the loop to be stoppable while a task is pending. - `test_tc_bit_defers_last_response_missing`: patch `_TC_DELAY_RANDOM_INTERVAL` (400-500ms, RFC 6762 §7.2 TC-bit deferral) to (10, 20)ms and tighten the cleanup poll loop. - `test_integration`: wait for the browser's first startup query to fire (with empty cache) before registering. Previously the test relied on the register's 150-250ms random wait being longer than the browser's 20-120ms first-query delay; with the new fast register that race flipped and the first query started seeing the known PTR via §7.1 known-answer suppression. Speedups on loopback (CPython 3.12): | test | before | after | | ----------------------------------------------------- | ------ | ----- | | test_shutdown_loop | 1.16s | 0.26s | | test_tc_bit_defers_last_response_missing | 0.79s | 0.32s | | test_async_service_registration_name_conflict | 0.72s | 0.10s | | test_name_conflicts | 0.50s | 0.10s | Plus a smaller transparent shave on every other `quick_timing` register-based test. Full suite: 338 passed, 2 skipped. No production behavior change — `_REGISTER_RANDOM_INTERVAL` still defaults to the RFC 6762 §8.1 value on real networks.
1 parent 4bae30a commit a22d068

5 files changed

Lines changed: 23 additions & 9 deletions

File tree

src/zeroconf/_core.py

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

105105
_REGISTER_BROADCASTS = 3
106106

107+
# Random delay before probing (RFC 6762 §8.1) to avoid a thundering
108+
# herd when multiple services start at once.
109+
_REGISTER_RANDOM_INTERVAL = (150, 250) # ms
110+
107111

108112
def async_send_with_transport(
109113
log_debug: bool,
@@ -561,7 +565,7 @@ async def async_check_service(
561565

562566
# Wait a random amount of time up avoid collisions and avoid
563567
# a thundering herd when multiple services are started on the network
564-
await self.async_wait(random.randint(150, 250)) # noqa: S311
568+
await self.async_wait(random.randint(*_REGISTER_RANDOM_INTERVAL)) # noqa: S311
565569

566570
next_instance_number = 2
567571
next_time = now = current_time_millis()

tests/conftest.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,17 @@ def quick_timing() -> Generator[None]:
4949
"""Shorten the probe/announce/goodbye intervals for tests on loopback.
5050
5151
The production values (_CHECK_TIME=500ms, _REGISTER_TIME=225ms,
52-
_UNREGISTER_TIME=125ms) exist for RFC 6762 interop on real
53-
networks. Tests on 127.0.0.1 do not need them and pay 1-2s per
54-
register/unregister cycle without this fixture. Opt in by adding
55-
`quick_timing` to a test's argument list.
52+
_UNREGISTER_TIME=125ms, _REGISTER_RANDOM_INTERVAL=(150, 250)ms)
53+
exist for RFC 6762 interop on real networks. Tests on 127.0.0.1
54+
do not need them and pay 1-2s per register/unregister cycle
55+
without this fixture. Opt in by adding `quick_timing` to a
56+
test's argument list.
5657
"""
5758
with (
5859
patch.object(_core, "_CHECK_TIME", 10),
5960
patch.object(_core, "_REGISTER_TIME", 10),
6061
patch.object(_core, "_UNREGISTER_TIME", 10),
62+
patch.object(_core, "_REGISTER_RANDOM_INTERVAL", (1, 5)),
6163
):
6264
yield
6365

tests/test_asyncio.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1060,6 +1060,12 @@ def send(out, addr=const._MDNS_ADDR, port=const._MDNS_PORT, v6_flow_scope=()):
10601060
"ash-2.local.",
10611061
addresses=[socket.inet_aton("10.0.1.2")],
10621062
)
1063+
# Wait for the browser's first startup query to land (with an empty
1064+
# cache) before registering — otherwise on fast loopback the register
1065+
# may finish before the first query fires, and answers[0] picks up
1066+
# the known PTR via §7.1 suppression.
1067+
await asyncio.wait_for(got_query.wait(), 1)
1068+
got_query.clear()
10631069
task = await aio_zeroconf_registrar.async_register_service(info)
10641070
await task
10651071
loop = asyncio.get_running_loop()

tests/test_core.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import pytest
2020

2121
import zeroconf as r
22-
from zeroconf import NotRunningException, Zeroconf, const, current_time_millis
22+
from zeroconf import NotRunningException, Zeroconf, _listener, const, current_time_millis
2323
from zeroconf._listener import AsyncListener, _WrappedTransport
2424
from zeroconf._protocol.incoming import DNSIncoming
2525
from zeroconf.asyncio import AsyncZeroconf
@@ -631,6 +631,7 @@ def test_tc_bit_defers():
631631
zc.close()
632632

633633

634+
@patch.object(_listener, "_TC_DELAY_RANDOM_INTERVAL", (10, 20))
634635
def test_tc_bit_defers_last_response_missing():
635636
zc = Zeroconf(interfaces=["127.0.0.1"])
636637
_wait_for_start(zc)
@@ -730,8 +731,8 @@ def test_tc_bit_defers_last_response_missing():
730731
assert timer3.cancelled()
731732
assert timer4 != timer3
732733

733-
for _ in range(8):
734-
time.sleep(0.1)
734+
for _ in range(20):
735+
time.sleep(0.02)
735736
if source_ip not in protocol._timers and source_ip not in protocol._deferred:
736737
break
737738

tests/utils/test_asyncio.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ async def _async_wait_or_timeout():
6565
await task
6666

6767

68+
@patch.object(aioutils, "_TASK_AWAIT_TIMEOUT", 0.1)
6869
def test_shutdown_loop() -> None:
6970
"""Test shutting down an event loop."""
7071
loop = None
@@ -89,7 +90,7 @@ def _run_coro() -> None:
8990
runcoro_thread_ready.set()
9091
assert loop is not None
9192
with contextlib.suppress(concurrent.futures.TimeoutError):
92-
asyncio.run_coroutine_threadsafe(_still_running(), loop).result(1)
93+
asyncio.run_coroutine_threadsafe(_still_running(), loop).result(0.1)
9394

9495
runcoro_thread = threading.Thread(target=_run_coro, daemon=True)
9596
runcoro_thread.start()

0 commit comments

Comments
 (0)