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 time import monotonic
from typing import TYPE_CHECKING, Any, Dict, Optional, Set from typing import TYPE_CHECKING, Any, Dict, Optional, Set
from async_timeout import timeout
import voluptuous as vol import voluptuous as vol
import yarl import yarl
@ -44,6 +43,11 @@ DATA_LOGGING = "logging"
LOG_SLOW_STARTUP_INTERVAL = 60 LOG_SLOW_STARTUP_INTERVAL = 60
STAGE_1_TIMEOUT = 120
STAGE_2_TIMEOUT = 300
WRAP_UP_TIMEOUT = 300
COOLDOWN_TIME = 60
DEBUGGER_INTEGRATIONS = {"debugpy", "ptvsd"} DEBUGGER_INTEGRATIONS = {"debugpy", "ptvsd"}
CORE_INTEGRATIONS = ("homeassistant", "persistent_notification") CORE_INTEGRATIONS = ("homeassistant", "persistent_notification")
LOGGING_INTEGRATIONS = { LOGGING_INTEGRATIONS = {
@ -136,7 +140,7 @@ async def async_setup_hass(
hass.async_track_tasks() hass.async_track_tasks()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP, {}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP, {})
with contextlib.suppress(asyncio.TimeoutError): with contextlib.suppress(asyncio.TimeoutError):
async with timeout(10): async with hass.timeout.async_timeout(10):
await hass.async_block_till_done() await hass.async_block_till_done()
safe_mode = True 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 stage_2_domains = domains_to_setup - logging_domains - debuggers - stage_1_domains
# Kick off loading the registries. They don't need to be awaited. # Kick off loading the registries. They don't need to be awaited.
asyncio.gather( asyncio.create_task(hass.helpers.device_registry.async_get_registry())
hass.helpers.device_registry.async_get_registry(), asyncio.create_task(hass.helpers.entity_registry.async_get_registry())
hass.helpers.entity_registry.async_get_registry(), asyncio.create_task(hass.helpers.area_registry.async_get_registry())
hass.helpers.area_registry.async_get_registry(),
)
# Start setup # Start setup
if stage_1_domains: if stage_1_domains:
_LOGGER.info("Setting up stage 1: %s", 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 # Enables after dependencies
async_set_domains_to_be_loaded(hass, stage_1_domains | stage_2_domains) async_set_domains_to_be_loaded(hass, stage_1_domains | stage_2_domains)
if stage_2_domains: if stage_2_domains:
_LOGGER.info("Setting up stage 2: %s", 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 # Wrap up startup
_LOGGER.debug("Waiting for startup to wrap up") _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 import homeassistant.util.dt as dt_util
from . import migration, purge 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 .models import Base, Events, RecorderRuns, States
from .util import session_scope, validate_or_move_away_sqlite_database from .util import session_scope, validate_or_move_away_sqlite_database
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DOMAIN = "recorder"
SERVICE_PURGE = "purge" SERVICE_PURGE = "purge"
ATTR_KEEP_DAYS = "keep_days" ATTR_KEEP_DAYS = "keep_days"

View file

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

View file

@ -1,22 +1,19 @@
"""Schema migration helpers.""" """Schema migration helpers."""
import logging import logging
import os
from sqlalchemy import Table, text from sqlalchemy import Table, text
from sqlalchemy.engine import reflection from sqlalchemy.engine import reflection
from sqlalchemy.exc import InternalError, OperationalError, SQLAlchemyError from sqlalchemy.exc import InternalError, OperationalError, SQLAlchemyError
from .const import DOMAIN
from .models import SCHEMA_VERSION, Base, SchemaChanges from .models import SCHEMA_VERSION, Base, SchemaChanges
from .util import session_scope from .util import session_scope
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
PROGRESS_FILE = ".migration_progress"
def migrate_schema(instance): def migrate_schema(instance):
"""Check if the schema needs to be upgraded.""" """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: with session_scope(session=instance.get_session()) as session:
res = ( res = (
session.query(SchemaChanges) session.query(SchemaChanges)
@ -32,20 +29,13 @@ def migrate_schema(instance):
) )
if current_version == SCHEMA_VERSION: 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 return
with open(progress_path, "w"):
pass
_LOGGER.warning( _LOGGER.warning(
"Database is about to upgrade. Schema version: %s", current_version "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): for version in range(current_version, SCHEMA_VERSION):
new_version = version + 1 new_version = version + 1
_LOGGER.info("Upgrading recorder db schema to version %s", new_version) _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)) session.add(SchemaChanges(schema_version=new_version))
_LOGGER.info("Upgrade to version %s done", 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): def _create_index(engine, table_name, index_name):

View file

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

View file

@ -177,7 +177,8 @@ class EntityPlatform:
try: try:
task = async_create_setup_task() 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 # Block till all entities are done
if self._tasks: if self._tasks:

View file

@ -2,7 +2,6 @@
import asyncio import asyncio
import logging import logging
import os import os
from pathlib import Path
from typing import Any, Dict, Iterable, List, Optional, Set, Union, cast from typing import Any, Dict, Iterable, List, Optional, Set, Union, cast
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -14,7 +13,6 @@ DATA_PIP_LOCK = "pip_lock"
DATA_PKG_CACHE = "pkg_cache" DATA_PKG_CACHE = "pkg_cache"
DATA_INTEGRATIONS_WITH_REQS = "integrations_with_reqs" DATA_INTEGRATIONS_WITH_REQS = "integrations_with_reqs"
CONSTRAINT_FILE = "package_constraints.txt" CONSTRAINT_FILE = "package_constraints.txt"
PROGRESS_FILE = ".pip_progress"
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DISCOVERY_INTEGRATIONS: Dict[str, Iterable[str]] = { DISCOVERY_INTEGRATIONS: Dict[str, Iterable[str]] = {
"ssdp": ("ssdp",), "ssdp": ("ssdp",),
@ -124,22 +122,16 @@ async def async_process_requirements(
if pkg_util.is_installed(req): if pkg_util.is_installed(req):
continue 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: if not ret:
raise RequirementsNotFound(name, [req]) 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]: def pip_kwargs(config_dir: Optional[str]) -> Dict[str, Any]:
"""Return keyword arguments for PIP install.""" """Return keyword arguments for PIP install."""
is_docker = pkg_util.is_docker_env() is_docker = pkg_util.is_docker_env()

View file

@ -22,11 +22,7 @@ DATA_SETUP = "setup_tasks"
DATA_DEPS_REQS = "deps_reqs_processed" DATA_DEPS_REQS = "deps_reqs_processed"
SLOW_SETUP_WARNING = 10 SLOW_SETUP_WARNING = 10
SLOW_SETUP_MAX_WAIT = 300
# 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
@core.callback @core.callback
@ -89,7 +85,8 @@ async def _async_process_dependencies(
return True return True
_LOGGER.debug("Dependency %s will wait for %s", integration.domain, list(tasks)) _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 = [ failed = [
domain domain
@ -190,7 +187,8 @@ async def _async_setup_component(
hass.data[DATA_SETUP_STARTED].pop(domain) hass.data[DATA_SETUP_STARTED].pop(domain)
return False 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: except asyncio.TimeoutError:
_LOGGER.error( _LOGGER.error(
"Setup of %s is taking longer than %s seconds." "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.") raise HomeAssistantError("Could not set up all dependencies.")
if not hass.config.skip_pip and integration.requirements: if not hass.config.skip_pip and integration.requirements:
await requirements.async_get_integration_with_requirements( async with hass.timeout.async_freeze(integration.domain):
hass, integration.domain await requirements.async_get_integration_with_requirements(
) hass, integration.domain
)
processed.add(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, device_id=device_entry.id,
) )
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
await hass.async_block_till_done()
expected_capabilities = { expected_capabilities = {
"arm_away": {"extra_fields": []}, "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, device_id=device_entry.id,
) )
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
await hass.async_block_till_done()
expected_capabilities = { expected_capabilities = {
"arm_away": { "arm_away": {
@ -267,6 +269,8 @@ async def test_action(hass):
}, },
) )
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
await hass.async_block_till_done()
assert ( assert (
hass.states.get("alarm_control_panel.alarm_no_arm_code").state == STATE_UNKNOWN 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"}}) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
await hass.async_block_till_done()
expected_conditions = [ 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"}}) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
await hass.async_block_till_done()
expected_triggers = [ 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 DOMAIN, "test", ent.unique_id, device_id=device_entry.id
) )
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
await hass.async_block_till_done()
expected_actions = [ 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 DOMAIN, "test", ent.unique_id, device_id=device_entry.id
) )
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
await hass.async_block_till_done()
expected_actions = [ 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 DOMAIN, "test", ent.unique_id, device_id=device_entry.id
) )
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
await hass.async_block_till_done()
expected_actions = [ 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 DOMAIN, "test", ent.unique_id, device_id=device_entry.id
) )
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
await hass.async_block_till_done()
expected_actions = [ 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"}}) 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) actions = await async_get_device_automations(hass, "action", device_entry.id)
assert len(actions) == 3 # open, close, stop 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"}}) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
await hass.async_block_till_done()
expected_capabilities = { expected_capabilities = {
"extra_fields": [ "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"}}) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}})
await hass.async_block_till_done()
expected_capabilities = { expected_capabilities = {
"extra_fields": [ "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") open_calls = async_mock_service(hass, "cover", "open_cover")
close_calls = async_mock_service(hass, "cover", "close_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") open_calls = async_mock_service(hass, "cover", "open_cover_tilt")
close_calls = async_mock_service(hass, "cover", "close_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") cover_pos_calls = async_mock_service(hass, "cover", "set_cover_position")
tilt_pos_calls = async_mock_service(hass, "cover", "set_cover_tilt_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 protocol.wait_closed = wait_closed
await async_setup_component(hass, "sensor", {"sensor": config}) await async_setup_component(hass, "sensor", {"sensor": config})
await hass.async_block_till_done()
assert connection_factory.call_count == 1 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): async def test_valid_config_no_name(hass):
@ -114,6 +115,7 @@ async def test_valid_config_no_name(hass):
"switch", "switch",
{"switch": {"platform": "flux", "lights": ["light.desk", "light.lamp"]}}, {"switch": {"platform": "flux", "lights": ["light.desk", "light.lamp"]}},
) )
await hass.async_block_till_done()
async def test_invalid_config_no_lights(hass): 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( assert await async_setup_component(
hass, "switch", {"switch": {"platform": "flux", "name": "flux"}} 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): 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) async_fire_time_changed(hass, test_time)
await hass.async_block_till_done() 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) turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
await common.async_turn_on(hass, "switch.flux") await common.async_turn_on(hass, "switch.flux")
await hass.async_block_till_done() 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) turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
await common.async_turn_on(hass, "switch.flux") await common.async_turn_on(hass, "switch.flux")
await hass.async_block_till_done() 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) turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
await common.async_turn_on(hass, "switch.flux") await common.async_turn_on(hass, "switch.flux")
await hass.async_block_till_done() 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) turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
common.turn_on(hass, "switch.flux") common.turn_on(hass, "switch.flux")
await hass.async_block_till_done() 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) turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
common.turn_on(hass, "switch.flux") common.turn_on(hass, "switch.flux")
await hass.async_block_till_done() 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) turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
common.turn_on(hass, "switch.flux") common.turn_on(hass, "switch.flux")
await hass.async_block_till_done() 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) turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
common.turn_on(hass, "switch.flux") common.turn_on(hass, "switch.flux")
await hass.async_block_till_done() 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) turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
common.turn_on(hass, "switch.flux") common.turn_on(hass, "switch.flux")
await hass.async_block_till_done() 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) turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
common.turn_on(hass, "switch.flux") common.turn_on(hass, "switch.flux")
await hass.async_block_till_done() 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) turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
common.turn_on(hass, "switch.flux") common.turn_on(hass, "switch.flux")
await hass.async_block_till_done() 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) turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
common.turn_on(hass, "switch.flux") common.turn_on(hass, "switch.flux")
await hass.async_block_till_done() 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) turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
common.turn_on(hass, "switch.flux") common.turn_on(hass, "switch.flux")
await hass.async_block_till_done() 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) turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
common.turn_on(hass, "switch.flux") common.turn_on(hass, "switch.flux")
await hass.async_block_till_done() 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) turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
common.turn_on(hass, "switch.flux") common.turn_on(hass, "switch.flux")
await hass.async_block_till_done() 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) turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
common.turn_on(hass, "switch.flux") common.turn_on(hass, "switch.flux")
await hass.async_block_till_done() 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) turn_on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
await common.async_turn_on(hass, "switch.flux") await common.async_turn_on(hass, "switch.flux")
await hass.async_block_till_done() 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 = getattr(hass.components, f"test.{DOMAIN}")
platform.init() platform.init()
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) 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 = MockConfigEntry(domain="test", data={})
config_entry.add_to_hass(hass) 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 = getattr(hass.components, f"test.{DOMAIN}")
platform.init() platform.init()
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) 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 = MockConfigEntry(domain="test", data={})
config_entry.add_to_hass(hass) 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") lock_calls = async_mock_service(hass, "lock", "lock")
unlock_calls = async_mock_service(hass, "lock", "unlock") 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 assert mock_call.called
# mock_calls[0] is the warning message for component setup # mock_calls[0] is the warning message for component setup
# mock_calls[6] is the warning message for platform setup # mock_calls[4] is the warning message for platform setup
timeout, logger_method = mock_call.mock_calls[6][1][:2] timeout, logger_method = mock_call.mock_calls[4][1][:2]
assert timeout == entity_platform.SLOW_SETUP_WARNING assert timeout == entity_platform.SLOW_SETUP_WARNING
assert logger_method == _LOGGER.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) caplog.set_level(logging.WARNING)
try: try:
with patch( with patch.object(
"homeassistant.core.timeout", side_effect=asyncio.TimeoutError hass, "async_block_till_done", side_effect=asyncio.TimeoutError
), patch("homeassistant.core._async_create_timer") as mock_timer: ), patch("homeassistant.core._async_create_timer") as mock_timer:
await hass.async_start() await hass.async_start()

View file

@ -1,15 +1,12 @@
"""Test requirements module.""" """Test requirements module."""
import os import os
from pathlib import Path
import pytest import pytest
from homeassistant import loader, setup from homeassistant import loader, setup
from homeassistant.requirements import ( from homeassistant.requirements import (
CONSTRAINT_FILE, CONSTRAINT_FILE,
PROGRESS_FILE,
RequirementsNotFound, RequirementsNotFound,
_install,
async_get_integration_with_requirements, async_get_integration_with_requirements,
async_process_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): async def test_discovery_requirements_ssdp(hass):
"""Test that we load discovery requirements.""" """Test that we load discovery requirements."""
hass.config.skip_pip = False hass.config.skip_pip = False

View file

@ -488,15 +488,12 @@ async def test_component_warn_slow_setup(hass):
assert result assert result
assert mock_call.called 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] timeout, logger_method = mock_call.mock_calls[0][1][:2]
assert timeout == setup.SLOW_SETUP_WARNING assert timeout == setup.SLOW_SETUP_WARNING
assert logger_method == setup._LOGGER.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 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: with patch.object(hass.loop, "call_later") as mock_call:
result = await setup.async_setup_component(hass, "test_component1", {}) result = await setup.async_setup_component(hass, "test_component1", {})
assert result assert result
timeout, function = mock_call.mock_calls[0][1][:2] assert len(mock_call.mock_calls) == 0
assert timeout == setup.SLOW_SETUP_MAX_WAIT
async def test_platform_error_slow_setup(hass, caplog): 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)