Slowdown docker stop/start to avoid issue on slow IO (#1763)

* Slowdown docker stop/start to avoid issue on slow IO

* Fix lint

* Address comments

* Fix restore logic & Style

* Fix type

* Slow plugins
This commit is contained in:
Pascal Vizeli 2020-06-02 11:26:10 +02:00 committed by GitHub
parent 7c345db6fe
commit e50515a17c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 95 additions and 72 deletions

View File

@ -5,7 +5,7 @@ import logging
import tarfile import tarfile
from typing import Dict, List, Optional, Union from typing import Dict, List, Optional, Union
from ..const import BOOT_AUTO, STATE_STARTED from ..const import BOOT_AUTO, STATE_STARTED, AddonStartup
from ..coresys import CoreSys, CoreSysAttributes from ..coresys import CoreSys, CoreSysAttributes
from ..exceptions import ( from ..exceptions import (
AddonsError, AddonsError,
@ -78,30 +78,49 @@ class AddonManager(CoreSysAttributes):
# Sync DNS # Sync DNS
await self.sync_dns() await self.sync_dns()
async def boot(self, stage: str) -> None: async def boot(self, stage: AddonStartup) -> None:
"""Boot add-ons with mode auto.""" """Boot add-ons with mode auto."""
tasks = [] tasks: List[Addon] = []
for addon in self.installed: for addon in self.installed:
if addon.boot != BOOT_AUTO or addon.startup != stage: if addon.boot != BOOT_AUTO or addon.startup != stage:
continue continue
tasks.append(addon.start()) tasks.append(addon)
# Evaluate add-ons which need to be started
_LOGGER.info("Phase '%s' start %d add-ons", stage, len(tasks)) _LOGGER.info("Phase '%s' start %d add-ons", stage, len(tasks))
if tasks: if not tasks:
await asyncio.wait(tasks) return
await asyncio.sleep(self.sys_config.wait_boot)
async def shutdown(self, stage: str) -> None: # Start Add-ons sequential
# avoid issue on slow IO
for addon in tasks:
try:
await addon.start()
except Exception as err: # pylint: disable=broad-except
_LOGGER.warning("Can't start Add-on %s: %s", addon.slug, err)
await asyncio.sleep(self.sys_config.wait_boot)
async def shutdown(self, stage: AddonStartup) -> None:
"""Shutdown addons.""" """Shutdown addons."""
tasks = [] tasks: List[Addon] = []
for addon in self.installed: for addon in self.installed:
if await addon.state() != STATE_STARTED or addon.startup != stage: if await addon.state() != STATE_STARTED or addon.startup != stage:
continue continue
tasks.append(addon.stop()) tasks.append(addon)
# Evaluate add-ons which need to be stopped
_LOGGER.info("Phase '%s' stop %d add-ons", stage, len(tasks)) _LOGGER.info("Phase '%s' stop %d add-ons", stage, len(tasks))
if tasks: if not tasks:
await asyncio.wait(tasks) return
# Stop Add-ons sequential
# avoid issue on slow IO
for addon in tasks:
try:
await addon.stop()
except Exception as err: # pylint: disable=broad-except
_LOGGER.warning("Can't stop Add-on %s: %s", addon.slug, err)
async def install(self, slug: str) -> None: async def install(self, slug: str) -> None:
"""Install an add-on.""" """Install an add-on."""

View File

@ -64,6 +64,7 @@ from ..const import (
SECURITY_DISABLE, SECURITY_DISABLE,
SECURITY_PROFILE, SECURITY_PROFILE,
AddonStages, AddonStages,
AddonStartup,
) )
from ..coresys import CoreSys, CoreSysAttributes from ..coresys import CoreSys, CoreSysAttributes
from .validate import RE_SERVICE, RE_VOLUME, schema_ui_options, validate_options from .validate import RE_SERVICE, RE_VOLUME, schema_ui_options, validate_options
@ -193,9 +194,9 @@ class AddonModel(CoreSysAttributes):
return True return True
@property @property
def startup(self) -> Optional[str]: def startup(self) -> AddonStartup:
"""Return startup type of add-on.""" """Return startup type of add-on."""
return self.data.get(ATTR_STARTUP) return self.data[ATTR_STARTUP]
@property @property
def advanced(self) -> bool: def advanced(self) -> bool:

View File

@ -85,12 +85,10 @@ from ..const import (
PRIVILEGED_ALL, PRIVILEGED_ALL,
ROLE_ALL, ROLE_ALL,
ROLE_DEFAULT, ROLE_DEFAULT,
STARTUP_ALL,
STARTUP_APPLICATION,
STARTUP_SERVICES,
STATE_STARTED, STATE_STARTED,
STATE_STOPPED, STATE_STOPPED,
AddonStages, AddonStages,
AddonStartup,
) )
from ..coresys import CoreSys from ..coresys import CoreSys
from ..discovery.validate import valid_discovery_service from ..discovery.validate import valid_discovery_service
@ -169,12 +167,12 @@ MACHINE_ALL = [
] ]
def _simple_startup(value): def _simple_startup(value) -> str:
"""Define startup schema.""" """Define startup schema."""
if value == "before": if value == "before":
return STARTUP_SERVICES return AddonStartup.SERVICES.value
if value == "after": if value == "after":
return STARTUP_APPLICATION return AddonStartup.APPLICATION.value
return value return value
@ -188,7 +186,7 @@ SCHEMA_ADDON_CONFIG = vol.Schema(
vol.Required(ATTR_ARCH): [vol.In(ARCH_ALL)], vol.Required(ATTR_ARCH): [vol.In(ARCH_ALL)],
vol.Optional(ATTR_MACHINE): [vol.In(MACHINE_ALL)], vol.Optional(ATTR_MACHINE): [vol.In(MACHINE_ALL)],
vol.Optional(ATTR_URL): vol.Url(), vol.Optional(ATTR_URL): vol.Url(),
vol.Required(ATTR_STARTUP): vol.All(_simple_startup, vol.In(STARTUP_ALL)), vol.Required(ATTR_STARTUP): vol.All(_simple_startup, vol.Coerce(AddonStartup)),
vol.Required(ATTR_BOOT): vol.In([BOOT_AUTO, BOOT_MANUAL]), vol.Required(ATTR_BOOT): vol.In([BOOT_AUTO, BOOT_MANUAL]),
vol.Optional(ATTR_INIT, default=True): vol.Boolean(), vol.Optional(ATTR_INIT, default=True): vol.Boolean(),
vol.Optional(ATTR_ADVANCED, default=False): vol.Boolean(), vol.Optional(ATTR_ADVANCED, default=False): vol.Boolean(),

View File

@ -245,20 +245,6 @@ PROVIDE_SERVICE = "provide"
NEED_SERVICE = "need" NEED_SERVICE = "need"
WANT_SERVICE = "want" WANT_SERVICE = "want"
STARTUP_INITIALIZE = "initialize"
STARTUP_SYSTEM = "system"
STARTUP_SERVICES = "services"
STARTUP_APPLICATION = "application"
STARTUP_ONCE = "once"
STARTUP_ALL = [
STARTUP_ONCE,
STARTUP_INITIALIZE,
STARTUP_SYSTEM,
STARTUP_SERVICES,
STARTUP_APPLICATION,
]
BOOT_AUTO = "auto" BOOT_AUTO = "auto"
BOOT_MANUAL = "manual" BOOT_MANUAL = "manual"
@ -339,6 +325,16 @@ CHAN_ID = "chan_id"
CHAN_TYPE = "chan_type" CHAN_TYPE = "chan_type"
class AddonStartup(str, Enum):
"""Startup types of Add-on."""
INITIALIZE = "initialize"
SYSTEM = "system"
SERVICES = "services"
APPLICATION = "application"
ONCE = "once"
class AddonStages(str, Enum): class AddonStages(str, Enum):
"""Stage types of add-on.""" """Stage types of add-on."""

View File

@ -5,13 +5,7 @@ import logging
import async_timeout import async_timeout
from .const import ( from .const import AddonStartup, CoreStates
STARTUP_APPLICATION,
STARTUP_INITIALIZE,
STARTUP_SERVICES,
STARTUP_SYSTEM,
CoreStates,
)
from .coresys import CoreSys, CoreSysAttributes from .coresys import CoreSys, CoreSysAttributes
from .exceptions import HassioError, HomeAssistantError, SupervisorUpdateError from .exceptions import HassioError, HomeAssistantError, SupervisorUpdateError
@ -133,7 +127,7 @@ class Core(CoreSysAttributes):
) )
# Start addon mark as initialize # Start addon mark as initialize
await self.sys_addons.boot(STARTUP_INITIALIZE) await self.sys_addons.boot(AddonStartup.INITIALIZE)
try: try:
# HomeAssistant is already running / supervisor have only reboot # HomeAssistant is already running / supervisor have only reboot
@ -145,10 +139,10 @@ class Core(CoreSysAttributes):
self.sys_services.reset() self.sys_services.reset()
# start addon mark as system # start addon mark as system
await self.sys_addons.boot(STARTUP_SYSTEM) await self.sys_addons.boot(AddonStartup.SYSTEM)
# start addon mark as services # start addon mark as services
await self.sys_addons.boot(STARTUP_SERVICES) await self.sys_addons.boot(AddonStartup.SERVICES)
# run HomeAssistant # run HomeAssistant
if ( if (
@ -161,7 +155,7 @@ class Core(CoreSysAttributes):
_LOGGER.info("Skip start of Home Assistant") _LOGGER.info("Skip start of Home Assistant")
# start addon mark as application # start addon mark as application
await self.sys_addons.boot(STARTUP_APPLICATION) await self.sys_addons.boot(AddonStartup.APPLICATION)
# store new last boot # store new last boot
self._update_last_boot() self._update_last_boot()
@ -214,22 +208,24 @@ class Core(CoreSysAttributes):
async def shutdown(self): async def shutdown(self):
"""Shutdown all running containers in correct order.""" """Shutdown all running containers in correct order."""
# don't process scheduler anymore # don't process scheduler anymore
self.state = CoreStates.STOPPING if self.state == CoreStates.RUNNING:
self.state = CoreStates.STOPPING
# Shutdown Application Add-ons, using Home Assistant API # Shutdown Application Add-ons, using Home Assistant API
await self.sys_addons.shutdown(STARTUP_APPLICATION) await self.sys_addons.shutdown(AddonStartup.APPLICATION)
# Close Home Assistant # Close Home Assistant
with suppress(HassioError): with suppress(HassioError):
await self.sys_homeassistant.stop() await self.sys_homeassistant.stop()
# Shutdown System Add-ons # Shutdown System Add-ons
await self.sys_addons.shutdown(STARTUP_SERVICES) await self.sys_addons.shutdown(AddonStartup.SERVICES)
await self.sys_addons.shutdown(STARTUP_SYSTEM) await self.sys_addons.shutdown(AddonStartup.SYSTEM)
await self.sys_addons.shutdown(STARTUP_INITIALIZE) await self.sys_addons.shutdown(AddonStartup.INITIALIZE)
# Shutdown all Plugins # Shutdown all Plugins
await self.sys_plugins.shutdown() if self.state == CoreStates.STOPPING:
await self.sys_plugins.shutdown()
def _update_last_boot(self): def _update_last_boot(self):
"""Update last boot time.""" """Update last boot time."""

View File

@ -1,9 +1,9 @@
"""A collection of tasks.""" """A collection of tasks."""
import asyncio
import logging import logging
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..exceptions import ( from ..exceptions import (
AddonsError,
AudioError, AudioError,
CliError, CliError,
CoreDNSError, CoreDNSError,
@ -131,24 +131,26 @@ class Tasks(CoreSysAttributes):
async def _update_addons(self): async def _update_addons(self):
"""Check if an update is available for an Add-on and update it.""" """Check if an update is available for an Add-on and update it."""
tasks = []
for addon in self.sys_addons.all: for addon in self.sys_addons.all:
if not addon.is_installed or not addon.auto_update: if not addon.is_installed or not addon.auto_update:
continue continue
# Evaluate available updates
if addon.version == addon.latest_version: if addon.version == addon.latest_version:
continue continue
if not addon.test_update_schema():
if addon.test_update_schema():
tasks.append(addon.update())
else:
_LOGGER.warning( _LOGGER.warning(
"Add-on %s will be ignored, schema tests fails", addon.slug "Add-on %s will be ignored, schema tests fails", addon.slug
) )
continue
if tasks: # Run Add-on update sequential
_LOGGER.info("Add-on auto update process %d tasks", len(tasks)) # avoid issue on slow IO
await asyncio.wait(tasks) _LOGGER.info("Add-on auto update process %s", addon.slug)
try:
await addon.update()
except AddonsError:
_LOGGER.error("Can't auto update Add-on %s", addon.slug)
async def _update_supervisor(self): async def _update_supervisor(self):
"""Check and run update of Supervisor Supervisor.""" """Check and run update of Supervisor Supervisor."""

View File

@ -51,9 +51,17 @@ class PluginManager(CoreSysAttributes):
async def load(self): async def load(self):
"""Load Supervisor plugins.""" """Load Supervisor plugins."""
await asyncio.wait( # Sequential to avoid issue on slow IO
[self.dns.load(), self.audio.load(), self.cli.load(), self.multicast.load()] for plugin in (
) self.dns,
self.audio,
self.cli,
self.multicast,
):
try:
await plugin.load()
except Exception: # pylint: disable=broad-except
_LOGGER.warning("Can't load plugin %s", type(plugin).__name__)
# Check requirements # Check requirements
for plugin, required_version in ( for plugin, required_version in (
@ -103,11 +111,14 @@ class PluginManager(CoreSysAttributes):
async def shutdown(self) -> None: async def shutdown(self) -> None:
"""Shutdown Supervisor plugin.""" """Shutdown Supervisor plugin."""
await asyncio.wait( # Sequential to avoid issue on slow IO
[ for plugin in (
self.dns.stop(), self.audio,
self.audio.stop(), self.cli,
self.cli.stop(), self.multicast,
self.multicast.stop(), self.dns,
] ):
) try:
await plugin.stop()
except Exception: # pylint: disable=broad-except
_LOGGER.warning("Can't stop plugin %s", type(plugin).__name__)