Skip to content

Commit b9aae1d

Browse files
authored
feat: make ServiceInfo aware of question history (#1348)
1 parent cf40470 commit b9aae1d

File tree

6 files changed

+234
-66
lines changed

6 files changed

+234
-66
lines changed

src/zeroconf/_history.pxd

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ cdef class QuestionHistory:
99

1010
cdef cython.dict _history
1111

12-
cpdef add_question_at_time(self, DNSQuestion question, double now, cython.set known_answers)
12+
cpdef void add_question_at_time(self, DNSQuestion question, double now, cython.set known_answers)
1313

1414
@cython.locals(than=double, previous_question=cython.tuple, previous_known_answers=cython.set)
1515
cpdef bint suppresses(self, DNSQuestion question, double now, cython.set known_answers)
1616

1717
@cython.locals(than=double, now_known_answers=cython.tuple)
18-
cpdef async_expire(self, double now)
18+
cpdef void async_expire(self, double now)

src/zeroconf/_protocol/outgoing.pxd

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11

22
import cython
33

4-
from .._cache cimport DNSCache
54
from .._dns cimport DNSEntry, DNSPointer, DNSQuestion, DNSRecord
65
from .incoming cimport DNSIncoming
76

@@ -127,20 +126,16 @@ cdef class DNSOutgoing:
127126
)
128127
cpdef packets(self)
129128

130-
cpdef add_question_or_all_cache(self, DNSCache cache, double now, str name, object type_, object class_)
129+
cpdef void add_question(self, DNSQuestion question)
131130

132-
cpdef add_question_or_one_cache(self, DNSCache cache, double now, str name, object type_, object class_)
133-
134-
cpdef add_question(self, DNSQuestion question)
135-
136-
cpdef add_answer(self, DNSIncoming inp, DNSRecord record)
131+
cpdef void add_answer(self, DNSIncoming inp, DNSRecord record)
137132

138133
@cython.locals(now_double=double)
139-
cpdef add_answer_at_time(self, DNSRecord record, double now)
134+
cpdef void add_answer_at_time(self, DNSRecord record, double now)
140135

141-
cpdef add_authorative_answer(self, DNSPointer record)
136+
cpdef void add_authorative_answer(self, DNSPointer record)
142137

143-
cpdef add_additional_answer(self, DNSRecord record)
138+
cpdef void add_additional_answer(self, DNSRecord record)
144139

145140
cpdef bint is_query(self)
146141

src/zeroconf/_protocol/outgoing.py

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
from struct import Struct
2626
from typing import TYPE_CHECKING, Dict, List, Optional, Sequence, Tuple, Union
2727

