From b36b591ccf2f4306ace2fdced064a0e23e78ea04 Mon Sep 17 00:00:00 2001 From: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com> Date: Tue, 27 May 2025 07:49:18 +0200 Subject: [PATCH] Improve error message for global timeout (#141563) * Improve error message for global timeout * Add test * Message works with zone too --- homeassistant/bootstrap.py | 12 ++++++++++-- homeassistant/util/timeout.py | 24 +++++++++++++++++++----- tests/util/test_timeout.py | 22 ++++++++++++++++++++++ 3 files changed, 51 insertions(+), 7 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index f88912478a7..b3e056f787d 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -929,7 +929,11 @@ async def _async_set_up_integrations( await _async_setup_multi_components(hass, stage_all_domains, config) continue try: - async with hass.timeout.async_timeout(timeout, cool_down=COOLDOWN_TIME): + async with hass.timeout.async_timeout( + timeout, + cool_down=COOLDOWN_TIME, + cancel_message=f"Bootstrap stage {name} timeout", + ): await _async_setup_multi_components(hass, stage_all_domains, config) except TimeoutError: _LOGGER.warning( @@ -941,7 +945,11 @@ async def _async_set_up_integrations( # Wrap up startup _LOGGER.debug("Waiting for startup to wrap up") try: - async with hass.timeout.async_timeout(WRAP_UP_TIMEOUT, cool_down=COOLDOWN_TIME): + async with hass.timeout.async_timeout( + WRAP_UP_TIMEOUT, + cool_down=COOLDOWN_TIME, + cancel_message="Bootstrap startup wrap up timeout", + ): await hass.async_block_till_done() except TimeoutError: _LOGGER.warning( diff --git a/homeassistant/util/timeout.py b/homeassistant/util/timeout.py index ddabdf2746d..3609fccd468 100644 --- a/homeassistant/util/timeout.py +++ b/homeassistant/util/timeout.py @@ -148,6 +148,7 @@ class _GlobalTaskContext: task: asyncio.Task[Any], timeout: float, cool_down: float, + cancel_message: str | None, ) -> None: """Initialize internal timeout context manager.""" self._loop: asyncio.AbstractEventLoop = asyncio.get_running_loop() @@ -161,6 +162,7 @@ class _GlobalTaskContext: self._state: _State = _State.INIT self._cool_down: float = cool_down self._cancelling = 0 + self._cancel_message = cancel_message async def __aenter__(self) -> Self: self._manager.global_tasks.append(self) @@ -242,7 +244,9 @@ class _GlobalTaskContext: """Cancel own task.""" if self._task.done(): return - self._task.cancel("Global task timeout") + self._task.cancel( + f"Global task timeout{': ' + self._cancel_message if self._cancel_message else ''}" + ) def pause(self) -> None: """Pause timers while it freeze.""" @@ -270,6 +274,7 @@ class _ZoneTaskContext: zone: _ZoneTimeoutManager, task: asyncio.Task[Any], timeout: float, + cancel_message: str | None, ) -> None: """Initialize internal timeout context manager.""" self._loop: asyncio.AbstractEventLoop = asyncio.get_running_loop() @@ -280,6 +285,7 @@ class _ZoneTaskContext: self._expiration_time: float | None = None self._timeout_handler: asyncio.Handle | None = None self._cancelling = 0 + self._cancel_message = cancel_message @property def state(self) -> _State: @@ -354,7 +360,9 @@ class _ZoneTaskContext: # Timeout if self._task.done(): return - self._task.cancel("Zone timeout") + self._task.cancel( + f"Zone timeout{': ' + self._cancel_message if self._cancel_message else ''}" + ) def pause(self) -> None: """Pause timers while it freeze.""" @@ -486,7 +494,11 @@ class TimeoutManager: task.zones_done_signal() def async_timeout( - self, timeout: float, zone_name: str = ZONE_GLOBAL, cool_down: float = 0 + self, + timeout: float, + zone_name: str = ZONE_GLOBAL, + cool_down: float = 0, + cancel_message: str | None = None, ) -> _ZoneTaskContext | _GlobalTaskContext: """Timeout based on a zone. @@ -497,7 +509,9 @@ class TimeoutManager: # Global Zone if zone_name == ZONE_GLOBAL: - return _GlobalTaskContext(self, current_task, timeout, cool_down) + return _GlobalTaskContext( + self, current_task, timeout, cool_down, cancel_message + ) # Zone Handling if zone_name in self.zones: @@ -506,7 +520,7 @@ class TimeoutManager: self.zones[zone_name] = zone = _ZoneTimeoutManager(self, zone_name) # Create Task - return _ZoneTaskContext(zone, current_task, timeout) + return _ZoneTaskContext(zone, current_task, timeout, cancel_message) def async_freeze( self, zone_name: str = ZONE_GLOBAL diff --git a/tests/util/test_timeout.py b/tests/util/test_timeout.py index 5e8261c4c02..f0d2561fb7b 100644 --- a/tests/util/test_timeout.py +++ b/tests/util/test_timeout.py @@ -36,6 +36,18 @@ async def test_simple_global_timeout_freeze() -> None: await asyncio.sleep(0.3) +async def test_simple_global_timeout_cancel_message() -> None: + """Test a simple global timeout cancel message.""" + timeout = TimeoutManager() + + with suppress(TimeoutError): + async with timeout.async_timeout(0.1, cancel_message="Test"): + with pytest.raises( + asyncio.CancelledError, match="Global task timeout: Test" + ): + await asyncio.sleep(0.3) + + async def test_simple_zone_timeout_freeze_inside_executor_job( hass: HomeAssistant, ) -> None: @@ -222,6 +234,16 @@ async def test_simple_zone_timeout() -> None: await asyncio.sleep(0.3) +async def test_simple_zone_timeout_cancel_message() -> None: + """Test a simple zone timeout cancel message.""" + timeout = TimeoutManager() + + with suppress(TimeoutError): + async with timeout.async_timeout(0.1, "test", cancel_message="Test"): + with pytest.raises(asyncio.CancelledError, match="Zone timeout: Test"): + await asyncio.sleep(0.3) + + async def test_simple_zone_timeout_does_not_leak_upward( hass: HomeAssistant, ) -> None: