Skip to content
Merged
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
10 changes: 6 additions & 4 deletions src/zeroconf/_handlers/multicast_outgoing_queue.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ cdef object RAND_INT
cdef class MulticastOutgoingQueue:

cdef object zc
cdef object queue
cdef cython.uint additional_delay
cdef cython.uint aggregation_delay
cdef public object queue
cdef public object _multicast_delay_random_min
cdef public object _multicast_delay_random_max
cdef object _additional_delay
cdef object _aggregation_delay

@cython.locals(last_group=AnswerGroup, random_int=cython.uint, random_delay=float, send_after=float, send_before=float)
@cython.locals(last_group=AnswerGroup, random_int=cython.uint)
cpdef async_add(self, float now, cython.dict answers)

@cython.locals(pending=AnswerGroup)
Expand Down
24 changes: 17 additions & 7 deletions src/zeroconf/_handlers/multicast_outgoing_queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,31 +38,41 @@
from .._core import Zeroconf

_float = float
_int = int


class MulticastOutgoingQueue:
"""An outgoing queue used to aggregate multicast responses."""

__slots__ = ("zc", "queue", "additional_delay", "aggregation_delay")
__slots__ = (
"zc",
"queue",
"_multicast_delay_random_min",
"_multicast_delay_random_max",
"_additional_delay",
"_aggregation_delay",
)

def __init__(self, zeroconf: 'Zeroconf', additional_delay: int, max_aggregation_delay: int) -> None:
def __init__(self, zeroconf: 'Zeroconf', additional_delay: _int, max_aggregation_delay: _int) -> None:
self.zc = zeroconf
self.queue: deque[AnswerGroup] = deque()
# Additional delay is used to implement
# Protect the network against excessive packet flooding
# https://datatracker.ietf.org/doc/html/rfc6762#section-14
self.additional_delay = additional_delay
self.aggregation_delay = max_aggregation_delay
self._multicast_delay_random_min = MULTICAST_DELAY_RANDOM_INTERVAL[0]
self._multicast_delay_random_max = MULTICAST_DELAY_RANDOM_INTERVAL[1]
self._additional_delay = additional_delay
self._aggregation_delay = max_aggregation_delay

def async_add(self, now: _float, answers: _AnswerWithAdditionalsType) -> None:
"""Add a group of answers with additionals to the outgoing queue."""
loop = self.zc.loop
if TYPE_CHECKING:
assert loop is not None
random_int = RAND_INT(*MULTICAST_DELAY_RANDOM_INTERVAL)
random_delay = random_int + self.additional_delay
random_int = RAND_INT(self._multicast_delay_random_min, self._multicast_delay_random_max)
random_delay = random_int + self._additional_delay
send_after = now + random_delay
send_before = now + self.aggregation_delay + self.additional_delay
send_before = now + self._aggregation_delay + self._additional_delay
if len(self.queue):
# If we calculate a random delay for the send after time
# that is less than the last group scheduled to go out,
Expand Down
50 changes: 29 additions & 21 deletions tests/test_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@
import unittest
import unittest.mock
from typing import List, cast
from unittest.mock import patch

import pytest

