Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/zeroconf/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@

_REGISTER_BROADCASTS = 3

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


def async_send_with_transport(
log_debug: bool,
Expand Down Expand Up @@ -561,7 +565,7 @@ async def async_check_service(

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

next_instance_number = 2
next_time = now = current_time_millis()
Expand Down
10 changes: 6 additions & 4 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,17 @@ def quick_timing() -> Generator[None]:
"""Shorten the probe/announce/goodbye intervals for tests on loopback.

The production values (_CHECK_TIME=500ms, _REGISTER_TIME=225ms,
_UNREGISTER_TIME=125ms) exist for RFC 6762 interop on real
networks. Tests on 127.0.0.1 do not need them and pay 1-2s per
register/unregister cycle without this fixture. Opt in by adding
`quick_timing` to a test's argument list.
_UNREGISTER_TIME=125ms, _REGISTER_RANDOM_INTERVAL=(150, 250)ms)
exist for RFC 6762 interop on real networks. Tests on 127.0.0.1
do not need them and pay 1-2s per register/unregister cycle
without this fixture. Opt in by adding `quick_timing` to a
test's argument list.
"""
with (
patch.object(_core, "_CHECK_TIME", 10),
patch.object(_core, "_REGISTER_TIME", 10),
patch.object(_core, "_UNREGISTER_TIME", 10),
patch.object(_core, "_REGISTER_RANDOM_INTERVAL", (1, 5)),
):
yield

Expand Down
6 changes: 6 additions & 0 deletions tests/test_asyncio.py
Original file line number Diff line number Diff line change
Expand Up @@ -1060,6 +1060,12 @@ def send(out, addr=const._MDNS_ADDR, port=const._MDNS_PORT, v6_flow_scope=()):
"ash-2.local.",
addresses=[socket.inet_aton("10.0.1.2")],
)
# Wait for the browser's first startup query to land (with an empty
# cache) before registering — otherwise on fast loopback the register
# may finish before the first query fires, and answers[0] picks up
# the known PTR via §7.1 suppression.
await asyncio.wait_for(got_query.wait(), 1)
got_query.clear()
task = await aio_zeroconf_registrar.async_register_service(info)
await task
loop = asyncio.get_running_loop()
Expand Down
3 changes: 2 additions & 1 deletion tests/utils/test_asyncio.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ async def _async_wait_or_timeout():
await task


@patch.object(aioutils, "_TASK_AWAIT_TIMEOUT", 0.1)
def test_shutdown_loop() -> None:
"""Test shutting down an event loop."""
loop = None
Expand All @@ -89,7 +90,7 @@ def _run_coro() -> None:
runcoro_thread_ready.set()
assert loop is not None
with contextlib.suppress(concurrent.futures.TimeoutError):
asyncio.run_coroutine_threadsafe(_still_running(), loop).result(1)
asyncio.run_coroutine_threadsafe(_still_running(), loop).result(0.1)

runcoro_thread = threading.Thread(target=_run_coro, daemon=True)
runcoro_thread.start()
Expand Down
Loading