Skip to content

Commit 44f16a5

Browse files
agnersclaude
andauthored
Send startup complete after the startup healthcheck (#7326)
Home Assistant refreshes Supervisor issues when it receives the startup complete event and removes repairs of issues which are no longer reported, including their ignored state. The startup healthcheck ran as a background task, so checks which only run in the running state (e.g. the pwned check) had not raised their issues yet when that refresh happened. An ignored repair got removed and raised again shortly after, without the ignored state. Wait for the startup healthcheck before sending startup complete. Bound the wait by a timeout so a slow check or autofix cannot hold back startup indefinitely. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent dd48d2b commit 44f16a5

2 files changed

Lines changed: 60 additions & 1 deletion

File tree

‎supervisor/core.py‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
_PORT_RESERVE_SERVICE: Final = "homeassistant-core-port-reserve.service"
4545
_PORT_RESERVE_TIMEOUT: Final = 10
4646
_TERMINAL_STATES: Final = {UnitActiveState.INACTIVE, UnitActiveState.FAILED}
47+
_STARTUP_HEALTHCHECK_TIMEOUT: Final = 30
4748

4849

4950
def _format_bind_address(host: str, port: int) -> str:
@@ -349,9 +350,21 @@ async def start(self) -> None:
349350

350351
# Update Host/Device information
351352
self.sys_create_task(self.sys_host.reload())
352-
self.sys_create_task(self.sys_resolution.healthcheck())
353353

354354
await self.set_state(CoreState.RUNNING)
355+
356+
# Home Assistant refreshes issues on startup complete and removes
357+
# repairs (and their ignored state) of issues not raised yet
358+
healthcheck = self.sys_create_task(self.sys_resolution.healthcheck())
359+
_, pending = await asyncio.wait(
360+
{healthcheck}, timeout=_STARTUP_HEALTHCHECK_TIMEOUT
361+
)
362+
if pending:
363+
_LOGGER.warning(
364+
"Healthcheck did not finish within %ds, completing startup",
365+
_STARTUP_HEALTHCHECK_TIMEOUT,
366+
)
367+
355368
self.sys_homeassistant.websocket.supervisor_update_event(
356369
"supervisor", {ATTR_STARTUP: "complete"}
357370
)

‎tests/test_core.py‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1002,3 +1002,49 @@ async def _failing_update() -> None:
10021002
coresys.tasks.load.assert_awaited_once()
10031003
coresys.homeassistant.core.start.assert_awaited_once()
10041004
assert coresys.core.state == CoreState.RUNNING
1005+
1006+
1007+
@pytest.mark.usefixtures("core_start_base_mocks")
1008+
@pytest.mark.parametrize(
1009+
("finish_healthcheck", "expected_order"),
1010+
[
1011+
pytest.param(
1012+
True,
1013+
["healthcheck running", "healthcheck done", "startup complete"],
1014+
id="finished",
1015+
),
1016+
pytest.param(
1017+
False, ["healthcheck running", "startup complete"], id="timed_out"
1018+
),
1019+
],
1020+
)
1021+
async def test_start_completes_after_healthcheck(
1022+
coresys: CoreSys, finish_healthcheck: bool, expected_order: list[str]
1023+
) -> None:
1024+
"""Test startup complete is sent once the healthcheck finished or timed out."""
1025+
order: list[str] = []
1026+
release = asyncio.Event()
1027+
if finish_healthcheck:
1028+
release.set()
1029+
1030+
async def mock_healthcheck() -> None:
1031+
order.append(f"healthcheck {coresys.core.state}")
1032+
await release.wait()
1033+
order.append("healthcheck done")
1034+
1035+
def mock_update_event(key: str, data: dict) -> None:
1036+
if data == {"startup": "complete"}:
1037+
order.append("startup complete")
1038+
1039+
coresys.homeassistant.websocket.supervisor_update_event.side_effect = (
1040+
mock_update_event
1041+
)
1042+
with (
1043+
patch.object(coresys.resolution, "healthcheck", new=mock_healthcheck),
1044+
patch("supervisor.core._STARTUP_HEALTHCHECK_TIMEOUT", 0.01),
1045+
):
1046+
await coresys.core.start()
1047+
assert order == expected_order
1048+
1049+
release.set()
1050+
await asyncio.sleep(0)

0 commit comments

Comments
 (0)