28-
from .._cache import DNSCache
2928
from .._dns import DNSPointer, DNSQuestion, DNSRecord
3029
from .._exceptions import NamePartTooLongException
3130
from .._logger import log
@@ -198,29 +197,6 @@ def add_additional_answer(self, record: DNSRecord) -> None:
198197
"""
199198
self.additionals.append(record)
200199

201-
def add_question_or_one_cache(
202-
self, cache: DNSCache, now: float_, name: str_, type_: int_, class_: int_
203-
) -> None:
204-
"""Add a question if it is not already cached."""
205-
cached_entry = cache.get_by_details(name, type_, class_)
206-
if not cached_entry:
207-
self.add_question(DNSQuestion(name, type_, class_))
208-
else:
209-
self.add_answer_at_time(cached_entry, now)
210-
211-
def add_question_or_all_cache(
212-
self, cache: DNSCache, now: float_, name: str_, type_: int_, class_: int_
213-
) -> None:
214-
"""Add a question if it is not already cached.
215-
This is currently only used for IPv6 addresses.
216-
"""
217-
cached_entries = cache.get_all_by_details(name, type_, class_)
218-
if not cached_entries:
219-
self.add_question(DNSQuestion(name, type_, class_))
220-
return
221-
for cached_entry in cached_entries:
222-
self.add_answer_at_time(cached_entry, now)
223-
224200
def _write_byte(self, value: int_) -> None:
225201
"""Writes a single byte to the packet"""
226202
self.data.append(BYTE_TABLE[value])

src/zeroconf/_services/info.pxd

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,16 @@
22
import cython
33

44
from .._cache cimport DNSCache
5-
from .._dns cimport DNSAddress, DNSNsec, DNSPointer, DNSRecord, DNSService, DNSText
5+
from .._dns cimport (
6+
DNSAddress,
7+
DNSNsec,
8+
DNSPointer,
9+
DNSQuestion,
10+
DNSRecord,
11+
DNSService,
12+
DNSText,
13+
)
14+
from .._history cimport QuestionHistory
615
from .._protocol.outgoing cimport DNSOutgoing
716
from .._record_update cimport RecordUpdate
817
from .._updates cimport RecordUpdateListener
@@ -27,18 +36,22 @@ cdef object _FLAGS_QR_QUERY
2736

2837
cdef object service_type_name
2938

30-
cdef object DNS_QUESTION_TYPE_QU
31-
cdef object DNS_QUESTION_TYPE_QM
39+
cdef object QU_QUESTION
40+
cdef object QM_QUESTION
3241

3342
cdef object _IPVersion_All_value
3443
cdef object _IPVersion_V4Only_value
3544

3645
cdef cython.set _ADDRESS_RECORD_TYPES
3746

47+
cdef unsigned int _DUPLICATE_QUESTION_INTERVAL
48+
3849
cdef bint TYPE_CHECKING
3950
cdef bint IPADDRESS_SUPPORTS_SCOPE_ID
4051
cdef object cached_ip_addresses
4152

53+
cdef object randint
54+
4255
cdef class ServiceInfo(RecordUpdateListener):
4356

4457
cdef public cython.bytes text
@@ -123,5 +136,23 @@ cdef class ServiceInfo(RecordUpdateListener):
123136

124137
cpdef void async_clear_cache(self)
125138

126-
@cython.locals(cache=DNSCache)
139+
@cython.locals(cache=DNSCache, history=QuestionHistory, out=DNSOutgoing, qu_question=bint)
127140
cdef DNSOutgoing _generate_request_query(self, object zc, double now, object question_type)
141+
142+
@cython.locals(question=DNSQuestion, answer=DNSRecord)
143+
cdef void _add_question_with_known_answers(
144+
self,
145+
DNSOutgoing out,
146+
bint qu_question,
147+
QuestionHistory question_history,
148+
DNSCache cache,
149+
double now,
150+
str name,
151+
object type_,
152+
object class_,
153+
bint skip_if_known_answers
154+
)
155+
156+
cdef double _get_initial_delay(self)
157+
158+
cdef double _get_random_delay(self)

src/zeroconf/_services/info.py

Lines changed: 76 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,19 @@
2626
from ipaddress import IPv4Address, IPv6Address, _BaseAddress
2727
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Union, cast
2828

29+
from .._cache import DNSCache
2930
from .._dns import (
3031
DNSAddress,
3132
DNSNsec,
3233
DNSPointer,
34+
DNSQuestion,
3335
DNSQuestionType,
3436
DNSRecord,
3537
DNSService,
3638
DNSText,
3739
)
3840
from .._exceptions import BadTypeInNameException
41+
from .._history import QuestionHistory
3942
from .._logger import log
4043
from .._protocol.outgoing import DNSOutgoing
4144
from .._record_update import RecordUpdate
@@ -61,6 +64,7 @@
6164
_CLASS_IN_UNIQUE,
6265
_DNS_HOST_TTL,
6366
_DNS_OTHER_TTL,
67+
_DUPLICATE_QUESTION_INTERVAL,
6468
_FLAGS_QR_QUERY,
6569
_LISTENER_TIME,
6670
_MDNS_PORT,
@@ -89,10 +93,12 @@
8993
bytes_ = bytes
9094
float_ = float
9195
int_ = int
96+
str_ = str
9297

93-
DNS_QUESTION_TYPE_QU = DNSQuestionType.QU
94-
DNS_QUESTION_TYPE_QM = DNSQuestionType.QM
98+
QU_QUESTION = DNSQuestionType.QU
99+
QM_QUESTION = DNSQuestionType.QM
95100

101+
randint = random.randint
96102

97103
if TYPE_CHECKING:
98104
from .._core import Zeroconf
@@ -774,6 +780,12 @@ def request(
774780
)
775781
)
776782

783+
def _get_initial_delay(self) -> float_:
784+
return _LISTENER_TIME
785+
786+
def _get_random_delay(self) -> int_:
787+
return randint(*_AVOID_SYNC_DELAY_RANDOM_INTERVAL)
788+
777789
async def async_request(
778790
self,
779791
zc: 'Zeroconf',
@@ -804,7 +816,7 @@ async def async_request(
804816
assert zc.loop is not None
805817

806818
first_request = True
807-
delay = _LISTENER_TIME
819+
delay = self._get_initial_delay()
808820
next_ = now
809821
last = now + timeout
810822
try:
@@ -813,18 +825,25 @@ async def async_request(
813825
if last <= now:
814826
return False
815827
if next_ <= now:
816-
out = self._generate_request_query(
817-
zc,
818-
now,
819-
question_type or DNS_QUESTION_TYPE_QU if first_request else DNS_QUESTION_TYPE_QM,
820-
)
828+
this_question_type = question_type or QU_QUESTION if first_request else QM_QUESTION
829+
out = self._generate_request_query(zc, now, this_question_type)
821830
first_request = False
822-
if not out.questions:
823-
return self._load_from_cache(zc, now)
824-
zc.async_send(out, addr, port)
831+
if out.questions:
832+
# All questions may have been suppressed
833+
# by the question history, so nothing to send,
834+
# but keep waiting for answers in case another
835+
# client on the network is asking the same
836+
# question or they have not arrived yet.
837+
zc.async_send(out, addr, port)
825838
next_ = now + delay
826-
delay *= 2
827-
next_ += random.randint(*_AVOID_SYNC_DELAY_RANDOM_INTERVAL)
839+
next_ += self._get_random_delay()
840+
if this_question_type is QM_QUESTION and delay < _DUPLICATE_QUESTION_INTERVAL:
841+
# If we just asked a QM question, we need to
842+
# wait at least the duplicate question interval
843+
# before asking another QM question otherwise
844+
# its likely to be suppressed by the question
845+
# history of the remote responder.
846+
delay = _DUPLICATE_QUESTION_INTERVAL
828847

829848
await self.async_wait(min(next_, last) - now, zc.loop)
830849
now = current_time_millis()
@@ -833,21 +852,57 @@ async def async_request(
833852

834853
return True
835854

855+
def _add_question_with_known_answers(
856+
self,
857+
out: DNSOutgoing,
858+
qu_question: bool,
859+
question_history: QuestionHistory,
860+
cache: DNSCache,
861+
now: float_,
862+
name: str_,
863+
type_: int_,
864+
class_: int_,
865+
skip_if_known_answers: bool,
866+
) -> None:
867+
"""Add a question with known answers if its not suppressed."""
868+
known_answers = {
869+
answer for answer in cache.get_all_by_details(name, type_, class_) if not answer.is_stale(now)
870+
}
871+
if skip_if_known_answers and known_answers:
872+
return
873+
question = DNSQuestion(name, type_, class_)
874+
if qu_question:
875+
question.unicast = True
876+
elif question_history.suppresses(question, now, known_answers):
877+
return
878+
else:
879+
question_history.add_question_at_time(question, now, known_answers)
880+
out.add_question(question)
881+
for answer in known_answers:
882+
out.add_answer_at_time(answer, now)
883+
836884
def _generate_request_query(
837885
self, zc: 'Zeroconf', now: float_, question_type: DNSQuestionType
838886
) -> DNSOutgoing:
839887
"""Generate the request query."""
840888
out = DNSOutgoing(_FLAGS_QR_QUERY)
841889
name = self._name
842-
server_or_name = self.server or name
890+
server = self.server or name
843891
cache = zc.cache
844-
out.add_question_or_one_cache(cache, now, name, _TYPE_SRV, _CLASS_IN)
845-
out.add_question_or_one_cache(cache, now, name, _TYPE_TXT, _CLASS_IN)
846-
out.add_question_or_all_cache(cache, now, server_or_name, _TYPE_A, _CLASS_IN)
847-
out.add_question_or_all_cache(cache, now, server_or_name, _TYPE_AAAA, _CLASS_IN)
848-
if question_type is DNS_QUESTION_TYPE_QU:
849-
for question in out.questions:
850-
question.unicast = True
892+
history = zc.question_history
893+
qu_question = question_type is QU_QUESTION
894+
self._add_question_with_known_answers(
895+
out, qu_question, history, cache, now, name, _TYPE_SRV, _CLASS_IN, True
896+
)
897+
self._add_question_with_known_answers(
898+
out, qu_question, history, cache, now, name, _TYPE_TXT, _CLASS_IN, True
899+
)
900+
self._add_question_with_known_answers(
901+
out, qu_question, history, cache, now, server, _TYPE_A, _CLASS_IN, False
902+
)
903+
self._add_question_with_known_answers(
904+
out, qu_question, history, cache, now, server, _TYPE_AAAA, _CLASS_IN, False
905+
)
851906
return out
852907

853908
def __repr__(self) -> str:

0 commit comments

Comments
 (0)