Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
cb109f7
Switch ServiceBrowser query scheduling to use call_later instead of a…
bdraco Jun 27, 2021
8a15237
reduce
bdraco Jun 27, 2021
23c064c
reduce
bdraco Jun 27, 2021
6d20581
attempt2
bdraco Jun 27, 2021
5bbea32
attempt reconfig
bdraco Jun 27, 2021
604a3e7
attempt reconfig
bdraco Jun 27, 2021
4c2f378
attempt reconfig
bdraco Jun 27, 2021
27ea8fa
attempt reconfig
bdraco Jun 27, 2021
900d2fc
attempt reconfig
bdraco Jun 27, 2021
cabf78a
attempt reconfig
bdraco Jun 27, 2021
fb4600e
attempt reconfig
bdraco Jun 27, 2021
ebbedad
attempt reconfig
bdraco Jun 27, 2021
1f692b9
attempt reconfig
bdraco Jun 27, 2021
e97e85a
attempt reconfig
bdraco Jun 27, 2021
c31c00b
attempt reconfig
bdraco Jun 27, 2021
e9f673c
attempt reconfig
bdraco Jun 27, 2021
acdaced
attempt reconfig
bdraco Jun 27, 2021
628bf26
attempt reconfig
bdraco Jun 27, 2021
135c841
attempt reconfig
bdraco Jun 27, 2021
491844d
attempt reconfig
bdraco Jun 27, 2021
59f8cbf
attempt reconfig
bdraco Jun 27, 2021
b28af92
attempt reconfig
bdraco Jun 27, 2021
5fb3f1c
attempt reconfig
bdraco Jun 27, 2021
fedc856
attempt reconfig
bdraco Jun 27, 2021
a84bbf9
attempt reconfig
bdraco Jun 27, 2021
f52d10e
attempt reconfig
bdraco Jun 27, 2021
219eae1
attempt reconfig
bdraco Jun 27, 2021
769a07f
attempt reconfig
bdraco Jun 27, 2021
830bf73
attempt reconfig
bdraco Jun 27, 2021
c816888
attempt reconfig
bdraco Jun 27, 2021
62b644d
attempt reconfig
bdraco Jun 27, 2021
021e845
attempt reconfig
bdraco Jun 27, 2021
38d3035
attempt reconfig
bdraco Jun 27, 2021
a943f15
attempt reconfig
bdraco Jun 27, 2021
4909956
attempt reconfig
bdraco Jun 27, 2021
24821a8
attempt reconfig
bdraco Jun 27, 2021
09db60f
attempt reconfig
bdraco Jun 27, 2021
ab8b254
attempt reconfig
bdraco Jun 27, 2021
a834884
attempt reconfig
bdraco Jun 27, 2021
1266f3c
attempt reconfig
bdraco Jun 27, 2021
00fda4e
attempt reconfig
bdraco Jun 27, 2021
97a405a
attempt reconfig
bdraco Jun 27, 2021
930124e
attempt reconfig
bdraco Jun 27, 2021
a4abe91
attempt reconfig
bdraco Jun 27, 2021
dff2e5b
attempt reconfig
bdraco Jun 27, 2021
22314db
attempt reconfig
bdraco Jun 27, 2021
4d4d577
attempt reconfig
bdraco Jun 27, 2021
d1fdb0f
attempt reconfig
bdraco Jun 27, 2021
495b68d
attempt reconfig
bdraco Jun 27, 2021
60941ee
attempt reconfig
bdraco Jun 27, 2021
09e9da1
attempt reconfig
bdraco Jun 27, 2021
e4f5e57
attempt reconfig
bdraco Jun 27, 2021
858e588
attempt reconfig
bdraco Jun 27, 2021
aa9e242
attempt reconfig
bdraco Jun 27, 2021
8c338e1
attempt reconfig
bdraco Jun 27, 2021
cd53498
attempt reconfig
bdraco Jun 27, 2021
0531fe8
attempt reconfig
bdraco Jun 27, 2021
05c4c6b
attempt reconfig
bdraco Jun 27, 2021
b5707de
attempt reconfig
bdraco Jun 27, 2021
17892d5
attempt reconfig
bdraco Jun 27, 2021
ad3b615
attempt reconfig
bdraco Jun 27, 2021
ea7106f
attempt reconfig
bdraco Jun 27, 2021
82d47de
attempt reconfig
bdraco Jun 27, 2021
2951f0a
attempt reconfig
bdraco Jun 27, 2021
e3eaeb5
attempt reconfig
bdraco Jun 27, 2021
08dd980
attempt reconfig
bdraco Jun 27, 2021
f4e04bc
attempt reconfig
bdraco Jun 27, 2021
799a368
attempt reconfig
bdraco Jun 27, 2021
bef4ba6
attempt reconfig
bdraco Jun 27, 2021
12c8c16
attempt reconfig
bdraco Jun 27, 2021
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
44 changes: 3 additions & 41 deletions tests/services/test_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -496,7 +496,7 @@ def on_service_state_change(zeroconf, service_type, state_change, name):
else:
assert not got_query.is_set()
time_offset += initial_query_interval
zeroconf_browser.loop.call_soon_threadsafe(browser.query_scheduler.set_schedule_changed)
zeroconf_browser.loop.call_soon_threadsafe(browser.schedule_changed)

