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:
Mike Degatano 2023-10-04 12:54:42 -04:00 committed by GitHub
parent d70aa5f9a9
commit 682b8e0535
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 57 additions and 6 deletions

View File

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

View File

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

View File

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