feat: make ServiceInfo aware of question history (#1348) · python-zeroconf/python-zeroconf@b9aae1d
@@ -26,16 +26,19 @@
2626from ipaddress import IPv4Address, IPv6Address, _BaseAddress
2727from typing import TYPE_CHECKING, Dict, List, Optional, Set, Union, cast
282829+from .._cache import DNSCache
2930from .._dns import (
3031DNSAddress,
3132DNSNsec,
3233DNSPointer,
34+DNSQuestion,
3335DNSQuestionType,
3436DNSRecord,
3537DNSService,
3638DNSText,
3739)
3840from .._exceptions import BadTypeInNameException
41+from .._history import QuestionHistory
3942from .._logger import log
4043from .._protocol.outgoing import DNSOutgoing
4144from .._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,
8993bytes_ = bytes
9094float_ = float
9195int_ = int
96+str_ = str
929793-DNS_QUESTION_TYPE_QU = DNSQuestionType.QU
94-DNS_QUESTION_TYPE_QM = DNSQuestionType.QM
98+QU_QUESTION = DNSQuestionType.QU
99+QM_QUESTION = DNSQuestionType.QM
95100101+randint = random.randint
9610297103if TYPE_CHECKING:
98104from .._core import Zeroconf
@@ -774,6 +780,12 @@ def request(
774780 )
775781 )
776782783+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+777789async def async_request(
778790self,
779791zc: 'Zeroconf',
@@ -804,7 +816,7 @@ async def async_request(
804816assert zc.loop is not None
805817806818first_request = True
807-delay = _LISTENER_TIME
819+delay = self._get_initial_delay()
808820next_ = now
809821last = now + timeout
810822try:
@@ -813,18 +825,25 @@ async def async_request(
813825if last <= now:
814826return False
815827if 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)
821830first_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)
825838next_ = 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
828847829848await self.async_wait(min(next_, last) - now, zc.loop)
830849now = current_time_millis()
@@ -833,21 +852,57 @@ async def async_request(
833852834853return True
835854855+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+836884def _generate_request_query(
837885self, zc: 'Zeroconf', now: float_, question_type: DNSQuestionType
838886 ) -> DNSOutgoing:
839887"""Generate the request query."""
840888out = DNSOutgoing(_FLAGS_QR_QUERY)
841889name = self._name
842-server_or_name = self.server or name
890+server = self.server or name
843891cache = 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+ )
851906return out
852907853908def __repr__(self) -> str: