Skip to content

Commit 64fc600

Browse files
authored
[PR #10897/4624fed backport][3.12] Fix DNS resolver object churn for multiple sessions (#10906)
1 parent c8c3d5f commit 64fc600

File tree

3 files changed

+334
-23
lines changed

3 files changed

+334
-23
lines changed

CHANGES/10847.feature.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Implemented shared DNS resolver management to fix excessive resolver object creation
2+
when using multiple client sessions. The new ``_DNSResolverManager`` singleton ensures
3+
only one ``DNSResolver`` object is created for default configurations, significantly
4+
reducing resource usage and improving performance for applications using multiple
5+
client sessions simultaneously -- by :user:`bdraco`.

aiohttp/resolver.py

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import asyncio
22
import socket
3+
import weakref
34
from typing import Any, Dict, Final, List, Optional, Tuple, Type, Union
45

56
from .abc import AbstractResolver, ResolveResult
@@ -93,7 +94,17 @@ def __init__(
9394
if aiodns is None:
9495
raise RuntimeError("Resolver requires aiodns library")
9596

96-
self._resolver = aiodns.DNSResolver(*args, **kwargs)
97+
self._loop = asyncio.get_running_loop()
98+
self._manager: Optional[_DNSResolverManager] = None
99+
# If custom args are provided, create a dedicated resolver instance
100+
# This means each AsyncResolver with custom args gets its own
101+
# aiodns.DNSResolver instance
102+
if args or kwargs:
103+
self._resolver = aiodns.DNSResolver(*args, **kwargs)
104+
return
105+
# Use the shared resolver from the manager for default arguments
106+
self._manager = _DNSResolverManager()
107+
self._resolver = self._manager.get_resolver(self, self._loop)
97108

98109
if not hasattr(self._resolver, "gethostbyname"):
99110
# aiodns 1.1 is not available, fallback to DNSResolver.query
@@ -180,7 +191,78 @@ async def _resolve_with_query(
180191
return hosts
181192

182193
async def close(self) -> None:
194+
if self._manager:
195+
# Release the resolver from the manager if using the shared resolver
196+
self._manager.release_resolver(self, self._loop)
197+
self._manager = None # Clear reference to manager
198+
self._resolver = None # type: ignore[assignment] # Clear reference to resolver
199+
return
200+
# Otherwise cancel our dedicated resolver
183201
self._resolver.cancel()
202+
self._resolver = None # type: ignore[assignment] # Clear reference
203+
204+
205+
class _DNSResolverManager:
206+
"""Manager for aiodns.DNSResolver objects.
207+
208+
This class manages shared aiodns.DNSResolver instances
209+
with no custom arguments across different event loops.
210+
"""
211+
212+
_instance: Optional["_DNSResolverManager"] = None
213+
214+
def __new__(cls) -> "_DNSResolverManager":
215+
if cls._instance is None:
216+
cls._instance = super().__new__(cls)
217+
cls._instance._init()
218+
return cls._instance
219+
220+
def _init(self) -> None:
221+
# Use WeakKeyDictionary to allow event loops to be garbage collected
222+
self._loop_data: weakref.WeakKeyDictionary[
223+
asyncio.AbstractEventLoop,
224+
tuple["aiodns.DNSResolver", weakref.WeakSet["AsyncResolver"]],
225+
] = weakref.WeakKeyDictionary()
226+
227+
def get_resolver(
228+
self, client: "AsyncResolver", loop: asyncio.AbstractEventLoop
229+
) -> "aiodns.DNSResolver":
230+
"""Get or create the shared aiodns.DNSResolver instance for a specific event loop.
231+
232+
Args:
233+
client: The AsyncResolver instance requesting the resolver.
234+
This is required to track resolver usage.
235+
loop: The event loop to use for the resolver.
236+
"""
237+
# Create a new resolver and client set for this loop if it doesn't exist
238+
if loop not in self._loop_data:
239+
resolver = aiodns.DNSResolver(loop=loop)
240+
client_set: weakref.WeakSet["AsyncResolver"] = weakref.WeakSet()
241+
self._loop_data[loop] = (resolver, client_set)
242+
else:
243+
# Get the existing resolver and client set
244+
resolver, client_set = self._loop_data[loop]
245+
246+
# Register this client with the loop
247+
client_set.add(client)
248+
return resolver
249+
250+
def release_resolver(
251+
self, client: "AsyncResolver", loop: asyncio.AbstractEventLoop
252+
) -> None:
253+
"""Release the resolver for an AsyncResolver client when it's closed.
254+
255+
Args:
256+
client: The AsyncResolver instance to release.
257+
loop: The event loop the resolver was using.
258+
"""
259+
# Remove client from its loop's tracking
260+
resolver, client_set = self._loop_data[loop]
261+
client_set.discard(client)
262+
# If no more clients for this loop, cancel and remove its resolver
263+
if not client_set:
264+
resolver.cancel()
265+
del self._loop_data[loop]
184266

185267

186268
_DefaultType = Type[Union[AsyncResolver, ThreadedResolver]]

0 commit comments

Comments
 (0)