From c1dda18443da1e9b2efec76e63704caee697c20a Mon Sep 17 00:00:00 2001 From: Nick Sweeting Date: Mon, 31 Aug 2026 22:26:06 -0700 Subject: [PATCH] Document daemon shutdown ownership --- archivebox/cli/archivebox_run.py | 5 +++++ archivebox/misc/checks.py | 5 +++++ archivebox/services/runner.py | 4 ++++ archivebox/workers/supervisord_util.py | 7 +++++++ 4 files changed, 21 insertions(+) diff --git a/archivebox/cli/archivebox_run.py b/archivebox/cli/archivebox_run.py index 3d2a65e4..432483d8 100644 --- a/archivebox/cli/archivebox_run.py +++ b/archivebox/cli/archivebox_run.py @@ -54,6 +54,11 @@ RUNNER_DAEMON_ENV = "ARCHIVEBOX_RUNNER_DAEMON" def _exit_daemon_runner_on_signal(sig: signal.Signals) -> None: + # A supervised `archivebox run --daemon` is intentionally a disposable + # child. If it receives SIGINT/SIGTERM directly, exit with the conventional + # signal status so supervisord treats it as an unexpected worker death and + # restarts only the runner. The parent `archivebox server` owns supervisord + # shutdown and must not be pulled down by a killed daemon worker. os._exit(128 + int(sig)) diff --git a/archivebox/misc/checks.py b/archivebox/misc/checks.py index 0570d379..3796cc60 100644 --- a/archivebox/misc/checks.py +++ b/archivebox/misc/checks.py @@ -48,6 +48,11 @@ def _exit_on_migration_interrupt(): os.write(sys.stderr.fileno(), _migration_interrupt_message().encode()) except Exception: pass + # Django's migration executor can catch or delay normal exceptions while + # unwinding transactions. Use the real process exit path after printing + # the recovery command so Ctrl+C/SIGTERM during auto-migrations does not + # leave `archivebox server` apparently hung after the user asked it to + # stop. Migrations are atomic, so this does not record partial progress. os._exit(130) try: diff --git a/archivebox/services/runner.py b/archivebox/services/runner.py index 3ac3c808..4eec268b 100644 --- a/archivebox/services/runner.py +++ b/archivebox/services/runner.py @@ -261,6 +261,10 @@ class CrawlRunner: def _request_abort_from_signal(self, _sig: signal.Signals) -> None: if os.environ.get("ARCHIVEBOX_RUNNER_DAEMON") == "1": + # The daemon runner is owned by supervisord, not by the interactive + # CLI foreground flow. A direct signal to this child should be short + # and unambiguous: exit non-zero immediately so supervisord restarts + # the runner, while the parent server and supervisord stay alive. os._exit(128 + int(_sig)) already_requested = self._signal_abort_requested self._signal_abort_requested = True diff --git a/archivebox/workers/supervisord_util.py b/archivebox/workers/supervisord_util.py index 6bca68b6..e69325a5 100644 --- a/archivebox/workers/supervisord_util.py +++ b/archivebox/workers/supervisord_util.py @@ -275,6 +275,10 @@ def RUNNER_WORKER(): "command": _shell_join(archivebox_cmd("run", "--daemon")), "autostart": "false", "autorestart": "true", + # Mark the long-lived runner child so its own SIGINT/SIGTERM path exits + # with a signal code instead of running foreground server cleanup. That + # keeps "kill just archivebox run --daemon" as a worker restart event; + # only killing the parent server or supervisord should stop the stack. "environment": 'PYTHONUNBUFFERED="1",COLUMNS="200",ARCHIVEBOX_RUNNER_DAEMON="1"', "stopasgroup": "true", "killasgroup": "true", @@ -288,6 +292,9 @@ RUNNER_ONCE_WORKER = lambda args, name="worker_runner_once": { **RUNNER_WORKER(), "name": name, "command": _shell_join(archivebox_cmd("run", "--no-stdin", *args)), + # One-shot foreground jobs are awaited by the command that launched them, + # so they keep the normal cooperative shutdown path instead of the daemon + # marker that tells supervisord to restart an independently killed worker. "environment": 'PYTHONUNBUFFERED="1",COLUMNS="200"', "autorestart": "false", "stopwaitsecs": "1",