Lifecycle
“Lifecycle” refers to AppDaemon itself starting, running, and stopping.
Contexts
AppDaemon makes heavy use of context managers to ensure safe and efficient startup and shutdown. Each context guarantees certain things happen as it enters and exits. The logic for the AppDaemon lifecycle consists of the following contexts, all of which get added to an ExitStack.
Backstop logic to catch any exceptions and log them more prettily.
Creates a PID file for the duration of the context, if necessary/applicable.
Creates a new async event loop and cleans it up afterwards.
Attaches/removes signal handlers to catch termination signals and stop gracefully
Startup/shutdown text in the logs about the versions and time it took to shut down
Creates the
AppDaemonandHTTPobjects.Sets/unsets the default exception handler for the event loop to prettify any unhandled exceptions. The difference with the previous exception handler is that this one has access to the
AppDaemonobject
Some of these are entered as part of the ADMain context, and some are entered in the ADMain.run method. All of them are exited in reverse order as the ExitStack is closed at shutdown.
Startup
The top-level entrypoint is appdaemon.__main__.main(), which uses argparse to parse the launch arguments.
The ADMain class primarily provides a context manager that allows it to be used with a with statement to smoothly control AppDaemon’s lifecycle. Internally it uses an instance of ExitStack. Several methods are wrapped with the contextmanager() decorator to create context managers that get entered as part of AppDaemon starting.
493def main() -> None:
494 """Top-level entrypoint for AppDaemon
495
496 Parses the CLI arguments, configures logging, and runs the AppDaemon.
497 """
498 args = parse_arguments()
499
500 CLI_LOG_CFG = PRE_LOGGING.copy()
501
502 if args.debug is not None:
503 CLI_LOG_CFG["root"]["level"] = args.debug
504 logger.debug("Configured logging level from command line argument")
505
506 logging.config.dictConfig(CLI_LOG_CFG)
507
508 with ADMain(args) as admain:
509 admain.run()
Running
ADMain runs AppDaemon by calling the start() method on the top-level AppDaemon object, followed by the run_forever() method of the event loop.
411 def run(self) -> None:
412 """Start AppDaemon up after initial argument parsing.
413
414 This uses :py:meth:`~asyncio.loop.run_forever` on the event loop to run it indefinitely.
415 """
416 self._cleanup_stack.enter_context(self.startup_text())
417 self._cleanup_stack.enter_context(self.run_context(self.loop))
418 self.AD.start()
419 self.logger.debug("Running async event loop forever")
420 self.loop.run_forever()
421 self.logger.debug("Stopped running async event loop forever")
Shutdown
Shutdown is initiated by the process running AppDaemon receiving either a SIGINT or SIGTERM signal.
339 def handle_sig(self, signum: int):
340 """Function to handle signals.
341
342 Signals:
343 SIGUSR1 will result in internal info being dumped to the DIAG log
344 SIGUSR2 will reload apps with modified code/config (useful in production_mode)
345 SIGHUP will force a reload of all apps
346 SIGINT and SIGTEM both result in AD shutting down
347 """
348 match signum:
349 case signal.SIGUSR1:
350 self.AD.thread_async.call_async_no_wait(self.AD.sched.dump_schedule)
351 self.AD.thread_async.call_async_no_wait(self.AD.callbacks.dump_callbacks)
352 self.AD.thread_async.call_async_no_wait(self.AD.threading.dump_threads)
353 self.AD.thread_async.call_async_no_wait(self.AD.app_management.dump_objects)
354 self.AD.thread_async.call_async_no_wait(self.AD.sched.dump_sun)
355 case signal.SIGUSR2:
356 self.AD.thread_async.call_async_no_wait(self.AD.app_management.check_app_updates, mode=UpdateMode.NORMAL)
357 case signal.SIGHUP:
358 self.AD.thread_async.call_async_no_wait(self.AD.app_management.check_app_updates, mode=UpdateMode.TERMINATE)
359 case (signal.SIGINT | signal.SIGTERM) as sig:
360 self.logger.info(f"Received signal: {signal.Signals(sig).name}")
361 self.stop()
The stop() method is called, which in turn calls the stop methods of the various subsystems. It doesn’t return until all the tasks that existed at the time of the call have finished. This usually happens almost instantly, but this has a 3s timeout just to be safe.
405 def stop(self):
406 """Stop AppDaemon and stop the event loop afterwards."""
407 self.AD.stop_time = perf_counter()
408 task = self.loop.create_task(self.AD.stop())
409 task.add_done_callback(lambda _: self.loop.stop())
This will stop the event loop when all the tasks have finished, which breaks the run_forever() call in run() and causes the ExitStack to close.
The stop() method of the AppDaemon object is responsible for stopping all the subsystems in the correct order. It first sets a global stop event that all the subsystems check at the top of their respective loops. It then calls the stop methods of each subsystem in turn, waiting for each to finish before proceeding to the next one. Finally, it cancels any remaining tasks and waits for them to finish, with a timeout of 3 seconds.
Stop Event
AppDaemon shutdown is globally indicated by the stop Event getting set in the top-level AppDaemon object. All the subsystems check the status of this event using the is_set() method at the top of their respective loops, and exit if it is set. The general pattern is like this:
import asyncio
import contextlib
async def loop(self):
while not self.AD.stop_event.is_set():
... # Do stuff
with contextlib.suppress(asyncio.TimeoutError):
await asyncio.wait_for(self.AD.stop_event.wait(), timeout=1)
Rather than using sleep() to wait between iterations, they use wait_for() to wait for the stop event with a timeout. The timeout is suppressed with suppress() so that it doesn’t raise an exception. Whenever the event is set, wait() returns immediately, which causes wait_for() to return immediately rather than waiting for the timeout.
Reference
- class appdaemon.__main__.ADMain(args: Namespace)
Bases:
objectMain application class for AppDaemon, which contains the parsed CLI arguments, top-level config model, and the async event loop.
When this class is instantiated, it creates a
DependencyManagerfrom the app directory. This causes- add_cleanup(cleanup_func, *args, **kwargs)
Add a cleanup function to be called on exit.
- handle_sig(signum: int)
Function to handle signals.
- Signals:
SIGUSR1 will result in internal info being dumped to the DIAG log SIGUSR2 will reload apps with modified code/config (useful in production_mode) SIGHUP will force a reload of all apps SIGINT and SIGTEM both result in AD shutting down
- loop: AbstractEventLoop
- loop_context() Generator[AbstractEventLoop]
Context manager that creates a new async event loop and cleans it up afterwards.
Includes the logic to install uvloop if it’s enabled.
- model: MainConfig
Pydantic model of the top-level object for the appdaemon.yaml file.
- run() None
Start AppDaemon up after initial argument parsing.
This uses
run_forever()on the event loop to run it indefinitely.
- run_context(loop: AbstractEventLoop)
Context manager for the main run logic with exception handling.
- signal_handlers(loop: AbstractEventLoop)
Context manager for signal handler registration and cleanup.
- startup_text()
- stop()
Stop AppDaemon and stop the event loop afterwards.