diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index 3d8dc247857..a9976cf7e32 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -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)) diff --git a/tests/conftest.py b/tests/conftest.py index ff4a09096e0..9b861d5bde5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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: diff --git a/tests/helpers/test_aiohttp_client.py b/tests/helpers/test_aiohttp_client.py index 6d2a7e7a8bb..e44111634d1 100644 --- a/tests/helpers/test_aiohttp_client.py +++ b/tests/helpers/test_aiohttp_client.py @@ -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