Skip to content

Commit e9f8aa5

Browse files
authored
fix: set change during iteration when dispatching listeners (#1370)
1 parent 0758c1e commit e9f8aa5

2 files changed

Lines changed: 76 additions & 2 deletions

File tree

src/zeroconf/_handlers/record_manager.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def async_updates(self, now: _float, records: List[RecordUpdate]) -> None:
5656
5757
This method will be run in the event loop.
5858
"""
59-
for listener in self.listeners:
59+
for listener in self.listeners.copy():
6060
listener.async_update_records(self.zc, now, records)
6161

6262
def async_updates_complete(self, notify: bool) -> None:
@@ -67,7 +67,7 @@ def async_updates_complete(self, notify: bool) -> None:
6767
6868
This method will be run in the event loop.
6969
"""
70-
for listener in self.listeners:
70+
for listener in self.listeners.copy():
7171
listener.async_update_records_complete()
7272
if notify:
7373
self.zc.async_notify_all()

tests/test_handlers.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1762,3 +1762,77 @@ def async_update_records(self, zc: 'Zeroconf', now: float, records: List[r.Recor
17621762
)
17631763

17641764
await aiozc.async_close()
1765+
1766+
1767+
@pytest.mark.asyncio
1768+
async def test_async_updates_iteration_safe():
1769+
"""Ensure we can safely iterate over the async_updates."""
1770+
1771+
aiozc = AsyncZeroconf(interfaces=['127.0.0.1'])
1772+
zc: Zeroconf = aiozc.zeroconf
1773+
updated = []
1774+
good_bye_answer = r.DNSPointer(
1775+
"myservicelow_tcp._tcp.local.",
1776+
const._TYPE_PTR,
1777+
const._CLASS_IN | const._CLASS_UNIQUE,
1778+
0,
1779+
'goodbye.local.',
1780+
)
1781+
1782+
class OtherListener(r.RecordUpdateListener):
1783+
"""A RecordUpdateListener that does not implement update_records."""
1784+
1785+
def async_update_records(self, zc: 'Zeroconf', now: float, records: List[r.RecordUpdate]) -> None:
1786+
"""Update multiple records in one shot."""
1787+
updated.extend(records)
1788+
1789+
other = OtherListener()
1790+
1791+
class ListenerThatAddsListener(r.RecordUpdateListener):
1792+
"""A RecordUpdateListener that does not implement update_records."""
1793+
1794+
def async_update_records(self, zc: 'Zeroconf', now: float, records: List[r.RecordUpdate]) -> None:
1795+
"""Update multiple records in one shot."""
1796+
updated.extend(records)
1797+
zc.async_add_listener(other, None)
1798+
1799+
zc.async_add_listener(ListenerThatAddsListener(), None)
1800+
await asyncio.sleep(0) # flush out any call soons
1801+
1802+
# This should not raise RuntimeError: set changed size during iteration
1803+
zc.record_manager.async_updates(
1804+
now=current_time_millis(), records=[r.RecordUpdate(good_bye_answer, None)]
1805+
)
1806+
1807+
assert len(updated) == 1
1808+
await aiozc.async_close()
1809+
1810+
1811+
@pytest.mark.asyncio
1812+
async def test_async_updates_complete_iteration_safe():
1813+
"""Ensure we can safely iterate over the async_updates_complete."""
1814+
1815+
aiozc = AsyncZeroconf(interfaces=['127.0.0.1'])
1816+
zc: Zeroconf = aiozc.zeroconf
1817+
1818+
class OtherListener(r.RecordUpdateListener):
1819+
"""A RecordUpdateListener that does not implement update_records."""
1820+
1821+
def async_update_records_complete(self) -> None:
1822+
"""Update multiple records in one shot."""
1823+
1824+
other = OtherListener()
1825+
1826+
class ListenerThatAddsListener(r.RecordUpdateListener):
1827+
"""A RecordUpdateListener that does not implement update_records."""
1828+
1829+
def async_update_records_complete(self) -> None:
1830+
"""Update multiple records in one shot."""
1831+
zc.async_add_listener(other, None)
1832+
1833+
zc.async_add_listener(ListenerThatAddsListener(), None)
1834+
await asyncio.sleep(0) # flush out any call soons
1835+
1836+
# This should not raise RuntimeError: set changed size during iteration
1837+
zc.record_manager.async_updates_complete(False)
1838+
await aiozc.async_close()

0 commit comments

Comments
 (0)