Improve error message for global timeout (#141563)

* Improve error message for global timeout

* Add test

* Message works with zone too
This commit is contained in:
Artur Pragacz 2025-05-27 07:49:18 +02:00 committed by GitHub
parent d25ba79427
commit b36b591ccf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 51 additions and 7 deletions

View File

@ -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(

View File

@ -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

View File

@ -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: