1
0
mirror of https://github.com/home-assistant/core.git synced 2026-02-15 07:36:16 +00:00

Fix cancel propagation in update coordinator and config entry (#153504)

This commit is contained in:
Joakim Plate
2025-11-27 19:48:45 +01:00
committed by GitHub
parent 4f6624d0aa
commit 5f522e5afa
7 changed files with 102 additions and 14 deletions

View File

@@ -211,7 +211,7 @@ async def ws_start_preview(
@callback
def async_preview_updated(
last_exception: Exception | None, state: str, attributes: Mapping[str, Any]
last_exception: BaseException | None, state: str, attributes: Mapping[str, Any]
) -> None:
"""Forward config entry state events to websocket."""
if last_exception:

View File

@@ -241,7 +241,9 @@ class HistoryStatsSensor(HistoryStatsSensorBase):
async def async_start_preview(
self,
preview_callback: Callable[[Exception | None, str, Mapping[str, Any]], None],
preview_callback: Callable[
[BaseException | None, str, Mapping[str, Any]], None
],
) -> CALLBACK_TYPE:
"""Render a preview."""

View File

@@ -754,6 +754,7 @@ class ConfigEntry[_DataT = Any]:
error_reason_translation_key = None
error_reason_translation_placeholders = None
result = False
try:
with async_start_setup(
hass, integration=self.domain, group=self.entry_id, phase=setup_phase
@@ -775,8 +776,6 @@ class ConfigEntry[_DataT = Any]:
self.domain,
error_reason,
)
await self._async_process_on_unload(hass)
result = False
except ConfigEntryAuthFailed as exc:
message = str(exc)
auth_base_message = "could not authenticate"
@@ -792,9 +791,7 @@ class ConfigEntry[_DataT = Any]:
self.domain,
auth_message,
)
await self._async_process_on_unload(hass)
self.async_start_reauth(hass)
result = False
except ConfigEntryNotReady as exc:
message = str(exc)
error_reason_translation_key = exc.translation_key
@@ -835,14 +832,39 @@ class ConfigEntry[_DataT = Any]:
functools.partial(self._async_setup_again, hass),
)
await self._async_process_on_unload(hass)
return
# pylint: disable-next=broad-except
except (asyncio.CancelledError, SystemExit, Exception):
except asyncio.CancelledError:
# We want to propagate CancelledError if we are being cancelled.
if (task := asyncio.current_task()) and task.cancelling() > 0:
_LOGGER.exception(
"Setup of config entry '%s' for %s integration cancelled",
self.title,
self.domain,
)
self._async_set_state(
hass,
ConfigEntryState.SETUP_ERROR,
None,
None,
None,
)
raise
# This was not a "real" cancellation, log it and treat as a normal error.
_LOGGER.exception(
"Error setting up entry %s for %s", self.title, integration.domain
)
result = False
# pylint: disable-next=broad-except
except (SystemExit, Exception):
_LOGGER.exception(
"Error setting up entry %s for %s", self.title, integration.domain
)
finally:
if not result and domain_is_integration:
await self._async_process_on_unload(hass)
#
# After successfully calling async_setup_entry, it is important that this function

View File

@@ -131,7 +131,7 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]):
self._request_refresh_task: asyncio.TimerHandle | None = None
self._retry_after: float | None = None
self.last_update_success = True
self.last_exception: Exception | None = None
self.last_exception: BaseException | None = None
if request_refresh_debouncer is None:
request_refresh_debouncer = Debouncer(
@@ -492,8 +492,16 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]):
self.config_entry.async_start_reauth(self.hass)
except NotImplementedError as err:
self.last_exception = err
self.last_update_success = False
raise
except asyncio.CancelledError as err:
self.last_exception = err
self.last_update_success = False
if (task := asyncio.current_task()) and task.cancelling() > 0:
raise
except Exception as err:
self.last_exception = err
self.last_update_success = False

View File

@@ -334,6 +334,35 @@ async def test_refresh_no_update_method(
await crd.async_refresh()
async def test_refresh_cancelled(
hass: HomeAssistant,
crd: update_coordinator.DataUpdateCoordinator[int],
) -> None:
"""Test that we don't swallow cancellation."""
await crd.async_refresh()
start = asyncio.Event()
abort = asyncio.Event()
async def _update() -> bool:
start.set()
await abort.wait()
return True
crd.update_method = _update
crd.last_update_success = True
task = hass.async_create_task(crd.async_refresh())
await start.wait()
task.cancel()
with pytest.raises(asyncio.CancelledError):
await task
abort.set()
assert crd.last_update_success is False
async def test_update_interval(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,

View File

@@ -9727,3 +9727,31 @@ async def test_config_flow_create_entry_with_next_flow(hass: HomeAssistant) -> N
assert result["next_flow"][0] == config_entries.FlowType.CONFIG_FLOW
# Verify the target flow exists
hass.config_entries.flow.async_get(result["next_flow"][1])
async def test_canceled_exceptions_are_propagated(
hass: HomeAssistant, manager: config_entries.ConfigEntries
) -> None:
"""Tests that base exceptions like cancellations are not swallowed."""
entry = MockConfigEntry(title="test_title", domain="test")
start = asyncio.Event()
abort = asyncio.Event()
async def _setup(_: HomeAssistant, __: ConfigEntry) -> bool:
start.set()
await abort.wait()
return True
mock_integration(hass, MockModule("test", async_setup_entry=_setup))
mock_platform(hass, "test.config_flow", None)
entry.add_to_hass(hass)
task = hass.async_create_task(manager.async_setup(entry.entry_id))
await start.wait()
task.cancel()
with pytest.raises(asyncio.CancelledError):
await task
abort.set()

View File

@@ -13,6 +13,5 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Mock an unsuccessful entry setup."""
asyncio.current_task().cancel()
await asyncio.sleep(0)
"""Mock an leaked cancellation, without our own task being cancelled."""
raise asyncio.CancelledError