Intelligent timeout handler for setup/bootstrap (#38329)

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Pascal Vizeli 2020-08-05 14:58:19 +02:00 committed by GitHub
parent caca762088
commit c291d4aa7d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 901 additions and 89 deletions

View File

@ -9,7 +9,6 @@ import sys
from time import monotonic
from typing import TYPE_CHECKING, Any, Dict, Optional, Set
from async_timeout import timeout
import voluptuous as vol
import yarl
@ -44,6 +43,11 @@ DATA_LOGGING = "logging"
LOG_SLOW_STARTUP_INTERVAL = 60
STAGE_1_TIMEOUT = 120
STAGE_2_TIMEOUT = 300
WRAP_UP_TIMEOUT = 300
COOLDOWN_TIME = 60
DEBUGGER_INTEGRATIONS = {"debugpy", "ptvsd"}
CORE_INTEGRATIONS = ("homeassistant", "persistent_notification")
LOGGING_INTEGRATIONS = {
@ -136,7 +140,7 @@ async def async_setup_hass(
hass.async_track_tasks()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP, {})
with contextlib.suppress(asyncio.TimeoutError):
async with timeout(10):
async with hass.timeout.async_timeout(10):
await hass.async_block_till_done()
safe_mode = True
@ -496,24 +500,42 @@ async def _async_set_up_integrations(
stage_2_domains = domains_to_setup - logging_domains - debuggers - stage_1_domains
# Kick off loading the registries. They don't need to be awaited.
asyncio.gather(
hass.helpers.device_registry.async_get_registry(),
hass.helpers.entity_registry.async_get_registry(),
hass.helpers.area_registry.async_get_registry(),
)
asyncio.create_task(hass.helpers.device_registry.async_get_registry())
asyncio.create_task(hass.helpers.entity_registry.async_get_registry())
asyncio.create_task(hass.helpers.area_registry.async_get_registry())
# Start setup
if stage_1_domains:
_LOGGER.info("Setting up stage 1: %s", stage_1_domains)
await async_setup_multi_components(hass, stage_1_domains, config, setup_started)
try:
async with hass.timeout.async_timeout(
STAGE_1_TIMEOUT, cool_down=COOLDOWN_TIME
):
await async_setup_multi_components(
hass, stage_1_domains, config, setup_started
)
except asyncio.TimeoutError:
_LOGGER.warning("Setup timed out for stage 1 - moving forward")
# Enables after dependencies
async_set_domains_to_be_loaded(hass, stage_1_domains | stage_2_domains)
if stage_2_domains:
_LOGGER.info("Setting up stage 2: %s", stage_2_domains)
await async_setup_multi_components(hass, stage_2_domains, config, setup_started)
try:
async with hass.timeout.async_timeout(
STAGE_2_TIMEOUT, cool_down=COOLDOWN_TIME
):
await async_setup_multi_components(
hass, stage_2_domains, config, setup_started
)
except asyncio.TimeoutError:
_LOGGER.warning("Setup timed out for stage 2 - moving forward")
# Wrap up startup
_LOGGER.debug("Waiting for startup to wrap up")
await hass.async_block_till_done()
try:
async with hass.timeout.async_timeout(WRAP_UP_TIMEOUT, cool_down=COOLDOWN_TIME):
await hass.async_block_till_done()
except asyncio.TimeoutError:
_LOGGER.warning("Setup timed out for bootstrap - moving forward")

View File

@ -35,14 +35,12 @@ from homeassistant.helpers.typing import ConfigType
import homeassistant.util.dt as dt_util
from . import migration, purge
from .const import DATA_INSTANCE, SQLITE_URL_PREFIX
from .const import DATA_INSTANCE, DOMAIN, SQLITE_URL_PREFIX
from .models import Base, Events, RecorderRuns, States
from .util import session_scope, validate_or_move_away_sqlite_database
_LOGGER = logging.getLogger(__name__)
DOMAIN = "recorder"
SERVICE_PURGE = "purge"
ATTR_KEEP_DAYS = "keep_days"

View File

@ -2,3 +2,4 @@
DATA_INSTANCE = "recorder_instance"
SQLITE_URL_PREFIX = "sqlite://"
DOMAIN = "recorder"

View File

@ -1,22 +1,19 @@
"""Schema migration helpers."""
import logging
import os
from sqlalchemy import Table, text
from sqlalchemy.engine import reflection
from sqlalchemy.exc import InternalError, OperationalError, SQLAlchemyError
from .const import DOMAIN
from .models import SCHEMA_VERSION, Base, SchemaChanges
from .util import session_scope
_LOGGER = logging.getLogger(__name__)
PROGRESS_FILE = ".migration_progress"
def migrate_schema(instance):
"""Check if the schema needs to be upgraded."""
progress_path = instance.hass.config.path(PROGRESS_FILE)
with session_scope(session=instance.get_session()) as session:
res = (
session.query(SchemaChanges)
@ -32,20 +29,13 @@ def migrate_schema(instance):
)
if current_version == SCHEMA_VERSION:
# Clean up if old migration left file
if os.path.isfile(progress_path):
_LOGGER.warning("Found existing migration file, cleaning up")
os.remove(instance.hass.config.path(PROGRESS_FILE))
return
with open(progress_path, "w"):
pass
_LOGGER.warning(
"Database is about to upgrade. Schema version: %s", current_version
)
try:
with instance.hass.timeout.freeze(DOMAIN):
for version in range(current_version, SCHEMA_VERSION):
new_version = version + 1
_LOGGER.info("Upgrading recorder db schema to version %s", new_version)
@ -53,8 +43,6 @@ def migrate_schema(instance):
session.add(SchemaChanges(schema_version=new_version))
_LOGGER.info("Upgrade to version %s done", new_version)
finally:
os.remove(instance.hass.config.path(PROGRESS_FILE))
def _create_index(engine, table_name, index_name):

View File

@ -35,7 +35,6 @@ from typing import (
)
import uuid
from async_timeout import timeout
import attr
import voluptuous as vol
import yarl
@ -76,6 +75,7 @@ from homeassistant.util import location, network
from homeassistant.util.async_ import fire_coroutine_threadsafe, run_callback_threadsafe
import homeassistant.util.dt as dt_util
from homeassistant.util.thread import fix_threading_exception_logging
from homeassistant.util.timeout import TimeoutManager
from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM, UnitSystem
# Typing imports that create a circular dependency
@ -184,10 +184,12 @@ class HomeAssistant:
self.helpers = loader.Helpers(self)
# This is a dictionary that any component can store any data on.
self.data: dict = {}
self.state = CoreState.not_running
self.exit_code = 0
self.state: CoreState = CoreState.not_running
self.exit_code: int = 0
# If not None, use to signal end-of-loop
self._stopped: Optional[asyncio.Event] = None
# Timeout handler for Core/Helper namespace
self.timeout: TimeoutManager = TimeoutManager()
@property
def is_running(self) -> bool:
@ -255,7 +257,7 @@ class HomeAssistant:
try:
# Only block for EVENT_HOMEASSISTANT_START listener
self.async_stop_track_tasks()
async with timeout(TIMEOUT_EVENT_START):
async with self.timeout.async_timeout(TIMEOUT_EVENT_START):
await self.async_block_till_done()
except asyncio.TimeoutError:
_LOGGER.warning(
@ -460,17 +462,35 @@ class HomeAssistant:
self.state = CoreState.stopping
self.async_track_tasks()
self.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
await self.async_block_till_done()
try:
async with self.timeout.async_timeout(120):
await self.async_block_till_done()
except asyncio.TimeoutError:
_LOGGER.warning(
"Timed out waiting for shutdown stage 1 to complete, the shutdown will continue"
)
# stage 2
self.state = CoreState.final_write
self.bus.async_fire(EVENT_HOMEASSISTANT_FINAL_WRITE)
await self.async_block_till_done()
try:
async with self.timeout.async_timeout(60):
await self.async_block_till_done()
except asyncio.TimeoutError:
_LOGGER.warning(
"Timed out waiting for shutdown stage 2 to complete, the shutdown will continue"
)
# stage 3
self.state = CoreState.not_running
self.bus.async_fire(EVENT_HOMEASSISTANT_CLOSE)
await self.async_block_till_done()
try:
async with self.timeout.async_timeout(30):
await self.async_block_till_done()
except asyncio.TimeoutError:
_LOGGER.warning(
"Timed out waiting for shutdown stage 3 to complete, the shutdown will continue"
)
# Python 3.9+ and backported in runner.py
await self.loop.shutdown_default_executor() # type: ignore

View File

@ -177,7 +177,8 @@ class EntityPlatform:
try:
task = async_create_setup_task()
await asyncio.wait_for(asyncio.shield(task), SLOW_SETUP_MAX_WAIT)
async with hass.timeout.async_timeout(SLOW_SETUP_MAX_WAIT, self.domain):
await asyncio.shield(task)
# Block till all entities are done
if self._tasks:

View File

@ -2,7 +2,6 @@
import asyncio
import logging
import os
from pathlib import Path
from typing import Any, Dict, Iterable, List, Optional, Set, Union, cast
from homeassistant.core import HomeAssistant
@ -14,7 +13,6 @@ DATA_PIP_LOCK = "pip_lock"
DATA_PKG_CACHE = "pkg_cache"
DATA_INTEGRATIONS_WITH_REQS = "integrations_with_reqs"
CONSTRAINT_FILE = "package_constraints.txt"
PROGRESS_FILE = ".pip_progress"
_LOGGER = logging.getLogger(__name__)
DISCOVERY_INTEGRATIONS: Dict[str, Iterable[str]] = {
"ssdp": ("ssdp",),
@ -124,22 +122,16 @@ async def async_process_requirements(
if pkg_util.is_installed(req):
continue
ret = await hass.async_add_executor_job(_install, hass, req, kwargs)
def _install(req: str, kwargs: Dict) -> bool:
"""Install requirement."""
return pkg_util.install_package(req, **kwargs)
ret = await hass.async_add_executor_job(_install, req, kwargs)
if not ret:
raise RequirementsNotFound(name, [req])
def _install(hass: HomeAssistant, req: str, kwargs: Dict) -> bool:
"""Install requirement."""
progress_path = Path(hass.config.path(PROGRESS_FILE))
progress_path.touch()
try:
return pkg_util.install_package(req, **kwargs)
finally:
progress_path.unlink()
def pip_kwargs(config_dir: Optional[str]) -> Dict[str, Any]:
"""Return keyword arguments for PIP install."""
is_docker = pkg_util.is_docker_env()

View File

@ -22,11 +22,7 @@ DATA_SETUP = "setup_tasks"
DATA_DEPS_REQS = "deps_reqs_processed"
SLOW_SETUP_WARNING = 10
# Since its possible for databases to be
# upwards of 36GiB (or larger) in the wild
# we wait up to 3 hours for startup
SLOW_SETUP_MAX_WAIT = 10800
SLOW_SETUP_MAX_WAIT = 300
@core.callback
@ -89,7 +85,8 @@ async def _async_process_dependencies(
return True
_LOGGER.debug("Dependency %s will wait for %s", integration.domain, list(tasks))
results = await asyncio.gather(*tasks.values())
async with hass.timeout.async_freeze(integration.domain):
results = await asyncio.gather(*tasks.values())
failed = [
domain
@ -190,7 +187,8 @@ async def _async_setup_component(
hass.data[DATA_SETUP_STARTED].pop(domain)
return False
result = await asyncio.wait_for(task, SLOW_SETUP_MAX_WAIT)
async with hass.timeout.async_timeout(SLOW_SETUP_MAX_WAIT, domain):
result = await task
except asyncio.TimeoutError:
_LOGGER.error(
"Setup of %s is taking longer than %s seconds."
@ -319,9 +317,10 @@ async def async_process_deps_reqs(
raise HomeAssistantError("Could not set up all dependencies.")
if not hass.config.skip_pip and integration.requirements:
await requirements.async_get_integration_with_requirements(
hass, integration.domain
)
async with hass.timeout.async_freeze(integration.domain):
await requirements.async_get_integration_with_requirements(
hass, integration.domain
)
processed.add(integration.domain)

View File

@ -0,0 +1,508 @@
"""Advanced timeout handling.
Set of helper classes to handle timeouts of tasks with advanced options
like zones and freezing of timeouts.
"""
from __future__ import annotations
import asyncio
import enum
import logging
from types import TracebackType
from typing import Any, Dict, List, Optional, Type, Union
from .async_ import run_callback_threadsafe
ZONE_GLOBAL = "global"
_LOGGER = logging.getLogger(__name__)
class _State(str, enum.Enum):
"""States of a task."""
INIT = "INIT"
ACTIVE = "ACTIVE"
TIMEOUT = "TIMEOUT"
EXIT = "EXIT"
class _GlobalFreezeContext:
"""Context manager that freezes the global timeout."""
def __init__(self, manager: TimeoutManager) -> None:
"""Initialize internal timeout context manager."""
self._loop: asyncio.AbstractEventLoop = asyncio.get_running_loop()
self._manager: TimeoutManager = manager
async def __aenter__(self) -> _GlobalFreezeContext:
self._enter()
return self
async def __aexit__(
self,
exc_type: Type[BaseException],
exc_val: BaseException,
exc_tb: TracebackType,
) -> Optional[bool]:
self._exit()
return None
def __enter__(self) -> _GlobalFreezeContext:
self._loop.call_soon_threadsafe(self._enter)
return self
def __exit__(
self,
exc_type: Type[BaseException],
exc_val: BaseException,
exc_tb: TracebackType,
) -> Optional[bool]:
self._loop.call_soon_threadsafe(self._exit)
return True
def _enter(self) -> None:
"""Run freeze."""
if not self._manager.freezes_done:
return
# Global reset
for task in self._manager.global_tasks:
task.pause()
# Zones reset
for zone in self._manager.zones.values():
if not zone.freezes_done:
continue
zone.pause()
self._manager.global_freezes.append(self)
def _exit(self) -> None:
"""Finish freeze."""
self._manager.global_freezes.remove(self)
if not self._manager.freezes_done:
return
# Global reset
for task in self._manager.global_tasks:
task.reset()
# Zones reset
for zone in self._manager.zones.values():
if not zone.freezes_done:
continue
zone.reset()
class _ZoneFreezeContext:
"""Context manager that freezes a zone timeout."""
def __init__(self, zone: _ZoneTimeoutManager) -> None:
"""Initialize internal timeout context manager."""
self._loop: asyncio.AbstractEventLoop = asyncio.get_running_loop()
self._zone: _ZoneTimeoutManager = zone
async def __aenter__(self) -> _ZoneFreezeContext:
self._enter()
return self
async def __aexit__(
self,
exc_type: Type[BaseException],
exc_val: BaseException,
exc_tb: TracebackType,
) -> Optional[bool]:
self._exit()
return None
def __enter__(self) -> _ZoneFreezeContext:
self._loop.call_soon_threadsafe(self._enter)
return self
def __exit__(
self,
exc_type: Type[BaseException],
exc_val: BaseException,
exc_tb: TracebackType,
) -> Optional[bool]:
self._loop.call_soon_threadsafe(self._exit)
return True
def _enter(self) -> None:
"""Run freeze."""
if self._zone.freezes_done:
self._zone.pause()
self._zone.enter_freeze(self)
def _exit(self) -> None:
"""Finish freeze."""
self._zone.exit_freeze(self)
if not self._zone.freezes_done:
return
self._zone.reset()
class _GlobalTaskContext:
"""Context manager that tracks a global task."""
def __init__(
self,
manager: TimeoutManager,
task: asyncio.Task[Any],
timeout: float,
cool_down: float,
) -> None:
"""Initialize internal timeout context manager."""
self._loop: asyncio.AbstractEventLoop = asyncio.get_running_loop()
self._manager: TimeoutManager = manager
self._task: asyncio.Task[Any] = task
self._time_left: float = timeout
self._expiration_time: Optional[float] = None
self._timeout_handler: Optional[asyncio.Handle] = None
self._wait_zone: asyncio.Event = asyncio.Event()
self._state: _State = _State.INIT
self._cool_down: float = cool_down
async def __aenter__(self) -> _GlobalTaskContext:
self._manager.global_tasks.append(self)
self._start_timer()
self._state = _State.ACTIVE
return self
async def __aexit__(
self,
exc_type: Type[BaseException],
exc_val: BaseException,
exc_tb: TracebackType,
) -> Optional[bool]:
self._stop_timer()
self._manager.global_tasks.remove(self)
# Timeout on exit
if exc_type is asyncio.CancelledError and self.state == _State.TIMEOUT:
raise asyncio.TimeoutError
self._state = _State.EXIT
self._wait_zone.set()
return None
@property
def state(self) -> _State:
"""Return state of the Global task."""
return self._state
def zones_done_signal(self) -> None:
"""Signal that all zones are done."""
self._wait_zone.set()
def _start_timer(self) -> None:
"""Start timeout handler."""
if self._timeout_handler:
return
self._expiration_time = self._loop.time() + self._time_left
self._timeout_handler = self._loop.call_at(
self._expiration_time, self._on_timeout
)
def _stop_timer(self) -> None:
"""Stop zone timer."""
if self._timeout_handler is None:
return
self._timeout_handler.cancel()
self._timeout_handler = None
# Calculate new timeout
assert self._expiration_time
self._time_left = self._expiration_time - self._loop.time()
def _on_timeout(self) -> None:
"""Process timeout."""
self._state = _State.TIMEOUT
self._timeout_handler = None
# Reset timer if zones are running
if not self._manager.zones_done:
asyncio.create_task(self._on_wait())
else:
self._cancel_task()
def _cancel_task(self) -> None:
"""Cancel own task."""
if self._task.done():
return
self._task.cancel()
def pause(self) -> None:
"""Pause timers while it freeze."""
self._stop_timer()
def reset(self) -> None:
"""Reset timer after freeze."""
self._start_timer()
async def _on_wait(self) -> None:
"""Wait until zones are done."""
await self._wait_zone.wait()
await asyncio.sleep(self._cool_down) # Allow context switch
if not self.state == _State.TIMEOUT:
return
self._cancel_task()
class _ZoneTaskContext:
"""Context manager that tracks an active task for a zone."""
def __init__(
self, zone: _ZoneTimeoutManager, task: asyncio.Task[Any], timeout: float,
) -> None:
"""Initialize internal timeout context manager."""
self._loop: asyncio.AbstractEventLoop = asyncio.get_running_loop()
self._zone: _ZoneTimeoutManager = zone
self._task: asyncio.Task[Any] = task
self._state: _State = _State.INIT
self._time_left: float = timeout
self._expiration_time: Optional[float] = None
self._timeout_handler: Optional[asyncio.Handle] = None
@property
def state(self) -> _State:
"""Return state of the Zone task."""
return self._state
async def __aenter__(self) -> _ZoneTaskContext:
self._zone.enter_task(self)
self._state = _State.ACTIVE
# Zone is on freeze
if self._zone.freezes_done:
self._start_timer()
return self
async def __aexit__(
self,
exc_type: Type[BaseException],
exc_val: BaseException,
exc_tb: TracebackType,
) -> Optional[bool]:
self._zone.exit_task(self)
self._stop_timer()
# Timeout on exit
if exc_type is asyncio.CancelledError and self.state == _State.TIMEOUT:
raise asyncio.TimeoutError
self._state = _State.EXIT
return None
def _start_timer(self) -> None:
"""Start timeout handler."""
if self._timeout_handler:
return
self._expiration_time = self._loop.time() + self._time_left
self._timeout_handler = self._loop.call_at(
self._expiration_time, self._on_timeout
)
def _stop_timer(self) -> None:
"""Stop zone timer."""
if self._timeout_handler is None:
return
self._timeout_handler.cancel()
self._timeout_handler = None
# Calculate new timeout
assert self._expiration_time
self._time_left = self._expiration_time - self._loop.time()
def _on_timeout(self) -> None:
"""Process timeout."""
self._state = _State.TIMEOUT
self._timeout_handler = None
# Timeout
if self._task.done():
return
self._task.cancel()
def pause(self) -> None:
"""Pause timers while it freeze."""
self._stop_timer()
def reset(self) -> None:
"""Reset timer after freeze."""
self._start_timer()
class _ZoneTimeoutManager:
"""Manage the timeouts for a zone."""
def __init__(self, manager: TimeoutManager, zone: str) -> None:
"""Initialize internal timeout context manager."""
self._manager: TimeoutManager = manager
self._zone: str = zone
self._tasks: List[_ZoneTaskContext] = []
self._freezes: List[_ZoneFreezeContext] = []
@property
def name(self) -> str:
"""Return Zone name."""
return self._zone
@property
def active(self) -> bool:
"""Return True if zone is active."""
return len(self._tasks) > 0 or len(self._freezes) > 0
@property
def freezes_done(self) -> bool:
"""Return True if all freeze are done."""
return len(self._freezes) == 0 and self._manager.freezes_done
def enter_task(self, task: _ZoneTaskContext) -> None:
"""Start into new Task."""
self._tasks.append(task)
def exit_task(self, task: _ZoneTaskContext) -> None:
"""Exit a running Task."""
self._tasks.remove(task)
# On latest listener
if not self.active:
self._manager.drop_zone(self.name)
def enter_freeze(self, freeze: _ZoneFreezeContext) -> None:
"""Start into new freeze."""
self._freezes.append(freeze)
def exit_freeze(self, freeze: _ZoneFreezeContext) -> None:
"""Exit a running Freeze."""
self._freezes.remove(freeze)
# On latest listener
if not self.active:
self._manager.drop_zone(self.name)
def pause(self) -> None:
"""Stop timers while it freeze."""
if not self.active:
return
# Forward pause
for task in self._tasks:
task.pause()
def reset(self) -> None:
"""Reset timer after freeze."""
if not self.active:
return
# Forward reset
for task in self._tasks:
task.reset()
class TimeoutManager:
"""Class to manage timeouts over different zones.
Manages both global and zone based timeouts.
"""
def __init__(self) -> None:
"""Initialize TimeoutManager."""
self._loop: asyncio.AbstractEventLoop = asyncio.get_running_loop()
self._zones: Dict[str, _ZoneTimeoutManager] = {}
self._globals: List[_GlobalTaskContext] = []
self._freezes: List[_GlobalFreezeContext] = []
@property
def zones_done(self) -> bool:
"""Return True if all zones are finished."""
return not bool(self._zones)
@property
def freezes_done(self) -> bool:
"""Return True if all freezes are finished."""
return not self._freezes
@property
def zones(self) -> Dict[str, _ZoneTimeoutManager]:
"""Return all Zones."""
return self._zones
@property
def global_tasks(self) -> List[_GlobalTaskContext]:
"""Return all global Tasks."""
return self._globals
@property
def global_freezes(self) -> List[_GlobalFreezeContext]:
"""Return all global Freezes."""
return self._freezes
def drop_zone(self, zone_name: str) -> None:
"""Drop a zone out of scope."""
self._zones.pop(zone_name, None)
if self._zones:
return
# Signal Global task, all zones are done
for task in self._globals:
task.zones_done_signal()
def async_timeout(
self, timeout: float, zone_name: str = ZONE_GLOBAL, cool_down: float = 0
) -> Union[_ZoneTaskContext, _GlobalTaskContext]:
"""Timeout based on a zone.
For using as Async Context Manager.
"""
current_task: Optional[asyncio.Task[Any]] = asyncio.current_task()
assert current_task
# Global Zone
if zone_name == ZONE_GLOBAL:
task = _GlobalTaskContext(self, current_task, timeout, cool_down)
return task
# Zone Handling
if zone_name in self.zones:
zone: _ZoneTimeoutManager = self.zones[zone_name]
else:
self.zones[zone_name] = zone = _ZoneTimeoutManager(self, zone_name)
# Create Task
return _ZoneTaskContext(zone, current_task, timeout)
def async_freeze(
self, zone_name: str = ZONE_GLOBAL
) -> Union[_ZoneFreezeContext, _GlobalFreezeContext]:
"""Freeze all timer until job is done.
For using as Async Context Manager.
"""
# Global Freeze
if zone_name == ZONE_GLOBAL:
return _GlobalFreezeContext(self)
# Zone Freeze
if zone_name in self.zones:
zone: _ZoneTimeoutManager = self.zones[zone_name]
else:
self.zones[zone_name] = zone = _ZoneTimeoutManager(self, zone_name)
return _ZoneFreezeContext(zone)
def freeze(
self, zone_name: str = ZONE_GLOBAL
) -> Union[_ZoneFreezeContext, _GlobalFreezeContext]:
"""Freeze all timer until job is done.
For using as Context Manager.
"""
return run_callback_threadsafe(
self._loop, self.async_freeze, zone_name
).result()

View File

@ -133,6 +133,7 @@ async def test_get_action_capabilities(hass, device_reg, entity_reg):
device_id=device_entry.id,
)
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
await hass.async_block_till_done()
expected_capabilities = {
"arm_away": {"extra_fields": []},
@ -170,6 +171,7 @@ async def test_get_action_capabilities_arm_code(hass, device_reg, entity_reg):
device_id=device_entry.id,
)
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
await hass.async_block_till_done()
expected_capabilities = {
"arm_away": {
@ -267,6 +269,8 @@ async def test_action(hass):
},
)
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
await hass.async_block_till_done()
assert (
hass.states.get("alarm_control_panel.alarm_no_arm_code").state == STATE_UNKNOWN
)

View File

@ -60,6 +60,7 @@ async def test_get_conditions(hass, device_reg, entity_reg):
)
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
await hass.async_block_till_done()
expected_conditions = [
{

View File

@ -60,6 +60,7 @@ async def test_get_triggers(hass, device_reg, entity_reg):
)
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
await hass.async_block_till_done()
expected_triggers = [
{

View File

@ -46,6 +46,7 @@ async def test_get_actions(hass, device_reg, entity_reg):
DOMAIN, "test", ent.unique_id, device_id=device_entry.id
)
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
await hass.async_block_till_done()
expected_actions = [
{
@ -87,6 +88,7 @@ async def test_get_actions_tilt(hass, device_reg, entity_reg):
DOMAIN, "test", ent.unique_id, device_id=device_entry.id
)
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
await hass.async_block_till_done()
expected_actions = [
{
@ -140,6 +142,7 @@ async def test_get_actions_set_pos(hass, device_reg, entity_reg):
DOMAIN, "test", ent.unique_id, device_id=device_entry.id
)
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
await hass.async_block_till_done()
expected_actions = [
{
@ -169,6 +172,7 @@ async def test_get_actions_set_tilt_pos(hass, device_reg, entity_reg):
DOMAIN, "test", ent.unique_id, device_id=device_entry.id
)
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
await hass.async_block_till_done()
expected_actions = [
{
@ -217,6 +221,7 @@ async def test_get_action_capabilities(hass, device_reg, entity_reg):
)
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
await hass.async_block_till_done()
actions = await async_get_device_automations(hass, "action", device_entry.id)
assert len(actions) == 3 # open, close, stop
@ -244,6 +249,7 @@ async def test_get_action_capabilities_set_pos(hass, device_reg, entity_reg):
)
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
await hass.async_block_till_done()
expected_capabilities = {
"extra_fields": [
@ -286,6 +292,7 @@ async def test_get_action_capabilities_set_tilt_pos(hass, device_reg, entity_reg
)
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
await hass.async_block_till_done()
expected_capabilities = {
"extra_fields": [
@ -352,6 +359,7 @@ async def test_action(hass):
]
},
)
await hass.async_block_till_done()
open_calls = async_mock_service(hass, "cover", "open_cover")
close_calls = async_mock_service(hass, "cover", "close_cover")
@ -408,6 +416,7 @@ async def test_action_tilt(hass):
]
},
)
await hass.async_block_till_done()
open_calls = async_mock_service(hass, "cover", "open_cover_tilt")
close_calls = async_mock_service(hass, "cover", "close_cover_tilt")
@ -468,6 +477,7 @@ async def test_action_set_position(hass):
]
},
)
await hass.async_block_till_done()
cover_pos_calls = async_mock_service(hass, "cover", "set_cover_position")
tilt_pos_calls = async_mock_service(hass, "cover", "set_cover_tilt_position")

View File

@ -366,6 +366,7 @@ async def test_reconnect(hass, monkeypatch, mock_connection_factory):
protocol.wait_closed = wait_closed
await async_setup_component(hass, "sensor", {"sensor": config})
await hass.async_block_till_done()
assert connection_factory.call_count == 1

View File

@ -104,6 +104,7 @@ async def test_valid_config_with_info(hass):
}
},
)
await hass.async_block_till_done()
async def test_valid_config_no_name(hass):
@ -114,6 +115,7 @@ async def test_valid_config_no_name(hass):
"switch",
{"switch": {"platform": "flux", "lights": ["light.desk", "light.lamp"]}},
)
await hass.async_block_till_done()
async def test_invalid_config_no_lights(hass):
@ -122,6 +124,7 @@ async def test_invalid_config_no_lights(hass):
assert await async_setup_component(
hass, "switch", {"switch": {"platform": "flux", "name": "flux"}}
)
await hass.async_block_till_done()
async def test_flux_when_switch_is_off(hass, legacy_patchable_time):
@ -168,6 +171,7 @@ async def test_flux_when_switch_is_off(hass, legacy_patchable_time):
}
},
)
await hass.async_block_till_done()
async_fire_time_changed(hass, test_time)
await hass.async_block_till_done()
@ -218,6 +222,7 @@ async def test_flux_before_sunrise(hass, legacy_patchable_time):
}
},
)
await hass.async_block_till_done()
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
await common.async_turn_on(hass, "switch.flux")
await hass.async_block_till_done()
@ -271,6 +276,7 @@ async def test_flux_before_sunrise_known_location(hass, legacy_patchable_time):
}
},
)
await hass.async_block_till_done()
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
await common.async_turn_on(hass, "switch.flux")
await hass.async_block_till_done()
@ -325,6 +331,7 @@ async def test_flux_after_sunrise_before_sunset(hass, legacy_patchable_time):
}
},
)
await hass.async_block_till_done()
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
await common.async_turn_on(hass, "switch.flux")
await hass.async_block_till_done()
@ -380,6 +387,7 @@ async def test_flux_after_sunset_before_stop(hass, legacy_patchable_time):
}
},
)
await hass.async_block_till_done()
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
common.turn_on(hass, "switch.flux")
await hass.async_block_till_done()
@ -434,6 +442,7 @@ async def test_flux_after_stop_before_sunrise(hass, legacy_patchable_time):
}
},
)
await hass.async_block_till_done()
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
common.turn_on(hass, "switch.flux")
await hass.async_block_till_done()
@ -490,6 +499,7 @@ async def test_flux_with_custom_start_stop_times(hass, legacy_patchable_time):
}
},
)
await hass.async_block_till_done()
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
common.turn_on(hass, "switch.flux")
await hass.async_block_till_done()
@ -547,6 +557,7 @@ async def test_flux_before_sunrise_stop_next_day(hass, legacy_patchable_time):
}
},
)
await hass.async_block_till_done()
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
common.turn_on(hass, "switch.flux")
await hass.async_block_till_done()
@ -608,6 +619,7 @@ async def test_flux_after_sunrise_before_sunset_stop_next_day(
}
},
)
await hass.async_block_till_done()
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
common.turn_on(hass, "switch.flux")
await hass.async_block_till_done()
@ -669,6 +681,7 @@ async def test_flux_after_sunset_before_midnight_stop_next_day(
}
},
)
await hass.async_block_till_done()
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
common.turn_on(hass, "switch.flux")
await hass.async_block_till_done()
@ -729,6 +742,7 @@ async def test_flux_after_sunset_after_midnight_stop_next_day(
}
},
)
await hass.async_block_till_done()
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
common.turn_on(hass, "switch.flux")
await hass.async_block_till_done()
@ -789,6 +803,7 @@ async def test_flux_after_stop_before_sunrise_stop_next_day(
}
},
)
await hass.async_block_till_done()
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
common.turn_on(hass, "switch.flux")
await hass.async_block_till_done()
@ -846,6 +861,7 @@ async def test_flux_with_custom_colortemps(hass, legacy_patchable_time):
}
},
)
await hass.async_block_till_done()
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
common.turn_on(hass, "switch.flux")
await hass.async_block_till_done()
@ -902,6 +918,7 @@ async def test_flux_with_custom_brightness(hass, legacy_patchable_time):
}
},
)
await hass.async_block_till_done()
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
common.turn_on(hass, "switch.flux")
await hass.async_block_till_done()
@ -974,6 +991,7 @@ async def test_flux_with_multiple_lights(hass, legacy_patchable_time):
}
},
)
await hass.async_block_till_done()
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
common.turn_on(hass, "switch.flux")
await hass.async_block_till_done()
@ -1033,6 +1051,7 @@ async def test_flux_with_mired(hass, legacy_patchable_time):
}
},
)
await hass.async_block_till_done()
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
common.turn_on(hass, "switch.flux")
await hass.async_block_till_done()
@ -1085,6 +1104,7 @@ async def test_flux_with_rgb(hass, legacy_patchable_time):
}
},
)
await hass.async_block_till_done()
turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
await common.async_turn_on(hass, "switch.flux")
await hass.async_block_till_done()

View File

@ -34,6 +34,7 @@ async def test_get_actions_support_open(hass, device_reg, entity_reg):
platform = getattr(hass.components, f"test.{DOMAIN}")
platform.init()
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
await hass.async_block_till_done()
config_entry = MockConfigEntry(domain="test", data={})
config_entry.add_to_hass(hass)
@ -77,6 +78,7 @@ async def test_get_actions_not_support_open(hass, device_reg, entity_reg):
platform = getattr(hass.components, f"test.{DOMAIN}")
platform.init()
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
await hass.async_block_till_done()
config_entry = MockConfigEntry(domain="test", data={})
config_entry.add_to_hass(hass)
@ -146,6 +148,7 @@ async def test_action(hass):
]
},
)
await hass.async_block_till_done()
lock_calls = async_mock_service(hass, "lock", "lock")
unlock_calls = async_mock_service(hass, "lock", "unlock")

View File

@ -188,8 +188,8 @@ async def test_platform_warn_slow_setup(hass):
assert mock_call.called
# mock_calls[0] is the warning message for component setup
# mock_calls[6] is the warning message for platform setup
timeout, logger_method = mock_call.mock_calls[6][1][:2]
# mock_calls[4] is the warning message for platform setup
timeout, logger_method = mock_call.mock_calls[4][1][:2]
assert timeout == entity_platform.SLOW_SETUP_WARNING
assert logger_method == _LOGGER.warning

View File

@ -1140,8 +1140,8 @@ async def test_start_taking_too_long(loop, caplog):
caplog.set_level(logging.WARNING)
try:
with patch(
"homeassistant.core.timeout", side_effect=asyncio.TimeoutError
with patch.object(
hass, "async_block_till_done", side_effect=asyncio.TimeoutError
), patch("homeassistant.core._async_create_timer") as mock_timer:
await hass.async_start()

View File

@ -1,15 +1,12 @@
"""Test requirements module."""
import os
from pathlib import Path
import pytest
from homeassistant import loader, setup
from homeassistant.requirements import (
CONSTRAINT_FILE,
PROGRESS_FILE,
RequirementsNotFound,
_install,
async_get_integration_with_requirements,
async_process_requirements,
)
@ -190,24 +187,6 @@ async def test_install_on_docker(hass):
)
async def test_progress_lock(hass):
"""Test an install attempt on an existing package."""
progress_path = Path(hass.config.path(PROGRESS_FILE))
kwargs = {"hello": "world"}
def assert_env(req, **passed_kwargs):
"""Assert the env."""
assert progress_path.exists()
assert req == "hello"
assert passed_kwargs == kwargs
return True
with patch("homeassistant.util.package.install_package", side_effect=assert_env):
_install(hass, "hello", kwargs)
assert not progress_path.exists()
async def test_discovery_requirements_ssdp(hass):
"""Test that we load discovery requirements."""
hass.config.skip_pip = False