finally:
browser.cancel()
Expand Down Expand Up @@ -984,8 +984,8 @@ async def test_query_scheduler():

# Test query interval is increasing
assert query_scheduler.millis_to_wait(now - 1) == 1
assert query_scheduler.millis_to_wait(now) is None
assert query_scheduler.millis_to_wait(now + 1) is None
assert query_scheduler.millis_to_wait(now) is 0
assert query_scheduler.millis_to_wait(now + 1) is 0

assert set(query_scheduler.process_ready_types(now)) == types_
assert set(query_scheduler.process_ready_types(now)) == set()
Expand Down Expand Up @@ -1018,41 +1018,3 @@ async def test_query_scheduler():
assert set(query_scheduler.process_ready_types(now + delay * 20)) == set()

assert set(query_scheduler.process_ready_types(now + delay * 31)) == set(["_http._tcp.local."])


@pytest.mark.asyncio
async def test_query_scheduler_triggers_async_wait_ready_on_reschedule():
"""Test that a reschedule wakes up the async_wait_ready."""
delay = const._BROWSER_TIME
types_ = set(["_hap._tcp.local.", "_http._tcp.local."])
query_scheduler = _services_browser.QueryScheduler(types_, delay, (0, 0))

now = current_time_millis()
query_scheduler.start(now)
assert set(query_scheduler.process_ready_types(now)) == types_
assert query_scheduler.millis_to_wait(now) == delay

task = asyncio.ensure_future(query_scheduler.async_wait_ready(now))
await asyncio.sleep(0) # Start the task
await asyncio.sleep(0) # Make sure its waiting
assert not task.done()
assert query_scheduler.millis_to_wait(now + 1) == delay - 1
query_scheduler.reschedule_type("_hap._tcp.local.", now + 1)
assert query_scheduler.millis_to_wait(now + 1) is None
await asyncio.wait_for(task, timeout=0.1)
assert task.done()

task2 = asyncio.ensure_future(query_scheduler.async_wait_ready(now + 10000))
assert set(query_scheduler.process_ready_types(now + 1)) == set(["_hap._tcp.local."])
assert not task2.done()
assert query_scheduler.millis_to_wait(now + 2) == delay - 2
query_scheduler.reschedule_type("_hap._tcp.local.", now + 2)
assert query_scheduler.millis_to_wait(now + 2) is None
await asyncio.wait_for(task2, timeout=0.1)
assert task2.done()
assert set(query_scheduler.process_ready_types(now + 10000)) == types_
assert query_scheduler.millis_to_wait(now + 10000) == delay * 2

task3 = asyncio.ensure_future(query_scheduler.async_wait_ready(now + 10000))
with pytest.raises(asyncio.TimeoutError):
await asyncio.wait_for(task3, timeout=0.1)
9 changes: 5 additions & 4 deletions tests/test_aio.py
Original file line number Diff line number Diff line change
Expand Up @@ -688,7 +688,7 @@ def on_service_state_change(zeroconf, service_type, state_change, name):

time_offset = 0.0

def current_time_millis():
def _new_current_time_millis():
"""Current system time in milliseconds"""
return (time.time() * 1000) + (time_offset * 1000)

Expand All @@ -705,7 +705,6 @@ def send(out, addr=const._MDNS_ADDR, port=const._MDNS_PORT, v6_flow_scope=()):
unexpected_ttl.set()

got_query.set()
got_query.clear()

old_send(out, addr=addr, port=port, v6_flow_scope=v6_flow_scope)

Expand Down Expand Up @@ -734,7 +733,7 @@ def send(out, addr=const._MDNS_ADDR, port=const._MDNS_PORT, v6_flow_scope=()):
), patch.object(
zeroconf_browser, "async_send", send
), patch(
"zeroconf._services.browser.current_time_millis", current_time_millis
"zeroconf._services.browser.current_time_millis", _new_current_time_millis
), patch.object(
_services_browser, "_BROWSER_BACKOFF_LIMIT", int(expected_ttl / 4)
):
Expand Down Expand Up @@ -762,9 +761,11 @@ def send(out, addr=const._MDNS_ADDR, port=const._MDNS_PORT, v6_flow_scope=()):
while nbr_answers < test_iterations:
# Increase simulated time shift by 1/4 of the TTL in seconds
time_offset += expected_ttl / 4
browser.query_scheduler.set_schedule_changed()
now = _new_current_time_millis()
browser.reschedule_type(type_, now)
sleep_count += 1
await asyncio.wait_for(got_query.wait(), 1)
got_query.clear()
# Prevent the test running indefinitely in an error condition
assert sleep_count < test_iterations * 4
assert not unexpected_ttl.is_set()
Expand Down
86 changes: 38 additions & 48 deletions zeroconf/_services/browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
"""

import asyncio
import contextlib
import queue
import random
import threading
Expand All @@ -39,7 +38,7 @@
SignalRegistrationInterface,
)
from .._updates import RecordUpdate, RecordUpdateListener
from .._utils.aio import get_best_available_queue, wait_event_or_timeout
from .._utils.aio import get_best_available_queue
from .._utils.name import service_type_name
from .._utils.time import current_time_millis, millis_to_seconds
from ..const import (
Expand Down Expand Up @@ -221,25 +220,17 @@ def _generate_first_next_time(self, now: float) -> None:
next_time = now + delay
self._next_time = {check_type_: next_time for check_type_ in self._types}

def millis_to_wait(self, now: float) -> Optional[float]:
def millis_to_wait(self, now: float) -> float:
"""Returns the number of milliseconds to wait for the next event."""
# Wait for the type has the smallest next time
next_time = min(self._next_time.values())
return None if next_time <= now else next_time - now
return 0 if next_time <= now else next_time - now

def reschedule_type(self, type_: str, next_time: float) -> None:
"""Reschedule the query for a type to happen sooner."""
if next_time >= self._next_time[type_]:
return

self._next_time[type_] = next_time
self.set_schedule_changed()

def set_schedule_changed(self) -> None:
"""Set the event to unblock async_wait_ready to make sure the adjusted next time is seen."""
assert self._schedule_changed_event is not None
self._schedule_changed_event.set()
self._schedule_changed_event.clear()

def process_ready_types(self, now: float) -> List[str]:
"""Generate a list of ready types that is due and schedule the next time."""
Expand All @@ -258,13 +249,6 @@ def process_ready_types(self, now: float) -> List[str]:

return ready_types

async def async_wait_ready(self, now: float) -> None:
"""Wait for at least one query to be ready."""
timeout = self.millis_to_wait(now)
if timeout:
assert self._schedule_changed_event is not None
await wait_event_or_timeout(self._schedule_changed_event, timeout=millis_to_seconds(timeout))


class _ServiceBrowserBase(RecordUpdateListener):
"""Base class for ServiceBrowser."""
Expand Down Expand Up @@ -302,7 +286,6 @@ def __init__(
for check_type_ in self.types:
# Will generate BadTypeInNameException on a bad name
service_type_name(check_type_, strict=False)
self._browser_task: Optional[asyncio.Task] = None
self.zc = zc
self.addr = addr
self.port = port
Expand All @@ -313,6 +296,8 @@ def __init__(
self.query_scheduler = QueryScheduler(self.types, delay, _FIRST_QUERY_DELAY_RANDOM_INTERVAL)
self.queue: Optional[queue.Queue] = None
self.done = False
self._first_request: bool = True
self._next_send_timer: Optional[asyncio.TimerHandle] = None

if hasattr(handlers, 'add_service'):
listener = cast('ServiceListener', handlers)
Expand All @@ -335,7 +320,7 @@ def _async_start(self) -> None:
self.query_scheduler.start(current_time_millis())
self.zc.async_add_listener(self, [DNSQuestion(type_, _TYPE_PTR, _CLASS_IN) for type_ in self.types])
# Only start queries after the listener is installed
self._browser_task = cast(asyncio.Task, asyncio.ensure_future(self.async_browser_task()))
asyncio.ensure_future(self._async_start_query_sender())

@property
def service_state_changed(self) -> SignalRegistrationInterface:
Expand Down Expand Up @@ -378,9 +363,7 @@ def _async_process_record_update(
elif expired:
self._enqueue_callback(ServiceStateChange.Removed, record.name, record.alias)
else:
self.query_scheduler.reschedule_type(
record.name, record.get_expiration_time(_EXPIRE_REFRESH_TIME_PERCENT)
)
self.reschedule_type(record.name, record.get_expiration_time(_EXPIRE_REFRESH_TIME_PERCENT))
return

# If its expired or already exists in the cache it cannot be updated.
Expand Down Expand Up @@ -448,6 +431,7 @@ def _fire_service_state_changed_event(self, event: Tuple[Tuple[str, str], Servic
def _async_cancel(self) -> None:
"""Cancel the browser."""
self.done = True
self._cancel_send_timer()
self.zc.async_remove_listener(self)

def _generate_ready_queries(self, first_request: bool) -> List[DNSOutgoing]:
Expand All @@ -464,28 +448,40 @@ def _generate_ready_queries(self, first_request: bool) -> List[DNSOutgoing]:
question_type = DNSQuestionType.QU if not self.question_type and first_request else self.question_type
return generate_service_query(self.zc, now, ready_types, self.multicast, question_type)

async def async_browser_task(self) -> None:
"""Run the browser task."""
async def _async_start_query_sender(self) -> None:
"""Start scheduling queries."""
await self.zc.async_wait_for_start()
first_request = True
while True:
await self.query_scheduler.async_wait_ready(current_time_millis())
outs = self._generate_ready_queries(first_request)
if not outs:
continue
self._async_send_ready_queries_schedule_next()

def _cancel_send_timer(self) -> None:
"""Cancel the next send."""
if self._next_send_timer:
self._next_send_timer.cancel()

first_request = False
def reschedule_type(self, type_: str, next_time: float) -> None:
"""Reschedule a type to be refreshed in the future."""
self.query_scheduler.reschedule_type(type_, next_time)
self.schedule_changed()

def schedule_changed(self) -> None:
"""Called when the schedule has changed."""
self._cancel_send_timer()
self._async_send_ready_queries_schedule_next()

def _async_send_ready_queries_schedule_next(self) -> None:
"""Send any ready queries and scheule the next time."""
if self.done or self.zc.done:
return

outs = self._generate_ready_queries(self._first_request)
if outs:
self._first_request = False
for out in outs:
self.zc.async_send(out, addr=self.addr, port=self.port)

async def _async_cancel_browser(self) -> None:
"""Cancel the browser."""
assert self._browser_task is not None
self._browser_task.cancel()
browser_task = self._browser_task
self._browser_task = None
with contextlib.suppress(asyncio.CancelledError):
await browser_task
assert self.zc.loop is not None
delay = millis_to_seconds(self.query_scheduler.millis_to_wait(current_time_millis()))
self._next_send_timer = self.zc.loop.call_later(delay, self._async_send_ready_queries_schedule_next)


class ServiceBrowser(_ServiceBrowserBase, threading.Thread):
Expand Down Expand Up @@ -523,18 +519,12 @@ def __init__(
getattr(self, 'native_id', self.ident),
)

def _async_cancel_soon(self) -> None:
"""Cancel the browser from the event loop."""
self._async_cancel()
if self._browser_task:
asyncio.ensure_future(self._async_cancel_browser())

def cancel(self) -> None:
"""Cancel the browser."""
assert self.zc.loop is not None
assert self.queue is not None
self.queue.put(None)
self.zc.loop.call_soon_threadsafe(self._async_cancel_soon)
self.zc.loop.call_soon_threadsafe(self._async_cancel)
self.join()

def run(self) -> None:
Expand Down
2 changes: 0 additions & 2 deletions zeroconf/aio.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,6 @@ def __init__(
async def async_cancel(self) -> None:
"""Cancel the browser."""
self._async_cancel()
with contextlib.suppress(asyncio.CancelledError):
await self._async_cancel_browser()


class AsyncZeroconfServiceTypes(ZeroconfServiceTypes):
Expand Down