mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-19 15:16:33 +00:00
Core API check during startup can timeout (#4595)
* Core API check during startup can timeout * Use a more specific exception so caller can differentiate
This commit is contained in:
parent
d70aa5f9a9
commit
682b8e0535
@ -67,6 +67,10 @@ class HomeAssistantCrashError(HomeAssistantError):
|
|||||||
"""Error on crash of a Home Assistant startup."""
|
"""Error on crash of a Home Assistant startup."""
|
||||||
|
|
||||||
|
|
||||||
|
class HomeAssistantStartupTimeout(HomeAssistantCrashError):
|
||||||
|
"""Timeout waiting for Home Assistant successful startup."""
|
||||||
|
|
||||||
|
|
||||||
class HomeAssistantAPIError(HomeAssistantError):
|
class HomeAssistantAPIError(HomeAssistantError):
|
||||||
"""Home Assistant API exception."""
|
"""Home Assistant API exception."""
|
||||||
|
|
||||||
|
@ -2,12 +2,14 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Awaitable
|
from collections.abc import Awaitable
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime, timedelta
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import secrets
|
import secrets
|
||||||
import shutil
|
import shutil
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
import attr
|
|
||||||
from awesomeversion import AwesomeVersion
|
from awesomeversion import AwesomeVersion
|
||||||
|
|
||||||
from ..const import ATTR_HOMEASSISTANT, BusEvent
|
from ..const import ATTR_HOMEASSISTANT, BusEvent
|
||||||
@ -21,6 +23,7 @@ from ..exceptions import (
|
|||||||
HomeAssistantCrashError,
|
HomeAssistantCrashError,
|
||||||
HomeAssistantError,
|
HomeAssistantError,
|
||||||
HomeAssistantJobError,
|
HomeAssistantJobError,
|
||||||
|
HomeAssistantStartupTimeout,
|
||||||
HomeAssistantUpdateError,
|
HomeAssistantUpdateError,
|
||||||
JobException,
|
JobException,
|
||||||
)
|
)
|
||||||
@ -40,15 +43,17 @@ from .const import (
|
|||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
SECONDS_BETWEEN_API_CHECKS: Final[int] = 5
|
||||||
|
STARTUP_API_CHECK_TIMEOUT: Final[timedelta] = timedelta(minutes=5)
|
||||||
RE_YAML_ERROR = re.compile(r"homeassistant\.util\.yaml")
|
RE_YAML_ERROR = re.compile(r"homeassistant\.util\.yaml")
|
||||||
|
|
||||||
|
|
||||||
@attr.s(frozen=True)
|
@dataclass
|
||||||
class ConfigResult:
|
class ConfigResult:
|
||||||
"""Return object from config check."""
|
"""Return object from config check."""
|
||||||
|
|
||||||
valid = attr.ib()
|
valid: bool
|
||||||
log = attr.ib()
|
log: str
|
||||||
|
|
||||||
|
|
||||||
class HomeAssistantCore(JobGroup):
|
class HomeAssistantCore(JobGroup):
|
||||||
@ -435,8 +440,9 @@ class HomeAssistantCore(JobGroup):
|
|||||||
return
|
return
|
||||||
_LOGGER.info("Wait until Home Assistant is ready")
|
_LOGGER.info("Wait until Home Assistant is ready")
|
||||||
|
|
||||||
while True:
|
start = datetime.now()
|
||||||
await asyncio.sleep(5)
|
while not (timeout := datetime.now() >= start + STARTUP_API_CHECK_TIMEOUT):
|
||||||
|
await asyncio.sleep(SECONDS_BETWEEN_API_CHECKS)
|
||||||
|
|
||||||
# 1: Check if Container is is_running
|
# 1: Check if Container is is_running
|
||||||
if not await self.instance.is_running():
|
if not await self.instance.is_running():
|
||||||
@ -450,6 +456,11 @@ class HomeAssistantCore(JobGroup):
|
|||||||
return
|
return
|
||||||
|
|
||||||
self._error_state = True
|
self._error_state = True
|
||||||
|
if timeout:
|
||||||
|
raise HomeAssistantStartupTimeout(
|
||||||
|
"No API response in 5 minutes, assuming core has had a fatal startup error",
|
||||||
|
_LOGGER.error,
|
||||||
|
)
|
||||||
raise HomeAssistantCrashError()
|
raise HomeAssistantCrashError()
|
||||||
|
|
||||||
@Job(
|
@Job(
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
"""Test Home Assistant core."""
|
"""Test Home Assistant core."""
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
from unittest.mock import MagicMock, Mock, PropertyMock, patch
|
from unittest.mock import MagicMock, Mock, PropertyMock, patch
|
||||||
|
|
||||||
from awesomeversion import AwesomeVersion
|
from awesomeversion import AwesomeVersion
|
||||||
from docker.errors import DockerException, ImageNotFound, NotFound
|
from docker.errors import DockerException, ImageNotFound, NotFound
|
||||||
import pytest
|
import pytest
|
||||||
|
from time_machine import travel
|
||||||
|
|
||||||
from supervisor.const import CpuArch
|
from supervisor.const import CpuArch
|
||||||
from supervisor.coresys import CoreSys
|
from supervisor.coresys import CoreSys
|
||||||
@ -14,6 +17,7 @@ from supervisor.exceptions import (
|
|||||||
AudioUpdateError,
|
AudioUpdateError,
|
||||||
CodeNotaryError,
|
CodeNotaryError,
|
||||||
DockerError,
|
DockerError,
|
||||||
|
HomeAssistantCrashError,
|
||||||
HomeAssistantError,
|
HomeAssistantError,
|
||||||
HomeAssistantJobError,
|
HomeAssistantJobError,
|
||||||
)
|
)
|
||||||
@ -263,3 +267,35 @@ async def test_stats_failures(
|
|||||||
|
|
||||||
with pytest.raises(HomeAssistantError):
|
with pytest.raises(HomeAssistantError):
|
||||||
await coresys.homeassistant.core.stats()
|
await coresys.homeassistant.core.stats()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_api_check_timeout(
|
||||||
|
coresys: CoreSys, container: MagicMock, caplog: pytest.LogCaptureFixture
|
||||||
|
):
|
||||||
|
"""Test attempts to contact the API timeout."""
|
||||||
|
container.status = "stopped"
|
||||||
|
coresys.homeassistant.version = AwesomeVersion("2023.9.0")
|
||||||
|
coresys.homeassistant.api.check_api_state.return_value = False
|
||||||
|
|
||||||
|
async def mock_instance_start(*_):
|
||||||
|
container.status = "running"
|
||||||
|
|
||||||
|
with patch.object(
|
||||||
|
DockerHomeAssistant, "start", new=mock_instance_start
|
||||||
|
), patch.object(DockerAPI, "container_is_initialized", return_value=True), travel(
|
||||||
|
datetime(2023, 10, 2, 0, 0, 0), tick=False
|
||||||
|
) as traveller:
|
||||||
|
|
||||||
|
async def mock_sleep(*args):
|
||||||
|
traveller.shift(timedelta(minutes=1))
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"supervisor.homeassistant.core.asyncio.sleep", new=mock_sleep
|
||||||
|
), pytest.raises(HomeAssistantCrashError):
|
||||||
|
await coresys.homeassistant.core.start()
|
||||||
|
|
||||||
|
assert coresys.homeassistant.api.check_api_state.call_count == 5
|
||||||
|
assert (
|
||||||
|
"No API response in 5 minutes, assuming core has had a fatal startup error"
|
||||||
|
in caplog.text
|
||||||
|
)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user