View File

@ -488,15 +488,12 @@ async def test_component_warn_slow_setup(hass):
assert result
assert mock_call.called
assert len(mock_call.mock_calls) == 5
assert len(mock_call.mock_calls) == 3
timeout, logger_method = mock_call.mock_calls[0][1][:2]
assert timeout == setup.SLOW_SETUP_WARNING
assert logger_method == setup._LOGGER.warning
timeout, function = mock_call.mock_calls[1][1][:2]
assert timeout == setup.SLOW_SETUP_MAX_WAIT
assert mock_call().cancel.called
@ -508,8 +505,7 @@ async def test_platform_no_warn_slow(hass):
with patch.object(hass.loop, "call_later") as mock_call:
result = await setup.async_setup_component(hass, "test_component1", {})
assert result
timeout, function = mock_call.mock_calls[0][1][:2]
assert timeout == setup.SLOW_SETUP_MAX_WAIT
assert len(mock_call.mock_calls) == 0
async def test_platform_error_slow_setup(hass, caplog):

268
tests/util/test_timeout.py Normal file
View File

@ -0,0 +1,268 @@
"""Test Home Assistant timeout handler."""
import asyncio
import time
import pytest
from homeassistant.util.timeout import TimeoutManager
async def test_simple_global_timeout():
"""Test a simple global timeout."""
timeout = TimeoutManager()
with pytest.raises(asyncio.TimeoutError):
async with timeout.async_timeout(0.1):
await asyncio.sleep(0.3)
async def test_simple_global_timeout_with_executor_job(hass):
"""Test a simple global timeout with executor job."""
timeout = TimeoutManager()
with pytest.raises(asyncio.TimeoutError):
async with timeout.async_timeout(0.1):
await hass.async_add_executor_job(lambda: time.sleep(0.2))
async def test_simple_global_timeout_freeze():
"""Test a simple global timeout freeze."""
timeout = TimeoutManager()
async with timeout.async_timeout(0.2):
async with timeout.async_freeze():
await asyncio.sleep(0.3)
async def test_simple_zone_timeout_freeze_inside_executor_job(hass):
"""Test a simple zone timeout freeze inside an executor job."""
timeout = TimeoutManager()
def _some_sync_work():
with timeout.freeze("recorder"):
time.sleep(0.3)
async with timeout.async_timeout(1.0):
async with timeout.async_timeout(0.2, zone_name="recorder"):
await hass.async_add_executor_job(_some_sync_work)
async def test_simple_global_timeout_freeze_inside_executor_job(hass):
"""Test a simple global timeout freeze inside an executor job."""
timeout = TimeoutManager()
def _some_sync_work():
with timeout.freeze():
time.sleep(0.3)
async with timeout.async_timeout(0.2):
await hass.async_add_executor_job(_some_sync_work)
async def test_mix_global_timeout_freeze_and_zone_freeze_inside_executor_job(hass):
"""Test a simple global timeout freeze inside an executor job."""
timeout = TimeoutManager()
def _some_sync_work():
with timeout.freeze("recorder"):
time.sleep(0.3)
async with timeout.async_timeout(0.1):
async with timeout.async_timeout(0.2, zone_name="recorder"):
await hass.async_add_executor_job(_some_sync_work)
async def test_mix_global_timeout_freeze_and_zone_freeze_different_order(hass):
"""Test a simple global timeout freeze inside an executor job before timeout was set."""
timeout = TimeoutManager()
def _some_sync_work():
with timeout.freeze("recorder"):
time.sleep(0.4)
async with timeout.async_timeout(0.1):
hass.async_add_executor_job(_some_sync_work)
async with timeout.async_timeout(0.2, zone_name="recorder"):
await asyncio.sleep(0.3)
async def test_mix_global_timeout_freeze_and_zone_freeze_other_zone_inside_executor_job(
hass,
):
"""Test a simple global timeout freeze other zone inside an executor job."""
timeout = TimeoutManager()
def _some_sync_work():
with timeout.freeze("not_recorder"):
time.sleep(0.3)
with pytest.raises(asyncio.TimeoutError):
async with timeout.async_timeout(0.1):
async with timeout.async_timeout(0.2, zone_name="recorder"):
async with timeout.async_timeout(0.2, zone_name="not_recorder"):
await hass.async_add_executor_job(_some_sync_work)
async def test_mix_global_timeout_freeze_and_zone_freeze_inside_executor_job_second_job_outside_zone_context(
hass,
):
"""Test a simple global timeout freeze inside an executor job with second job outside of zone context."""
timeout = TimeoutManager()
def _some_sync_work():
with timeout.freeze("recorder"):
time.sleep(0.3)
with pytest.raises(asyncio.TimeoutError):
async with timeout.async_timeout(0.1):
async with timeout.async_timeout(0.2, zone_name="recorder"):
await hass.async_add_executor_job(_some_sync_work)
await hass.async_add_executor_job(lambda: time.sleep(0.2))
async def test_simple_global_timeout_freeze_with_executor_job(hass):
"""Test a simple global timeout freeze with executor job."""
timeout = TimeoutManager()
async with timeout.async_timeout(0.2):
async with timeout.async_freeze():
await hass.async_add_executor_job(lambda: time.sleep(0.3))
async def test_simple_global_timeout_freeze_reset():
"""Test a simple global timeout freeze reset."""
timeout = TimeoutManager()
with pytest.raises(asyncio.TimeoutError):
async with timeout.async_timeout(0.2):
async with timeout.async_freeze():
await asyncio.sleep(0.1)
await asyncio.sleep(0.2)
async def test_simple_zone_timeout():
"""Test a simple zone timeout."""
timeout = TimeoutManager()
with pytest.raises(asyncio.TimeoutError):
async with timeout.async_timeout(0.1, "test"):
await asyncio.sleep(0.3)
async def test_multiple_zone_timeout():
"""Test a simple zone timeout."""
timeout = TimeoutManager()
with pytest.raises(asyncio.TimeoutError):
async with timeout.async_timeout(0.1, "test"):
async with timeout.async_timeout(0.5, "test"):
await asyncio.sleep(0.3)
async def test_different_zone_timeout():
"""Test a simple zone timeout."""
timeout = TimeoutManager()
with pytest.raises(asyncio.TimeoutError):
async with timeout.async_timeout(0.1, "test"):
async with timeout.async_timeout(0.5, "other"):
await asyncio.sleep(0.3)
async def test_simple_zone_timeout_freeze():
"""Test a simple zone timeout freeze."""
timeout = TimeoutManager()
async with timeout.async_timeout(0.2, "test"):
async with timeout.async_freeze("test"):
await asyncio.sleep(0.3)
async def test_simple_zone_timeout_freeze_without_timeout():
"""Test a simple zone timeout freeze on a zone that does not have a timeout set."""
timeout = TimeoutManager()
async with timeout.async_timeout(0.1, "test"):
async with timeout.async_freeze("test"):
await asyncio.sleep(0.3)
async def test_simple_zone_timeout_freeze_reset():
"""Test a simple zone timeout freeze reset."""
timeout = TimeoutManager()
with pytest.raises(asyncio.TimeoutError):
async with timeout.async_timeout(0.2, "test"):
async with timeout.async_freeze("test"):
await asyncio.sleep(0.1)
await asyncio.sleep(0.2, "test")
async def test_mix_zone_timeout_freeze_and_global_freeze():
"""Test a mix zone timeout freeze and global freeze."""
timeout = TimeoutManager()
async with timeout.async_timeout(0.2, "test"):
async with timeout.async_freeze("test"):
async with timeout.async_freeze():
await asyncio.sleep(0.3)
async def test_mix_global_and_zone_timeout_freeze_():
"""Test a mix zone timeout freeze and global freeze."""
timeout = TimeoutManager()
async with timeout.async_timeout(0.2, "test"):
async with timeout.async_freeze():
async with timeout.async_freeze("test"):
await asyncio.sleep(0.3)
async def test_mix_zone_timeout_freeze():
"""Test a mix zone timeout global freeze."""
timeout = TimeoutManager()
async with timeout.async_timeout(0.2, "test"):
async with timeout.async_freeze():
await asyncio.sleep(0.3)
async def test_mix_zone_timeout():
"""Test a mix zone timeout global."""
timeout = TimeoutManager()
async with timeout.async_timeout(0.1):
try:
async with timeout.async_timeout(0.2, "test"):
await asyncio.sleep(0.4)
except asyncio.TimeoutError:
pass
async def test_mix_zone_timeout_trigger_global():
"""Test a mix zone timeout global with trigger it."""
timeout = TimeoutManager()
with pytest.raises(asyncio.TimeoutError):
async with timeout.async_timeout(0.1):
try:
async with timeout.async_timeout(0.1, "test"):
await asyncio.sleep(0.3)
except asyncio.TimeoutError:
pass
await asyncio.sleep(0.3)
async def test_mix_zone_timeout_trigger_global_cool_down():
"""Test a mix zone timeout global with trigger it with cool_down."""
timeout = TimeoutManager()
async with timeout.async_timeout(0.1, cool_down=0.3):
try:
async with timeout.async_timeout(0.1, "test"):
await asyncio.sleep(0.3)
except asyncio.TimeoutError:
pass
await asyncio.sleep(0.2)