Skip to content

Commit 76d41bc

Browse files
authored
Merge pull request #2433 from AppDaemon/restart-logic
restart fix
2 parents 00b6c39 + 3a64001 commit 76d41bc

File tree

2 files changed

+83
-58
lines changed

2 files changed

+83
-58
lines changed

appdaemon/app_management.py

Lines changed: 82 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from pydantic import ValidationError
2323

2424

25-
from appdaemon.dependency import DependencyResolutionFail, get_full_module_name
25+
from appdaemon.dependency import DependencyResolutionFail, find_all_dependents, get_full_module_name
2626
from appdaemon.dependency_manager import DependencyManager
2727
from appdaemon.models.config import AllAppConfig, AppConfig, GlobalModule
2828
from appdaemon.models.config.app import SequenceConfig
@@ -338,7 +338,7 @@ async def initialize_app(self, app_name: str):
338338
else:
339339
await utils.run_in_executor(self, init_func)
340340

341-
async def terminate_app(self, app_name: str, delete: bool = True) -> bool:
341+
async def terminate_app(self, app_name: str, *, delete: bool = True) -> bool:
342342
try:
343343
if (obj := self.objects.get(app_name)) and (terminate := getattr(obj.object, "terminate", None)):
344344
self.logger.info("Calling terminate() for '%s'", app_name)
@@ -367,11 +367,9 @@ async def terminate_app(self, app_name: str, delete: bool = True) -> bool:
367367

368368
finally:
369369
self.logger.debug("Cleaning up app '%s'", app_name)
370-
if obj := self.objects.get(app_name):
371-
if delete:
372-
del self.objects[app_name]
373-
else:
374-
obj.running = False
370+
obj = self.objects.pop(app_name, None) if delete else self.objects.get(app_name)
371+
if obj is not None:
372+
obj.running = False
375373

376374
await self.increase_inactive_apps(app_name)
377375

@@ -434,13 +432,8 @@ async def start_app(self, app_name: str):
434432
case AppConfig():
435433
# There is a valid app configuration for this dependency
436434
match self.objects.get(dep_name):
437-
case ManagedObject(type="app") as obj:
438-
# There is an app being managed that matches the dependency
439-
if not obj.running:
440-
# But it's not running, so raise an exception
441-
raise ade.DependencyNotRunning(*exc_args)
442-
case _:
443-
# TODO: make this a different exception
435+
case ManagedObject(type="app", running=False):
436+
# There is an app being managed that matches the dependency and isn't running
444437
raise ade.DependencyNotRunning(*exc_args)
445438
case GlobalModule() as dep_cfg:
446439
# The dependency is a legacy global module
@@ -472,7 +465,7 @@ async def start_app(self, app_name: str):
472465
}
473466
await self.AD.events.process_event("admin", event_data)
474467

475-
async def stop_app(self, app_name: str, delete: bool = False) -> bool:
468+
async def stop_app(self, app_name: str, *, delete: bool = False) -> bool:
476469
"""Stops the app
477470
478471
Returns:
@@ -481,7 +474,7 @@ async def stop_app(self, app_name: str, delete: bool = False) -> bool:
481474
try:
482475
if isinstance(self.app_config[app_name], AppConfig):
483476
self.logger.debug("Stopping app '%s'", app_name)
484-
await self.terminate_app(app_name, delete)
477+
await self.terminate_app(app_name, delete=delete)
485478
except Exception:
486479
error_logger = logging.getLogger(f"Error.{app_name}")
487480
error_logger.warning("-" * 60)
@@ -1131,70 +1124,102 @@ async def _start_plugin_apps(self, plugin_ns: str | None, update_actions: Update
11311124
update_actions.apps.init |= deps
11321125

11331126
async def _stop_apps(self, update_actions: UpdateActions):
1134-
"""Terminate apps. Returns the set of app names that failed to properly terminate.
1127+
"""Terminate the apps from the update actions, including any dependent ones.
11351128
11361129
Part of self.check_app_updates sequence
11371130
"""
11381131
stop_order = update_actions.apps.term_sort(self.dependency_manager)
1139-
# stop_order = update_actions.apps.term_sort(self.app_config.depedency_graph())
1132+
indirect_stops = set(stop_order) - update_actions.apps.term_set
11401133
if stop_order:
1141-
self.logger.info("Stopping apps: %s", update_actions.apps.term_set)
1142-
self.logger.debug("App stop order: %s", stop_order)
1134+
self.logger.info("Stopping apps: %s", stop_order)
1135+
if indirect_stops:
1136+
self.logger.debug("Dependent apps: %s", indirect_stops)
11431137

11441138
failed_to_stop = set() # stores apps that had a problem terminating
11451139
for app_name in stop_order:
1146-
if not await self.stop_app(app_name):
1147-
failed_to_stop.add(app_name)
1148-
else:
1140+
successfully_stopped = await self.stop_app(app_name)
1141+
if successfully_stopped:
11491142
self.logger.info("Stopped app '%s'", app_name)
1143+
if app_name in indirect_stops:
1144+
update_actions.apps.init.add(app_name)
1145+
else:
1146+
failed_to_stop.add(app_name)
11501147

11511148
if failed_to_stop:
11521149
self.logger.debug("Removing %s apps because they failed to stop cleanly", len(failed_to_stop))
11531150
update_actions.apps.init -= failed_to_stop
11541151
update_actions.apps.reload -= failed_to_stop
11551152

