Skip to content
Closed
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
23 changes: 21 additions & 2 deletions src/zeroconf/_services/browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
cast,
)

from .._cache import DNSCache
from .._dns import DNSPointer, DNSQuestion, DNSQuestionType, DNSRecord
from .._logger import log
from .._protocol.outgoing import DNSOutgoing
Expand Down Expand Up @@ -69,6 +70,8 @@
# https://datatracker.ietf.org/doc/html/rfc6762#section-5.2
_FIRST_QUERY_DELAY_RANDOM_INTERVAL = (20, 120) # ms

_BROWSER_BACKOFF_LIMIT_MS = _BROWSER_BACKOFF_LIMIT * 1000

_ON_CHANGE_DISPATCH = {
ServiceStateChange.Added: "add_service",
ServiceStateChange.Removed: "remove_service",
Expand Down Expand Up @@ -198,6 +201,7 @@ class QueryScheduler:

def __init__(
self,
cache: DNSCache,
types: Set[str],
delay: int,
first_random_delay_interval: Tuple[int, int],
Expand All @@ -207,6 +211,7 @@ def __init__(
self._next_time: Dict[str, float] = {}
self._first_random_delay_interval = first_random_delay_interval
self._delay: Dict[str, float] = {check_type_: delay for check_type_ in self._types}
self._cache = cache

def start(self, now: float) -> None:
"""Start the scheduler."""
Expand Down Expand Up @@ -251,9 +256,23 @@ def process_ready_types(self, now: float) -> List[str]:
if due > now:
continue

if self._delay[type_] == _BROWSER_BACKOFF_LIMIT_MS:
# Once we reach the backoff limit, we have a primed cache. If there are
# no stale PTR records because the record came in after we scheduled the
# query, we can reschedule the query to happen later.
next_refresh_time_by_record = [
record.get_expiration_time(_EXPIRE_REFRESH_TIME_PERCENT)
for record in self._cache.async_all_by_details(type_, _TYPE_PTR, _CLASS_IN)
]
if next_refresh_time_by_record:
recalculated_next_time = min(next_refresh_time_by_record)
if recalculated_next_time > now:
self._next_time[type_] = recalculated_next_time
continue

ready_types.append(type_)
self._next_time[type_] = now + self._delay[type_]
self._delay[type_] = min(_BROWSER_BACKOFF_LIMIT * 1000, self._delay[type_] * 2)
self._delay[type_] = min(_BROWSER_BACKOFF_LIMIT_MS, self._delay[type_] * 2)

return ready_types

Expand Down Expand Up @@ -301,7 +320,7 @@ def __init__(
self.question_type = question_type
self._pending_handlers: Dict[Tuple[str, str], ServiceStateChange] = {}
self._service_state_changed = Signal()
self.query_scheduler = QueryScheduler(self.types, delay, _FIRST_QUERY_DELAY_RANDOM_INTERVAL)
self.query_scheduler = QueryScheduler(zc.cache, self.types, delay, _FIRST_QUERY_DELAY_RANDOM_INTERVAL)
self.done = False
self._first_request: bool = True
self._next_send_timer: Optional[asyncio.TimerHandle] = None
Expand Down
4 changes: 3 additions & 1 deletion tests/services/test_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
current_time_millis,
millis_to_seconds,
)
from zeroconf._cache import DNSCache
from zeroconf._services import ServiceStateChange
from zeroconf._services.browser import ServiceBrowser
from zeroconf._services.info import ServiceInfo
Expand Down Expand Up @@ -991,7 +992,8 @@ async def test_generate_service_query_suppress_duplicate_questions():
async def test_query_scheduler():
delay = const._BROWSER_TIME
types_ = {"_hap._tcp.local.", "_http._tcp.local."}
query_scheduler = _services_browser.QueryScheduler(types_, delay, (0, 0))
cache = DNSCache()
query_scheduler = _services_browser.QueryScheduler(cache, types_, delay, (0, 0))
Comment on lines 993 to +996

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need another query scheduler test that interacts with a populated DNSCache before we can merge this


now = current_time_millis()
query_scheduler.start(now)
Expand Down