From da2a753a0b93ff6c6911e10a48e5c7f05ec30f2c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 1 Oct 2026 17:00:38 +0200 Subject: [PATCH] Use wait_shared_future in singleton helper (#183889) Co-authored-by: Claude Opus 5.5 --- homeassistant/helpers/singleton.py | 3 ++- tests/helpers/test_singleton.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/singleton.py b/homeassistant/helpers/singleton.py index da1fe6baad1a..2abeca8a304e 100644 --- a/homeassistant/helpers/singleton.py +++ b/homeassistant/helpers/singleton.py @@ -7,6 +7,7 @@ import inspect from typing import Any, Literal, assert_type, cast, overload from homeassistant.core import HomeAssistant +from homeassistant.util.async_ import wait_shared_future from homeassistant.util.hass_dict import HassKey type _FuncType[_T] = Callable[[HomeAssistant], _T] @@ -78,7 +79,7 @@ def singleton[_S, _T, _U]( obj_or_future = hass.data[data_key] if isinstance(obj_or_future, asyncio.Future): - return cast(_T, await obj_or_future) + return cast(_T, await wait_shared_future(obj_or_future)) return cast(_T, obj_or_future) diff --git a/tests/helpers/test_singleton.py b/tests/helpers/test_singleton.py index 12d4b095cf81..4e4aa4f4df2a 100644 --- a/tests/helpers/test_singleton.py +++ b/tests/helpers/test_singleton.py @@ -101,3 +101,32 @@ async def test_singleton_async_concurrent_raises(mock_hass: HomeAssistant) -> No # Only the first caller ran the wrapped function; the waiter observed its error. assert calls == 1 assert "test_key" not in mock_hass.data + + +async def test_singleton_async_concurrent_waiter_cancelled( + mock_hass: HomeAssistant, +) -> None: + """Verify cancelling a waiting caller does not break the in-progress call.""" + release = asyncio.Event() + result = object() + + @singleton.singleton("test_key") + async def something(hass: HomeAssistant) -> Any: + await release.wait() + return result + + task1 = asyncio.create_task(something(mock_hass)) + await asyncio.sleep(0) + task2 = asyncio.create_task(something(mock_hass)) + await asyncio.sleep(0) + + task2.cancel() + with pytest.raises(asyncio.CancelledError): + await task2 + + release.set() + async with asyncio.timeout(1): + assert await task1 is result + + assert mock_hass.data["test_key"] is result + assert await something(mock_hass) is result