Skip to content

Commit 19643c9

Browse files
authored
Fix weakref garbage collection race condition in DNS resolver manager (#10923)
1 parent 6a682e2 commit 19643c9

File tree

3 files changed

+51
-1
lines changed

3 files changed

+51
-1
lines changed

CHANGES/10923.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
10847.feature.rst

aiohttp/resolver.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,11 +219,14 @@ def release_resolver(
219219
loop: The event loop the resolver was using.
220220
"""
221221
# Remove client from its loop's tracking
222+
if loop not in self._loop_data:
223+
return
222224
resolver, client_set = self._loop_data[loop]
223225
client_set.discard(client)
224226
# If no more clients for this loop, cancel and remove its resolver
225227
if not client_set:
226-
resolver.cancel()
228+
if resolver is not None:
229+
resolver.cancel()
227230
del self._loop_data[loop]
228231

229232

tests/test_resolver.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -614,3 +614,49 @@ async def test_dns_resolver_manager_multiple_event_loops(
614614
# Verify resolver cleanup
615615
resolver1.cancel.assert_called_once()
616616
resolver2.cancel.assert_called_once()
617+
618+
619+
@pytest.mark.skipif(not getaddrinfo, reason="aiodns >=3.2.0 required")
620+
async def test_dns_resolver_manager_weakref_garbage_collection() -> None:
621+
"""Test that release_resolver handles None resolver due to weakref garbage collection."""
622+
manager = _DNSResolverManager()
623+
624+
# Create a mock resolver that will be None when accessed
625+
mock_resolver = Mock()
626+
mock_resolver.cancel = Mock()
627+
628+
with patch("aiodns.DNSResolver", return_value=mock_resolver):
629+
# Create an AsyncResolver to get a resolver from the manager
630+
resolver = AsyncResolver()
631+
loop = asyncio.get_running_loop()
632+
633+
# Manually corrupt the data to simulate garbage collection
634+
# by setting the resolver to None
635+
manager._loop_data[loop] = (None, manager._loop_data[loop][1]) # type: ignore[assignment]
636+
637+
# This should not raise an AttributeError: 'NoneType' object has no attribute 'cancel'
638+
await resolver.close()
639+
640+
# Verify no exception was raised and the loop data was cleaned up properly
641+
# Since we set resolver to None and there was one client, the entry should be removed
642+
assert loop not in manager._loop_data
643+
644+
645+
@pytest.mark.skipif(not getaddrinfo, reason="aiodns >=3.2.0 required")
646+
async def test_dns_resolver_manager_missing_loop_data() -> None:
647+
"""Test that release_resolver handles missing loop data gracefully."""
648+
manager = _DNSResolverManager()
649+
650+
with patch("aiodns.DNSResolver"):
651+
# Create an AsyncResolver
652+
resolver = AsyncResolver()
653+
loop = asyncio.get_running_loop()
654+
655+
# Manually remove the loop data to simulate race condition
656+
manager._loop_data.clear()
657+
658+
# This should not raise a KeyError
659+
await resolver.close()
660+
661+
# Verify no exception was raised
662+
assert loop not in manager._loop_data

0 commit comments

Comments
 (0)