| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -104,6 +104,13 @@ | |||
| 104 | 104 | ||
| 105 | 105 | _REGISTER_BROADCASTS = 3 | |
| 106 | 106 | ||
| 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 | + | ||
| 107 | 114 | ||
| 108 | 115 | def async_send_with_transport( | |
| 109 | 116 | log_debug: bool, | |
@@ -561,7 +568,7 @@ async def async_check_service( | |||
| 561 | 568 | ||
| 562 | 569 | # Wait a random amount of time up avoid collisions and avoid | |
| 563 | 570 | # 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 | ||
| 565 | 572 | ||
| 566 | 573 | next_instance_number = 2 | |
| 567 | 574 | next_time = now = current_time_millis() | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -82,17 +82,22 @@ def quick_timing() -> Generator[None]: | |||
| 82 | 82 | """Shorten the probe/announce/goodbye/first-query intervals for tests on loopback. | |
| 83 | 83 | ||
| 84 | 84 | 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. | ||
| 91 | 95 | """ | |
| 92 | 96 | with ( | |
| 93 | 97 | patch.object(_core, "_CHECK_TIME", 10), | |
| 94 | 98 | patch.object(_core, "_REGISTER_TIME", 10), | |
| 95 | 99 | patch.object(_core, "_UNREGISTER_TIME", 10), | |
| 100 | + patch.object(_core, "_PROBE_RANDOM_DELAY_INTERVAL", (1, 5)), | ||
| 96 | 101 | patch.object(service_browser, "_FIRST_QUERY_DELAY_RANDOM_INTERVAL", (1, 5)), | |
| 97 | 102 | ): | |
| 98 | 103 | yield | |
@@ -105,9 +110,11 @@ def quick_request_timing() -> Generator[None]: | |||
| 105 | 110 | The 200ms `_LISTENER_TIME` and 20-120ms random jitter (RFC 6762 | |
| 106 | 111 | §5.2) help spread queries from multiple clients on real networks. | |
| 107 | 112 | 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). | ||
| 111 | 118 | """ | |
| 112 | 119 | with ( | |
| 113 | 120 | patch.object(service_info, "_LISTENER_TIME", 10), | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -251,6 +251,7 @@ def test_service_info_rejects_expired_records(self): | |||
| 251 | 251 | ||
| 252 | 252 | @unittest.skipIf(not has_working_ipv6(), "Requires IPv6") | |
| 253 | 253 | @unittest.skipIf(os.environ.get("SKIP_IPV6"), "IPv6 tests disabled") | |
| 254 | + @pytest.mark.usefixtures("quick_request_timing") | ||
| 254 | 255 | def test_get_info_partial(self): | |
| 255 | 256 | zc = r.Zeroconf(interfaces=["127.0.0.1"]) | |
| 256 | 257 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -608,7 +608,7 @@ async def test_async_wait_unblocks_on_update(quick_timing: None) -> None: | |||
| 608 | 608 | ||
| 609 | 609 | ||
| 610 | 610 | @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: | ||
| 612 | 612 | """Test registering services broadcasts and query with AsyncServceInfo.async_request.""" | |
| 613 | 613 | if not has_working_ipv6() or os.environ.get("SKIP_IPV6"): | |
| 614 | 614 | pytest.skip("Requires IPv6") | |
@@ -710,13 +710,13 @@ async def test_service_info_async_request(quick_timing: None) -> None: | |||
| 710 | 710 | aiozc.zeroconf.out_delay_queue.queue.clear() | |
| 711 | 711 | aiosinfo = AsyncServiceInfo(type_, registration_name) | |
| 712 | 712 | _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 | ||
| 717 | 717 | # before the loop times out. | |
| 718 | 718 | with patch("zeroconf.asyncio.AsyncServiceInfo._is_complete", False): | |
| 719 | - await aiosinfo.async_request(aiozc.zeroconf, 1500) | ||
| 719 | + await aiosinfo.async_request(aiozc.zeroconf, 300) | ||
| 720 | 720 | assert aiosinfo is not None | |
| 721 | 721 | assert aiosinfo.addresses == [socket.inet_aton("10.0.1.3")] | |
| 722 | 722 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -33,7 +33,7 @@ def teardown_module(): | |||
| 33 | 33 | ||
| 34 | 34 | ||
| 35 | 35 | class ListenerTest(unittest.TestCase): | |
| 36 | - @pytest.mark.usefixtures("quick_timing") | ||
| 36 | + @pytest.mark.usefixtures("quick_timing", "quick_request_timing") | ||
| 37 | 37 | def test_integration_with_listener_class(self): | |
| 38 | 38 | sub_service_added = Event() | |
| 39 | 39 | service_added = Event() | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -86,18 +86,22 @@ async def _still_running(): | |||
| 86 | 86 | await asyncio.sleep(5) | |
| 87 | 87 | ||
| 88 | 88 | def _run_coro() -> None: | |
| 89 | - runcoro_thread_ready.set() | ||
| 90 | 89 | assert loop is not None | |
| 90 | + future = asyncio.run_coroutine_threadsafe(_still_running(), loop) | ||
| 91 | + runcoro_thread_ready.set() | ||
| 91 | 92 | with contextlib.suppress(concurrent.futures.TimeoutError): | |
| 92 | - asyncio.run_coroutine_threadsafe(_still_running(), loop).result(1) | ||
| 93 | + future.result(0.1) | ||
| 93 | 94 | ||
| 94 | 95 | runcoro_thread = threading.Thread(target=_run_coro, daemon=True) | |
| 95 | 96 | runcoro_thread.start() | |
| 96 | 97 | runcoro_thread_ready.wait() | |
| 97 | 98 | ||
| 98 | - time.sleep(0.1) | ||
| 99 | 99 | 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) | ||
| 101 | 105 | for _ in range(5): | |
| 102 | 106 | if not loop.is_running(): | |
| 103 | 107 | break | |
| Back | FazBrowse Home | New Git URL |
0 commit comments