1153+
def _filter_running_apps(self, *trackers: Iterable[str]) -> Iterable[Iterable[str]]:
1154+
"""App names that get added to the start order indirectly may already be running."""
1155+
for app_name in copy(trackers[0]):
1156+
match self.objects.get(app_name):
1157+
case ManagedObject(running=True):
1158+
self.logger.debug("Dependent app '%s' is already running", app_name)
1159+
for tracker in trackers:
1160+
match tracker:
1161+
case set():
1162+
tracker.discard(app_name)
1163+
case list():
1164+
tracker.remove(app_name)
1165+
case dict():
1166+
tracker.pop(app_name, None)
1167+
return trackers
1168+
11561169
async def _create_and_start_apps(self, update_actions: UpdateActions) -> None:
11571170
"""Creates and starts apps that are in the init set of the update actions."""
11581171
if failed := update_actions.apps.failed:
11591172
self.logger.warning("Failed to start apps: %s", failed)
11601173

11611174
start_order = update_actions.apps.start_sort(self.dependency_manager, self.logger)
1162-
if start_order:
1163-
self.logger.info("Starting apps: %s", update_actions.apps.init_set)
1164-
self.logger.debug("App start order: %s", start_order)
1165-
1166-
for app_name in start_order:
1167-
match self.app_config.root.get(app_name):
1168-
case AppConfig() as cfg if not cfg.disable:
1169-
if await self.create_app_object(app_name) is None:
1170-
update_actions.apps.failed.add(app_name)
1171-
case GlobalModule():
1172-
# Global modules are not started, they are just imported
1173-
self.logger.debug(f"Skipping global module '{app_name}'")
1174-
case None:
1175-
self.logger.warning(f"App '{app_name}' not found in app config")
1175+
indirect_starts = set(start_order) - update_actions.apps.init_set
1176+
self._filter_running_apps(indirect_starts, start_order)
11761177

1177-
# Need to have already created the ManagedObjects for the threads to get assigned
1178-
await self.AD.threading.calculate_pin_threads()
1178+
if not start_order:
1179+
return
11791180

1180-
# Need to recalculate start order to account for any failed object creations
1181-
start_order = update_actions.apps.start_sort(self.dependency_manager, self.logger)
1182-
if start_order:
1183-
for app_name in start_order:
1184-
match self.app_config.root.get(app_name):
1185-
case GlobalModule() as global_module:
1186-
assert global_module.module_name in sys.modules, f"{global_module.module_name} not in sys.modules"
1187-
case AppConfig() as cfg:
1188-
@ade.wrap_async(self.error, self.AD.app_dir, f"Failed to start '{app_name}'")
1189-
async def safe_start(self: "AppManagement"):
1190-
try:
1191-
await self.start_app(app_name)
1192-
except Exception as exc:
1193-
update_actions.apps.failed.add(app_name)
1194-
raise ade.AppStartFailure(app_name) from exc
1195-
1196-
if await self.get_state(app_name) != "compile_error":
1197-
await safe_start(self)
1181+
self.logger.info("Starting apps: %s", start_order)
1182+
if indirect_starts:
1183+
self.logger.debug("Dependents: %s", indirect_starts)
1184+
1185+
for app_name in start_order.copy():
1186+
match self.app_config.root.get(app_name):
1187+
case AppConfig(disable=False):
1188+
if await self.create_app_object(app_name) is None:
1189+
update_actions.apps.failed.add(app_name)
1190+
start_order.remove(app_name)
1191+
case GlobalModule():
1192+
# Global modules are not started, they are just imported
1193+
self.logger.debug(f"Skipping global module '{app_name}'")
1194+
case None:
1195+
self.logger.warning(f"App '{app_name}' not found in app config")
1196+
1197+
# Need to have already created the ManagedObjects for the threads to get assigned
1198+
await self.AD.threading.calculate_pin_threads()
1199+
1200+
# Account for failures and apps that depend on them
1201+
failed = update_actions.apps.failed
1202+
failed_deps = find_all_dependents(update_actions.apps.failed, self.dependency_manager.app_deps.rev_graph)
1203+
prevented_apps = [a for a in start_order if a in failed_deps and a not in failed]
1204+
if prevented_apps:
1205+
self.logger.warning("Failures of other apps prevented these apps from starting: %s", prevented_apps)
1206+
start_order = [a for a in start_order if a not in (failed | failed_deps)]
1207+
1208+
for app_name in start_order:
1209+
match self.app_config.root.get(app_name):
1210+
case GlobalModule(module_name=str(mod_name)):
1211+
assert mod_name in sys.modules, f"{mod_name} not in sys.modules"
1212+
case AppConfig():
1213+
@ade.wrap_async(self.error, self.AD.app_dir, f"Failed to start '{app_name}'")
1214+
async def safe_start(self: "AppManagement"):
1215+
try:
1216+
await self.start_app(app_name)
1217+
except Exception as exc:
1218+
update_actions.apps.failed.add(app_name)
1219+
raise ade.AppStartFailure(app_name) from exc
1220+
1221+
if await self.get_state(app_name) != "compile_error":
1222+
await safe_start(self)
11981223

11991224
async def _import_modules(self, update_actions: UpdateActions) -> set[str]:
12001225
"""Calls ``self.import_module`` for each module in the list

appdaemon/models/internal/app_management.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ def start_sort(self, dm: DependencyManager, logger: Logger | None = None) -> lis
107107
def term_set(self) -> set[str]:
108108
return self.reload | self.term
109109

110-
def term_sort(self, dm: DependencyManager):
110+
def term_sort(self, dm: DependencyManager) -> list[str]:
111111
"""Finds all the apps that need to be terminated.
112112
113113
Uses a dependency graph to sort the internal ``reload`` and ``term`` sets together

0 commit comments

Comments
 (0)