1
0
mirror of https://github.com/home-assistant/supervisor.git synced 2026-05-02 22:30:59 +01:00
Files
supervisor/tests/homeassistant/test_home_assistant_watchdog.py
Stefan Agner 77f3da7014 Disable Home Assistant watchdog during system shutdown (#6512)
During system shutdown (reboot/poweroff), the watchdog was incorrectly
detecting the Home Assistant Core container as failed and attempting to
restart it. This occurred because Docker was stopping all containers in
parallel with Supervisor's own shutdown sequence, causing the watchdog
to trigger while add-ons were still being stopped.

This led to an abrupt termination of Core before it could cleanly shut
down its SQLite database, resulting in a warning on the next startup:
"The system could not validate that the sqlite3 database was shutdown
cleanly".

The fix registers a supervisor state change listener that unregisters
the watchdog when entering any shutdown state (SHUTDOWN, STOPPING, or
CLOSE). This prevents restart attempts during both user-initiated
reboots (via API) and external shutdown signals (Docker SIGTERM,
console reboot commands).

Since SHUTDOWN, STOPPING, and CLOSE are terminal states with no reverse
transition back to RUNNING, no re-registration logic is needed.

Fixes #6511

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-31 17:01:05 +01:00

248 lines
8.5 KiB
Python

"""Test Home Assistant watchdog."""
import asyncio
from unittest.mock import AsyncMock, PropertyMock, patch
from aiodocker.containers import DockerContainer
from awesomeversion import AwesomeVersion
from supervisor.const import BusEvent, CoreState
from supervisor.coresys import CoreSys
from supervisor.docker.const import ContainerState
from supervisor.docker.monitor import DockerContainerStateEvent
from supervisor.exceptions import HomeAssistantError
async def test_home_assistant_watchdog(coresys: CoreSys) -> None:
"""Test homeassistant watchdog works correctly."""
coresys.homeassistant.version = AwesomeVersion("2022.7.3")
with (
patch(
"supervisor.docker.interface.DockerInterface.version",
new=PropertyMock(return_value=AwesomeVersion("2022.7.3")),
),
patch.object(type(coresys.homeassistant.core.instance), "attach"),
):
await coresys.homeassistant.core.load()
coresys.homeassistant.core.watchdog = True
with (
patch.object(type(coresys.homeassistant.core), "restart") as restart,
patch.object(type(coresys.homeassistant.core), "start") as start,
patch.object(
type(coresys.homeassistant.core.instance), "current_state"
) as current_state,
):
current_state.return_value = ContainerState.UNHEALTHY
coresys.bus.fire_event(
BusEvent.DOCKER_CONTAINER_STATE_CHANGE,
DockerContainerStateEvent(
name="homeassistant",
state=ContainerState.UNHEALTHY,
id="abc123",
time=1,
),
)
await asyncio.sleep(0)
restart.assert_called_once()
start.assert_not_called()
restart.reset_mock()
current_state.return_value = ContainerState.FAILED
coresys.bus.fire_event(
BusEvent.DOCKER_CONTAINER_STATE_CHANGE,
DockerContainerStateEvent(
name="homeassistant",
state=ContainerState.FAILED,
id="abc123",
time=1,
),
)
await asyncio.sleep(0)
restart.assert_not_called()
start.assert_called_once()
start.reset_mock()
# Do not process event if container state has changed since fired
current_state.return_value = ContainerState.HEALTHY
coresys.bus.fire_event(
BusEvent.DOCKER_CONTAINER_STATE_CHANGE,
DockerContainerStateEvent(
name="homeassistant",
state=ContainerState.FAILED,
id="abc123",
time=1,
),
)
await asyncio.sleep(0)
restart.assert_not_called()
start.assert_not_called()
# Do not restart when home assistant stopped normally
coresys.bus.fire_event(
BusEvent.DOCKER_CONTAINER_STATE_CHANGE,
DockerContainerStateEvent(
name="homeassistant",
state=ContainerState.STOPPED,
id="abc123",
time=1,
),
)
await asyncio.sleep(0)
restart.assert_not_called()
start.assert_not_called()
# Other containers ignored
coresys.bus.fire_event(
BusEvent.DOCKER_CONTAINER_STATE_CHANGE,
DockerContainerStateEvent(
name="addon_local_other",
state=ContainerState.UNHEALTHY,
id="abc123",
time=1,
),
)
await asyncio.sleep(0)
restart.assert_not_called()
start.assert_not_called()
async def test_home_assistant_watchdog_rebuild_on_failure(coresys: CoreSys) -> None:
"""Test home assistant watchdog rebuilds if start fails."""
coresys.homeassistant.version = AwesomeVersion("2022.7.3")
with (
patch(
"supervisor.docker.interface.DockerInterface.version",
new=PropertyMock(return_value=AwesomeVersion("2022.7.3")),
),
patch.object(type(coresys.homeassistant.core.instance), "attach"),
):
await coresys.homeassistant.core.load()
coresys.homeassistant.core.watchdog = True
with (
patch.object(
type(coresys.homeassistant.core), "start", side_effect=HomeAssistantError()
) as start,
patch.object(type(coresys.homeassistant.core), "rebuild") as rebuild,
patch.object(
type(coresys.homeassistant.core.instance),
"current_state",
return_value=ContainerState.FAILED,
),
):
coresys.bus.fire_event(
BusEvent.DOCKER_CONTAINER_STATE_CHANGE,
DockerContainerStateEvent(
name="homeassistant",
state=ContainerState.FAILED,
id="abc123",
time=1,
),
)
await asyncio.sleep(0.1)
start.assert_called_once()
rebuild.assert_called_once()
async def test_home_assistant_watchdog_skip_on_load(
coresys: CoreSys, container: DockerContainer
) -> None:
"""Test home assistant watchdog skips a crash event on load."""
container.show.return_value["State"]["Status"] = "stopped"
container.show.return_value["State"]["Running"] = False
container.show.return_value["State"]["ExitCode"] = 1
coresys.homeassistant.core.watchdog = True
events = AsyncMock()
coresys.bus.register_event(BusEvent.DOCKER_CONTAINER_STATE_CHANGE, events)
coresys.homeassistant.version = AwesomeVersion("2022.7.3")
with (
patch(
"supervisor.docker.interface.DockerInterface.version",
new=PropertyMock(return_value=AwesomeVersion("2022.7.3")),
),
patch.object(type(coresys.homeassistant.core), "restart") as restart,
patch.object(type(coresys.homeassistant.core), "start") as start,
):
await coresys.homeassistant.core.load()
# No events should be raised on attach
await asyncio.sleep(0)
events.assert_not_called()
restart.assert_not_called()
start.assert_not_called()
async def test_home_assistant_watchdog_unregisters_on_shutdown(
coresys: CoreSys,
) -> None:
"""Test home assistant watchdog unregisters when entering shutdown states."""
coresys.homeassistant.version = AwesomeVersion("2022.7.3")
with (
patch(
"supervisor.docker.interface.DockerInterface.version",
new=PropertyMock(return_value=AwesomeVersion("2022.7.3")),
),
patch.object(type(coresys.homeassistant.core.instance), "attach"),
):
await coresys.homeassistant.core.load()
coresys.homeassistant.core.watchdog = True
# Verify watchdog listener is registered
assert coresys.homeassistant.core._watchdog_listener is not None
watchdog_listener = coresys.homeassistant.core._watchdog_listener
with (
patch.object(type(coresys.homeassistant.core), "restart") as restart,
patch.object(type(coresys.homeassistant.core), "start") as start,
patch.object(
type(coresys.homeassistant.core.instance),
"current_state",
return_value=ContainerState.FAILED,
),
):
# Watchdog should respond to events before shutdown
coresys.bus.fire_event(
BusEvent.DOCKER_CONTAINER_STATE_CHANGE,
DockerContainerStateEvent(
name="homeassistant",
state=ContainerState.FAILED,
id="abc123",
time=1,
),
)
await asyncio.sleep(0)
start.assert_called_once()
start.reset_mock()
# Test each shutdown state
for shutdown_state in (CoreState.SHUTDOWN, CoreState.STOPPING, CoreState.CLOSE):
# Reload to reset listener
coresys.homeassistant.core._watchdog_listener = watchdog_listener
# Fire shutdown state change
coresys.bus.fire_event(BusEvent.SUPERVISOR_STATE_CHANGE, shutdown_state)
await asyncio.sleep(0)
# Verify watchdog listener is unregistered
assert coresys.homeassistant.core._watchdog_listener is None
# Watchdog should not respond to events after shutdown
coresys.bus.fire_event(
BusEvent.DOCKER_CONTAINER_STATE_CHANGE,
DockerContainerStateEvent(
name="homeassistant",
state=ContainerState.FAILED,
id="abc123",
time=1,
),
)
await asyncio.sleep(0)
start.assert_not_called()
restart.assert_not_called()