Skip to content

Commit 3e89294

Browse files
authored
feat: optimize processing of records in RecordUpdateListener subclasses (#1231)
1 parent b492eb4 commit 3e89294

4 files changed

Lines changed: 136 additions & 123 deletions

File tree

src/zeroconf/_services/browser.py

Lines changed: 33 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
cast,
4040
)
4141

42-
from .._dns import DNSPointer, DNSQuestion, DNSQuestionType, DNSRecord
42+
from .._dns import DNSPointer, DNSQuestion, DNSQuestionType
4343
from .._logger import log
4444
from .._protocol.outgoing import DNSOutgoing
4545
from .._services import (
@@ -383,50 +383,46 @@ def _enqueue_callback(
383383
):
384384
self._pending_handlers[key] = state_change
385385

386-
def _async_process_record_update(
387-
self, now: float, record: DNSRecord, old_record: Optional[DNSRecord]
388-
) -> None:
389-
"""Process a single record update from a batch of updates."""
390-
record_type = record.type
391-
392-
if record_type is _TYPE_PTR:
393-
if TYPE_CHECKING:
394-
record = cast(DNSPointer, record)
395-
for type_ in self.types.intersection(cached_possible_types(record.name)):
396-
if old_record is None:
397-
self._enqueue_callback(ServiceStateChange.Added, type_, record.alias)
398-
elif record.is_expired(now):
399-
self._enqueue_callback(ServiceStateChange.Removed, type_, record.alias)
400-
else:
401-
self.reschedule_type(type_, now, record.get_expiration_time(_EXPIRE_REFRESH_TIME_PERCENT))
402-
return
403-
404-
# If its expired or already exists in the cache it cannot be updated.
405-
if old_record or record.is_expired(now):
406-
return
407-
408-
if record_type in _ADDRESS_RECORD_TYPES:
409-
# Iterate through the DNSCache and callback any services that use this address
410-
for type_, name in self._names_matching_types(
411-
{service.name for service in self.zc.cache.async_entries_with_server(record.name)}
412-
):
413-
self._enqueue_callback(ServiceStateChange.Updated, type_, name)
414-
return
415-
416-
for type_, name in self._names_matching_types((record.name,)):
417-
self._enqueue_callback(ServiceStateChange.Updated, type_, name)
418-
419386
def async_update_records(self, zc: 'Zeroconf', now: float, records: List[RecordUpdate]) -> None:
420387
"""Callback invoked by Zeroconf when new information arrives.
421388
422389
Updates information required by browser in the Zeroconf cache.
423390
424-
Ensures that there is are no unecessary duplicates in the list.
391+
Ensures that there is are no unnecessary duplicates in the list.
425392
426393
This method will be run in the event loop.
427394
"""
428-
for record in records:
429-
self._async_process_record_update(now, record[0], record[1])
395+
for record_update in records:
396+
record, old_record = record_update
397+
record_type = record.type
398+
399+
if record_type is _TYPE_PTR:
400+
if TYPE_CHECKING:
401+
record = cast(DNSPointer, record)
402+
for type_ in self.types.intersection(cached_possible_types(record.name)):
403+
if old_record is None:
404+
self._enqueue_callback(ServiceStateChange.Added, type_, record.alias)
405+
elif record.is_expired(now):
406+
self._enqueue_callback(ServiceStateChange.Removed, type_, record.alias)
407+
else:
408+
expire_time = record.get_expiration_time(_EXPIRE_REFRESH_TIME_PERCENT)
409+
self.reschedule_type(type_, now, expire_time)
410+
continue
411+
412+
# If its expired or already exists in the cache it cannot be updated.
413+
if old_record or record.is_expired(now):
414+
continue
415+
416+
if record_type in _ADDRESS_RECORD_TYPES:
417+
# Iterate through the DNSCache and callback any services that use this address
418+
for type_, name in self._names_matching_types(
419+
{service.name for service in self.zc.cache.async_entries_with_server(record.name)}
420+
):
421+
self._enqueue_callback(ServiceStateChange.Updated, type_, name)
422+
continue
423+
424+
for type_, name in self._names_matching_types((record.name,)):
425+
self._enqueue_callback(ServiceStateChange.Updated, type_, name)
430426

431427
@abstractmethod
432428
def async_update_records_complete(self) -> None:

src/zeroconf/_services/info.py

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -410,35 +410,17 @@ def _set_ipv4_addresses_from_cache(self, zc: 'Zeroconf', now: float) -> None:
410410
else:
411411
self._ipv4_addresses = self._get_ip_addresses_from_cache_lifo(zc, now, _TYPE_A)
412412

413-
def update_record(self, zc: 'Zeroconf', now: float, record: Optional[DNSRecord]) -> None:
414-
"""Updates service information from a DNS record.
415-
416-
This method is deprecated and will be removed in a future version.
417-
update_records should be implemented instead.
418-
419-
This method will be run in the event loop.
420-
"""
421-
if record is not None:
422-
self._process_record_threadsafe(zc, record, now)
423-
424413
def async_update_records(self, zc: 'Zeroconf', now: float, records: List[RecordUpdate]) -> None:
425414
"""Updates service information from a DNS record.
426415
427416
This method will be run in the event loop.
428417
"""
429418
new_records_futures = self._new_records_futures
430-
if self._process_records_threadsafe(zc, now, records) and new_records_futures:
431-
_resolve_all_futures_to_none(new_records_futures)
432-
433-
def _process_records_threadsafe(self, zc: 'Zeroconf', now: float, records: List[RecordUpdate]) -> bool:
434-
"""Thread safe record updating.
435-
436-
Returns True if new records were added.
437-
"""
438419
updated: bool = False
439420
for record_update in records:
440421
updated |= self._process_record_threadsafe(zc, record_update.new, now)
441-
return updated
422+
if updated and new_records_futures:
423+
_resolve_all_futures_to_none(new_records_futures)
442424

443425
def _process_record_threadsafe(self, zc: 'Zeroconf', record: DNSRecord, now: float) -> bool:
444426
"""Thread safe record updating.

src/zeroconf/_updates.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def async_update_records(self, zc: 'Zeroconf', now: float, records: List[RecordU
5656
All records that are received in a single packet are passed
5757
to update_records.
5858
59-
This implementation is a compatiblity shim to ensure older code
59+
This implementation is a compatibility shim to ensure older code
6060
that uses RecordUpdateListener as a base class will continue to
6161
get calls to update_record. This method will raise
6262
NotImplementedError in a future version.

tests/services/test_info.py

Lines changed: 100 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import pytest
1818

1919
import zeroconf as r
20-
from zeroconf import DNSAddress, const
20+
from zeroconf import DNSAddress, RecordUpdate, const
2121
from zeroconf._services import info
2222
from zeroconf._services.info import ServiceInfo
2323
from zeroconf._utils.net import IPVersion
@@ -68,89 +68,119 @@ def test_service_info_rejects_non_matching_updates(self):
6868
service_type, service_name, 22, 0, 0, desc, service_server, addresses=[service_address]
6969
)
7070
# Verify backwards compatiblity with calling with None
71-
info.update_record(zc, now, None)
71+
info.async_update_records(zc, now, [])
7272
# Matching updates
73-
info.update_record(
73+
info.async_update_records(
7474
zc,
7575
now,
76-
r.DNSText(
77-
service_name,
78-
const._TYPE_TXT,
79-
const._CLASS_IN | const._CLASS_UNIQUE,
80-
ttl,
81-
b'\x04ff=0\x04ci=2\x04sf=0\x0bsh=6fLM5A==',
82-
),
76+
[
77+
RecordUpdate(
78+
r.DNSText(
79+
service_name,
80+
const._TYPE_TXT,
81+
const._CLASS_IN | const._CLASS_UNIQUE,
82+
ttl,
83+
b'\x04ff=0\x04ci=2\x04sf=0\x0bsh=6fLM5A==',
84+
),
85+
None,
86+
)
87+
],
8388
)
8489
assert info.properties[b"ci"] == b"2"
85-
info.update_record(
90+
info.async_update_records(
8691
zc,
8792
now,
88-
r.DNSService(
89-
service_name,
90-
const._TYPE_SRV,
91-
const._CLASS_IN | const._CLASS_UNIQUE,
92-
ttl,
93-
0,
94-
0,
95-
80,
96-
'ASH-2.local.',
97-
),
93+
[
94+
RecordUpdate(
95+
r.DNSService(
96+
service_name,
97+
const._TYPE_SRV,
98+
const._CLASS_IN | const._CLASS_UNIQUE,
99+
ttl,
100+
0,
101+
0,
102+
80,
103+
'ASH-2.local.',
104+
),
105+
None,
106+
)
107+
],
98108
)
99109
assert info.server_key == 'ash-2.local.'
100110
assert info.server == 'ASH-2.local.'
101111
new_address = socket.inet_aton("10.0.1.3")
102-
info.update_record(
112+
info.async_update_records(
103113
zc,
104114
now,
105-
r.DNSAddress(
106-
'ASH-2.local.',
107-
const._TYPE_A,
108-
const._CLASS_IN | const._CLASS_UNIQUE,
109-
ttl,
110-
new_address,
111-
),
115+
[
116+
RecordUpdate(
117+
r.DNSAddress(
118+
'ASH-2.local.',
119+
const._TYPE_A,
120+
const._CLASS_IN | const._CLASS_UNIQUE,
121+
ttl,
122+
new_address,
123+
),
124+
None,
125+
)
126+
],
112127
)
113128
assert new_address in info.addresses
114129
# Non-matching updates
115-
info.update_record(
130+
info.async_update_records(
116131
zc,
117132
now,
118-
r.DNSText(
119-
"incorrect.name.",
120-
const._TYPE_TXT,
121-
const._CLASS_IN | const._CLASS_UNIQUE,
122-
ttl,
123-
b'\x04ff=0\x04ci=3\x04sf=0\x0bsh=6fLM5A==',
124-
),
133+
[
134+
RecordUpdate(
135+
r.DNSText(
136+
"incorrect.name.",
137+
const._TYPE_TXT,
138+
const._CLASS_IN | const._CLASS_UNIQUE,
139+
ttl,
140+
b'\x04ff=0\x04ci=3\x04sf=0\x0bsh=6fLM5A==',
141+
),
142+
None,
143+
)
144+
],
125145
)
126146
assert info.properties[b"ci"] == b"2"
127-
info.update_record(
147+
info.async_update_records(
128148
zc,
129149
now,
130-
r.DNSService(
131-
"incorrect.name.",
132-
const._TYPE_SRV,
133-
const._CLASS_IN | const._CLASS_UNIQUE,
134-
ttl,
135-
0,
136-
0,
137-
80,
138-
'ASH-2.local.',
139-
),
150+
[
151+
RecordUpdate(
152+
r.DNSService(
153+
"incorrect.name.",
154+
const._TYPE_SRV,
155+
const._CLASS_IN | const._CLASS_UNIQUE,
156+
ttl,
157+
0,
158+
0,
159+
80,
160+
'ASH-2.local.',
161+
),
162+
None,
163+
)
164+
],
140165
)
141166
assert info.server_key == 'ash-2.local.'
142167
assert info.server == 'ASH-2.local.'
143168
new_address = socket.inet_aton("10.0.1.4")
144-
info.update_record(
169+
info.async_update_records(
145170
zc,
146171
now,
147-
r.DNSAddress(
148-
"incorrect.name.",
149-
const._TYPE_A,
150-
const._CLASS_IN | const._CLASS_UNIQUE,
151-
ttl,
152-
new_address,
153-
),
172+
[
173+
RecordUpdate(
174+
r.DNSAddress(
175+
"incorrect.name.",
176+
const._TYPE_A,
177+
const._CLASS_IN | const._CLASS_UNIQUE,
178+
ttl,
179+
new_address,
180+
),
181+
None,
182+
)
183+
],
154184
)
155185
assert new_address not in info.addresses
156186
zc.close()
@@ -169,16 +199,21 @@ def test_service_info_rejects_expired_records(self):
169199
service_type, service_name, 22, 0, 0, desc, service_server, addresses=[service_address]
170200
)
171201
# Matching updates
172-
info.update_record(
202+
info.async_update_records(
173203
zc,
174204
now,
175-
r.DNSText(
176-
service_name,
177-
const._TYPE_TXT,
178-
const._CLASS_IN | const._CLASS_UNIQUE,
179-
ttl,
180-
b'\x04ff=0\x04ci=2\x04sf=0\x0bsh=6fLM5A==',
181-
),
205+
[
206+
RecordUpdate(
207+
r.DNSText(
208+
service_name,
209+
const._TYPE_TXT,
210+
const._CLASS_IN | const._CLASS_UNIQUE,
211+
ttl,
212+
b'\x04ff=0\x04ci=2\x04sf=0\x0bsh=6fLM5A==',
213+
),
214+
None,
215+
)
216+
],
182217
)
183218
assert info.properties[b"ci"] == b"2"
184219
# Expired record
@@ -190,7 +225,7 @@ def test_service_info_rejects_expired_records(self):
190225
b'\x04ff=0\x04ci=3\x04sf=0\x0bsh=6fLM5A==',
191226
)
192227
expired_record.set_created_ttl(1000, 1)
193-
info.update_record(zc, now, expired_record)
228+
info.async_update_records(zc, now, [RecordUpdate(expired_record, None)])
194229
assert info.properties[b"ci"] == b"2"
195230
zc.close()
196231

0 commit comments

Comments
 (0)