Skip to content

Commit 963d022

Browse files
authored
feat: improve performance of ServiceBrowser outgoing query scheduler (#1170)
1 parent 90410a2 commit 963d022

2 files changed

Lines changed: 73 additions & 1 deletion

File tree

src/zeroconf/_services/browser.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -460,13 +460,18 @@ def _cancel_send_timer(self) -> None:
460460
"""Cancel the next send."""
461461
if self._next_send_timer:
462462
self._next_send_timer.cancel()
463+
self._next_send_timer = None
463464

464465
def reschedule_type(self, type_: str, now: float, next_time: float) -> None:
465466
"""Reschedule a type to be refreshed in the future."""
466467
if self.query_scheduler.reschedule_type(type_, next_time):
468+
# We need to send the queries before rescheduling the next one
469+
# otherwise we may be scheduling a query to go out in the next
470+
# iteration of the event loop which should be sent now.
471+
if now >= next_time:
472+
self._async_send_ready_queries(now)
467473
self._cancel_send_timer()
468474
self._async_schedule_next(now)
469-
self._async_send_ready_queries(now)
470475

471476
def _async_send_ready_queries(self, now: float) -> None:
472477
"""Send any ready queries."""

tests/test_asyncio.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -996,6 +996,9 @@ def send(out, addr=const._MDNS_ADDR, port=const._MDNS_PORT, v6_flow_scope=()):
996996
# Increase simulated time shift by 1/4 of the TTL in seconds
997997
time_offset += expected_ttl / 4
998998
now = _new_current_time_millis()
999+
# Force the next query to be sent since we are testing
1000+
# to see if the query contains answers and not the scheduler
1001+
browser.query_scheduler._next_time[type_] = now + (1000 * expected_ttl)
9991002
browser.reschedule_type(type_, now, now)
10001003
sleep_count += 1
10011004
await asyncio.wait_for(got_query.wait(), 1)
@@ -1244,3 +1247,67 @@ def update_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-de
12441247
('add', '_http._tcp.local.', 'ShellyPro4PM-94B97EC07650._http._tcp.local.'),
12451248
('update', '_http._tcp.local.', 'ShellyPro4PM-94B97EC07650._http._tcp.local.'),
12461249
]
1250+
1251+
1252+
@pytest.mark.asyncio
1253+
async def test_service_browser_does_not_try_to_send_if_not_ready():
1254+
"""Test that the service browser does not try to send if not ready when rescheduling a type."""
1255+
service_added = asyncio.Event()
1256+
type_ = "_http._tcp.local."
1257+
registration_name = "nosend.%s" % type_
1258+
1259+
def on_service_state_change(zeroconf, service_type, state_change, name):
1260+
if name == registration_name:
1261+
if state_change is ServiceStateChange.Added:
1262+
service_added.set()
1263+
1264+
aiozc = AsyncZeroconf(interfaces=['127.0.0.1'])
1265+
zeroconf_browser = aiozc.zeroconf
1266+
await zeroconf_browser.async_wait_for_start()
1267+
1268+
expected_ttl = const._DNS_HOST_TTL
1269+
time_offset = 0.0
1270+
1271+
def _new_current_time_millis():
1272+
"""Current system time in milliseconds"""
1273+
return (time.monotonic() * 1000) + (time_offset * 1000)
1274+
1275+
assert len(zeroconf_browser.engine.protocols) == 2
1276+
1277+
aio_zeroconf_registrar = AsyncZeroconf(interfaces=['127.0.0.1'])
1278+
zeroconf_registrar = aio_zeroconf_registrar.zeroconf
1279+
await aio_zeroconf_registrar.zeroconf.async_wait_for_start()
1280+
assert len(zeroconf_registrar.engine.protocols) == 2
1281+
with patch("zeroconf._services.browser.current_time_millis", _new_current_time_millis):
1282+
service_added = asyncio.Event()
1283+
browser = AsyncServiceBrowser(zeroconf_browser, type_, [on_service_state_change])
1284+
desc = {'path': '/~paulsm/'}
1285+
info = ServiceInfo(
1286+
type_, registration_name, 80, 0, 0, desc, "ash-2.local.", addresses=[socket.inet_aton("10.0.1.2")]
1287+
)
1288+
task = await aio_zeroconf_registrar.async_register_service(info)
1289+
await task
1290+
1291+
try:
1292+
await asyncio.wait_for(service_added.wait(), 1)
1293+
time_offset = 1000 * expected_ttl # set the time to the end of the ttl
1294+
now = _new_current_time_millis()
1295+
browser.query_scheduler._next_time[type_] = now + (1000 * expected_ttl)
1296+
# Make sure the query schedule is to a time in the future
1297+
# so we will reschedule
1298+
with patch.object(
1299+
browser, "_async_send_ready_queries"
1300+
) as _async_send_ready_queries, patch.object(
1301+
browser, "_async_send_ready_queries_schedule_next"
1302+
) as _async_send_ready_queries_schedule_next:
1303+
# Reschedule the type to be sent in 1ms in the future
1304+
# to make sure the query is not sent
1305+
browser.reschedule_type(type_, now, now + 1)
1306+
assert not _async_send_ready_queries.called
1307+
await asyncio.sleep(0.01)
1308+
# Make sure it does happen after the sleep
1309+
assert _async_send_ready_queries_schedule_next.called
1310+
finally:
1311+
await aio_zeroconf_registrar.async_close()
1312+
await browser.async_cancel()
1313+
await aiozc.async_close()

0 commit comments

Comments
 (0)