import zeroconf as r
from zeroconf import ServiceInfo, Zeroconf, const, current_time_millis
from zeroconf._handlers import multicast_outgoing_queue
from zeroconf._handlers.multicast_outgoing_queue import (
MulticastOutgoingQueue,
construct_outgoing_multicast_answers,
Expand Down Expand Up @@ -1413,7 +1413,7 @@ async def test_response_aggregation_timings(run_isolated):
zc = aiozc.zeroconf
protocol = zc.engine.protocols[0]

with unittest.mock.patch.object(aiozc.zeroconf, "async_send") as send_mock:
with patch.object(aiozc.zeroconf, "async_send") as send_mock:
protocol.datagram_received(query.packets()[0], ('127.0.0.1', const._MDNS_PORT))
protocol.datagram_received(query2.packets()[0], ('127.0.0.1', const._MDNS_PORT))
protocol.datagram_received(query.packets()[0], ('127.0.0.1', const._MDNS_PORT))
Expand Down Expand Up @@ -1492,7 +1492,7 @@ async def test_response_aggregation_timings_multiple(run_isolated, disable_dupli
zc = aiozc.zeroconf
protocol = zc.engine.protocols[0]

with unittest.mock.patch.object(aiozc.zeroconf, "async_send") as send_mock:
with patch.object(aiozc.zeroconf, "async_send") as send_mock:
send_mock.reset_mock()
protocol.datagram_received(query2.packets()[0], ('127.0.0.1', const._MDNS_PORT))
protocol.last_time = 0 # manually reset the last time to avoid duplicate packet suppression
Expand Down Expand Up @@ -1581,16 +1581,19 @@ async def test_response_aggregation_random_delay():
outgoing_queue = MulticastOutgoingQueue(mocked_zc, 0, 500)

now = current_time_millis()
with unittest.mock.patch.object(multicast_outgoing_queue, "MULTICAST_DELAY_RANDOM_INTERVAL", (500, 600)):
outgoing_queue.async_add(now, {info.dns_pointer(): set()})
outgoing_queue._multicast_delay_random_min = 500
outgoing_queue._multicast_delay_random_max = 600
outgoing_queue.async_add(now, {info.dns_pointer(): set()})

# The second group should always be coalesced into first group since it will always come before
with unittest.mock.patch.object(multicast_outgoing_queue, "MULTICAST_DELAY_RANDOM_INTERVAL", (300, 400)):
outgoing_queue.async_add(now, {info2.dns_pointer(): set()})
outgoing_queue._multicast_delay_random_min = 300
outgoing_queue._multicast_delay_random_max = 400
outgoing_queue.async_add(now, {info2.dns_pointer(): set()})

# The third group should always be coalesced into first group since it will always come before
with unittest.mock.patch.object(multicast_outgoing_queue, "MULTICAST_DELAY_RANDOM_INTERVAL", (100, 200)):
outgoing_queue.async_add(now, {info3.dns_pointer(): set(), info4.dns_pointer(): set()})
outgoing_queue._multicast_delay_random_min = 100
outgoing_queue._multicast_delay_random_max = 200
outgoing_queue.async_add(now, {info3.dns_pointer(): set(), info4.dns_pointer(): set()})

assert len(outgoing_queue.queue) == 1
assert info.dns_pointer() in outgoing_queue.queue[0].answers
Expand All @@ -1599,8 +1602,9 @@ async def test_response_aggregation_random_delay():
assert info4.dns_pointer() in outgoing_queue.queue[0].answers

# The forth group should not be coalesced because its scheduled after the last group in the queue
with unittest.mock.patch.object(multicast_outgoing_queue, "MULTICAST_DELAY_RANDOM_INTERVAL", (700, 800)):
outgoing_queue.async_add(now, {info5.dns_pointer(): set()})
outgoing_queue._multicast_delay_random_min = 700
outgoing_queue._multicast_delay_random_max = 800
outgoing_queue.async_add(now, {info5.dns_pointer(): set()})

assert len(outgoing_queue.queue) == 2
assert info.dns_pointer() not in outgoing_queue.queue[1].answers
Expand Down Expand Up @@ -1630,21 +1634,22 @@ async def test_future_answers_are_removed_on_send():
outgoing_queue = MulticastOutgoingQueue(mocked_zc, 0, 0)

now = current_time_millis()
with unittest.mock.patch.object(multicast_outgoing_queue, "MULTICAST_DELAY_RANDOM_INTERVAL", (1, 1)):
outgoing_queue.async_add(now, {info.dns_pointer(): set()})
outgoing_queue._multicast_delay_random_min = 1
outgoing_queue._multicast_delay_random_max = 1
outgoing_queue.async_add(now, {info.dns_pointer(): set()})

assert len(outgoing_queue.queue) == 1

with unittest.mock.patch.object(multicast_outgoing_queue, "MULTICAST_DELAY_RANDOM_INTERVAL", (2, 2)):
outgoing_queue.async_add(now, {info.dns_pointer(): set()})
outgoing_queue._multicast_delay_random_min = 2
outgoing_queue._multicast_delay_random_max = 2
outgoing_queue.async_add(now, {info.dns_pointer(): set()})

assert len(outgoing_queue.queue) == 2

with unittest.mock.patch.object(
multicast_outgoing_queue, "MULTICAST_DELAY_RANDOM_INTERVAL", (1000, 1000)
):
outgoing_queue.async_add(now, {info2.dns_pointer(): set()})
outgoing_queue.async_add(now, {info.dns_pointer(): set()})
outgoing_queue._multicast_delay_random_min = 1000
outgoing_queue._multicast_delay_random_max = 1000
outgoing_queue.async_add(now, {info2.dns_pointer(): set()})
outgoing_queue.async_add(now, {info.dns_pointer(): set()})

assert len(outgoing_queue.queue) == 3

Expand Down Expand Up @@ -1676,6 +1681,9 @@ def async_update_records(self, zc: 'Zeroconf', now: float, records: List[r.Recor

zc.add_listener(MyListener(), None) # type: ignore[arg-type]
await asyncio.sleep(0) # flush out any call soons
assert "listeners passed to async_add_listener must inherit from RecordUpdateListener" in caplog.text
assert (
"listeners passed to async_add_listener must inherit from RecordUpdateListener" in caplog.text
or "TypeError: Argument \'listener\' has incorrect type" in caplog.text
)

await aiozc.async_close()