diff --git a/CHANGES/10923.feature.rst b/CHANGES/10923.feature.rst new file mode 120000 index 00000000000..879a4227358 --- /dev/null +++ b/CHANGES/10923.feature.rst @@ -0,0 +1 @@ +10847.feature.rst \ No newline at end of file diff --git a/aiohttp/resolver.py b/aiohttp/resolver.py index 0a646b0c189..8e30b05d47d 100644 --- a/aiohttp/resolver.py +++ b/aiohttp/resolver.py @@ -219,11 +219,14 @@ def release_resolver( loop: The event loop the resolver was using. """ # Remove client from its loop's tracking + if loop not in self._loop_data: + return resolver, client_set = self._loop_data[loop] client_set.discard(client) # If no more clients for this loop, cancel and remove its resolver if not client_set: - resolver.cancel() + if resolver is not None: + resolver.cancel() del self._loop_data[loop] diff --git a/tests/test_resolver.py b/tests/test_resolver.py index 1bc779c1ecf..7950f3b0f39 100644 --- a/tests/test_resolver.py +++ b/tests/test_resolver.py @@ -614,3 +614,49 @@ async def test_dns_resolver_manager_multiple_event_loops( # Verify resolver cleanup resolver1.cancel.assert_called_once() resolver2.cancel.assert_called_once() + + +@pytest.mark.skipif(not getaddrinfo, reason="aiodns >=3.2.0 required") +async def test_dns_resolver_manager_weakref_garbage_collection() -> None: + """Test that release_resolver handles None resolver due to weakref garbage collection.""" + manager = _DNSResolverManager() + + # Create a mock resolver that will be None when accessed + mock_resolver = Mock() + mock_resolver.cancel = Mock() + + with patch("aiodns.DNSResolver", return_value=mock_resolver): + # Create an AsyncResolver to get a resolver from the manager + resolver = AsyncResolver() + loop = asyncio.get_running_loop() + + # Manually corrupt the data to simulate garbage collection + # by setting the resolver to None + manager._loop_data[loop] = (None, manager._loop_data[loop][1]) # type: ignore[assignment] + + # This should not raise an AttributeError: 'NoneType' object has no attribute 'cancel' + await resolver.close() + + # Verify no exception was raised and the loop data was cleaned up properly + # Since we set resolver to None and there was one client, the entry should be removed + assert loop not in manager._loop_data + + +@pytest.mark.skipif(not getaddrinfo, reason="aiodns >=3.2.0 required") +async def test_dns_resolver_manager_missing_loop_data() -> None: + """Test that release_resolver handles missing loop data gracefully.""" + manager = _DNSResolverManager() + + with patch("aiodns.DNSResolver"): + # Create an AsyncResolver + resolver = AsyncResolver() + loop = asyncio.get_running_loop() + + # Manually remove the loop data to simulate race condition + manager._loop_data.clear() + + # This should not raise a KeyError + await resolver.close() + + # Verify no exception was raised + assert loop not in manager._loop_data