Only create a single resolver object if there are multiple aiohttp sessions (#144090)

This commit is contained in:
J. Nick Koston 2025-05-02 13:43:06 -05:00 committed by GitHub
parent 97be2c4ac9
commit 2890fc7dd2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 48 additions and 4 deletions

View File

@ -28,6 +28,7 @@ from homeassistant.util.json import json_loads
from .frame import warn_use
from .json import json_dumps
from .singleton import singleton
if TYPE_CHECKING:
from aiohttp.typedefs import JSONDecoder
@ -39,6 +40,7 @@ DATA_CONNECTOR: HassKey[dict[tuple[bool, int, str], aiohttp.BaseConnector]] = Ha
DATA_CLIENTSESSION: HassKey[dict[tuple[bool, int, str], aiohttp.ClientSession]] = (
HassKey("aiohttp_clientsession")
)
DATA_RESOLVER: HassKey[HassAsyncDNSResolver] = HassKey("aiohttp_resolver")
SERVER_SOFTWARE = (
f"{APPLICATION_NAME}/{__version__} "
@ -70,6 +72,21 @@ MAXIMUM_CONNECTIONS = 4096
MAXIMUM_CONNECTIONS_PER_HOST = 100
class HassAsyncDNSResolver(AsyncDualMDNSResolver):
"""Home Assistant AsyncDNSResolver.
This is a wrapper around the AsyncDualMDNSResolver to only
close the resolver when the Home Assistant instance is closed.
"""
async def real_close(self) -> None:
"""Close the resolver."""
await super().close()
async def close(self) -> None:
"""Close the resolver."""
class HassClientResponse(aiohttp.ClientResponse):
"""aiohttp.ClientResponse with a json method that uses json_loads by default."""
@ -363,7 +380,7 @@ def _async_get_connector(
ssl=ssl_context,
limit=MAXIMUM_CONNECTIONS,
limit_per_host=MAXIMUM_CONNECTIONS_PER_HOST,
resolver=_async_make_resolver(hass),
resolver=_async_get_or_create_resolver(hass),
)
connectors[connector_key] = connector
@ -376,6 +393,19 @@ def _async_get_connector(
return connector
@singleton(DATA_RESOLVER)
@callback
def _async_make_resolver(hass: HomeAssistant) -> AsyncDualMDNSResolver:
return AsyncDualMDNSResolver(async_zeroconf=zeroconf.async_get_async_zeroconf(hass))
def _async_get_or_create_resolver(hass: HomeAssistant) -> HassAsyncDNSResolver:
"""Return the HassAsyncDNSResolver."""
resolver = _async_make_resolver(hass)
async def _async_close_resolver(event: Event) -> None:
await resolver.real_close()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, _async_close_resolver)
return resolver
@callback
def _async_make_resolver(hass: HomeAssistant) -> HassAsyncDNSResolver:
return HassAsyncDNSResolver(async_zeroconf=zeroconf.async_get_async_zeroconf(hass))

View File

@ -1319,9 +1319,11 @@ def disable_translations_once(
@pytest_asyncio.fixture(autouse=True, scope="session", loop_scope="session")
async def mock_zeroconf_resolver() -> AsyncGenerator[_patch]:
"""Mock out the zeroconf resolver."""
resolver = AsyncResolver()
resolver.real_close = resolver.close
patcher = patch(
"homeassistant.helpers.aiohttp_client._async_make_resolver",
return_value=AsyncResolver(),
return_value=resolver,
)
patcher.start()
try:

View File

@ -401,3 +401,15 @@ async def test_async_mdnsresolver(
resp = await session.post("http://localhost/xyz", json={"x": 1})
assert resp.status == 200
assert await resp.json() == {"x": 1}
async def test_resolver_is_singleton(hass: HomeAssistant) -> None:
"""Test that the resolver is a singleton."""
session = client.async_get_clientsession(hass)
session2 = client.async_get_clientsession(hass)
session3 = client.async_create_clientsession(hass)
assert isinstance(session._connector, aiohttp.TCPConnector)
assert isinstance(session2._connector, aiohttp.TCPConnector)
assert isinstance(session3._connector, aiohttp.TCPConnector)
assert session._connector._resolver is session2._connector._resolver
assert session._connector._resolver is session3._connector._resolver