Skip to content

Commit cf0b5b9

Browse files
authored
Ensure services are removed from the registry when calling unregister_all_services (#644)
- There was a race condition where a query could be answered for a service in the registry while goodbye packets which could result a fresh record being broadcast after the goodbye if a query came in at just the right time. To avoid this, we now remove the services from the registry right after we generate the goodbye packet
1 parent a83d390 commit cf0b5b9

4 files changed

Lines changed: 71 additions & 19 deletions

File tree

tests/test_core.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,3 +317,32 @@ def test_invalid_packets_ignored_and_does_not_cause_loop_exception():
317317
time.sleep(0.2)
318318
zc.close()
319319
assert zc.cache.get(entry) is not None
320+
321+
322+
def test_goodbye_all_services():
323+
"""Verify generating the goodbye query does not change with time."""
324+
zc = Zeroconf(interfaces=['127.0.0.1'])
325+
out = zc.generate_unregister_all_services()
326+
assert out is None
327+
type_ = "_http._tcp.local."
328+
registration_name = "xxxyyy.%s" % type_
329+
desc = {'path': '/~paulsm/'}
330+
info = r.ServiceInfo(
331+
type_, registration_name, 80, 0, 0, desc, "ash-2.local.", addresses=[socket.inet_aton("10.0.1.2")]
332+
)
333+
zc.registry.add(info)
334+
out = zc.generate_unregister_all_services()
335+
assert out is not None
336+
first_packet = out.packets()
337+
zc.registry.add(info)
338+
out2 = zc.generate_unregister_all_services()
339+
assert out2 is not None
340+
second_packet = out.packets()
341+
assert second_packet == first_packet
342+
343+
# Verify the registery is empty
344+
out3 = zc.generate_unregister_all_services()
345+
assert out3 is None
346+
assert zc.registry.get_service_infos() == []
347+
348+
zc.close()

tests/test_init.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,8 +184,21 @@ def verify_name_change(self, zc, type_, name, number_hosts):
184184
# verify no name conflict https://tools.ietf.org/html/rfc6762#section-6.6
185185
zc.register_service(info_service, cooperating_responders=True)
186186

187-
zc.register_service(info_service, allow_name_change=True)
188-
assert info_service.name.split('.')[0] == '%s-%d' % (name, number_hosts + 1)
187+
# Create a new object since allow_name_change will mutate the
188+
# original object and then we will have the wrong service
189+
# in the registry
190+
info_service2 = ServiceInfo(
191+
type_,
192+
'%s.%s' % (name, type_),
193+
80,
194+
0,
195+
0,
196+
desc,
197+
"ash-2.local.",
198+
addresses=[socket.inet_aton("10.0.1.2")],
199+
)
200+
zc.register_service(info_service2, allow_name_change=True)
201+
assert info_service2.name.split('.')[0] == '%s-%d' % (name, number_hosts + 1)
189202

190203
def generate_many_hosts(self, zc, type_, name, number_hosts):
191204
records_per_server = 2

zeroconf/_core.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -476,10 +476,22 @@ def unregister_service(self, info: ServiceInfo) -> None:
476476
self.registry.remove(info)
477477
self._broadcast_service(info, _UNREGISTER_TIME, 0)
478478

479-
def unregister_all_services(self) -> None:
480-
"""Unregister all registered services."""
479+
def generate_unregister_all_services(self) -> Optional[DNSOutgoing]:
480+
"""Generate a DNSOutgoing goodbye for all services and remove them from the registry."""
481481
service_infos = self.registry.get_service_infos()
482482
if not service_infos:
483+
return None
484+
out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
485+
for info in service_infos:
486+
self._add_broadcast_answer(out, info, 0)
487+
self.registry.remove(service_infos)
488+
return out
489+
490+
def unregister_all_services(self) -> None:
491+
"""Unregister all registered services."""
492+
# Send Goodbye packets https://datatracker.ietf.org/doc/html/rfc6762#section-10.1
493+
out = self.generate_unregister_all_services()
494+
if not out:
483495
return
484496
now = current_time_millis()
485497
next_time = now
@@ -489,9 +501,6 @@ def unregister_all_services(self) -> None:
489501
self.wait(next_time - now)
490502
now = current_time_millis()
491503
continue
492-
out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
493-
for info in service_infos:
494-
self._add_broadcast_answer(out, info, 0)
495504
self.send(out)
496505
i += 1
497506
next_time += _UNREGISTER_TIME
@@ -604,8 +613,8 @@ def close(self) -> None:
604613
if self._GLOBAL_DONE:
605614
return
606615
# remove service listeners
607-
self.remove_all_service_listeners()
608616
self.unregister_all_services()
617+
self.remove_all_service_listeners()
609618
self._GLOBAL_DONE = True
610619
self.engine.close()
611620
# shutdown the rest

zeroconf/_services/registry.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"""
2222

2323
import threading
24-
from typing import Dict, List, Optional
24+
from typing import Dict, List, Optional, Union
2525

2626

2727
from .._exceptions import ServiceNameAlreadyRegistered
@@ -47,21 +47,21 @@ def __init__(
4747

4848
def add(self, info: ServiceInfo) -> None:
4949
"""Add a new service to the registry."""
50-
5150
with self._lock:
5251
self._add(info)
5352

54-
def remove(self, info: ServiceInfo) -> None:
53+
def remove(self, info: Union[List[ServiceInfo], ServiceInfo]) -> None:
5554
"""Remove a new service from the registry."""
55+
infos = info if isinstance(info, list) else [info]
5656

5757
with self._lock:
58-
self._remove(info)
58+
self._remove(infos)
5959

6060
def update(self, info: ServiceInfo) -> None:
6161
"""Update new service in the registry."""
6262

6363
with self._lock:
64-
self._remove(info)
64+
self._remove([info])
6565
self._add(info)
6666

6767
def get_service_infos(self) -> List[ServiceInfo]:
@@ -103,9 +103,10 @@ def _add(self, info: ServiceInfo) -> None:
103103
self.types.setdefault(info.type.lower(), []).append(info.key)
104104
self.servers.setdefault(info.server_key, []).append(info.key)
105105

106-
def _remove(self, info: ServiceInfo) -> None:
107-
"""Remove a service under the lock."""
108-
old_service_info = self._services[info.key]
109-
self.types[old_service_info.type.lower()].remove(info.key)
110-
self.servers[old_service_info.server_key].remove(info.key)
111-
del self._services[info.key]
106+
def _remove(self, infos: List[ServiceInfo]) -> None:
107+
"""Remove a services under the lock."""
108+
for info in infos:
109+
old_service_info = self._services[info.key]
110+
self.types[old_service_info.type.lower()].remove(info.key)
111+
self.servers[old_service_info.server_key].remove(info.key)
112+
del self._services[info.key]

0 commit comments

Comments
 (0)