feat: make ServiceInfo aware of question history (#1348) · python-zeroconf/python-zeroconf@b9aae1d

@@ -26,16 +26,19 @@

2626

from ipaddress import IPv4Address, IPv6Address, _BaseAddress

2727

from typing import TYPE_CHECKING, Dict, List, Optional, Set, Union, cast

282829+

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,

8993

bytes_ = bytes

9094

float_ = float

9195

int_ = 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

9610297103

if TYPE_CHECKING:

98104

from .._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+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

805817806818

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

828847829848

await self.async_wait(min(next_, last) - now, zc.loop)

830849

now = current_time_millis()

@@ -833,21 +852,57 @@ async def async_request(

833852834853

return 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+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

852907853908

def __repr__(self) -> str: