Draining for a planned reboot
Cordoning a running daemon so it fires no schedules and claims no new jobs while in-flight work finishes, then polling whether it is safe to reboot.
A kernel update or hardware maintenance means rebooting the control node. exocomp serve
already drains on shutdown — it stops the scheduler, gives in-flight jobs a grace window,
then cancels whatever is left (Running the daemon). But that drain is a
side-effect of the process exiting, and the grace window cancels work it does not like.
For a planned reboot you want the opposite: cordon the running daemon ahead of time, let
in-flight work finish on its own clock, and be told when it is safe to reboot.
exocomp drain is that action. It is one operator on one node (invariant
1); there is no failover partner and none is needed — this is planned
maintenance, not a crash.
What a cordon does
A cordoned daemon:
- fires no new schedules — the scheduler is paused, not torn down. A cron occurrence
that passes while cordoned is not lost: because a suppressed fire never stamps
last_run, lifting the cordon runs the same catch-up a daemon does after downtime, so the missed occurrence fires once (coalesced), never once per missed tick. - claims no new jobs — a row that reaches the queue while cordoned stays
queued. It is claimed once the cordon lifts, or swept tointerruptedon the next boot if the reboot happens first. - lets in-flight jobs finish — a job already running is never cancelled by a cordon.
This is the key difference from shutdown's grace window. A backup leg writing its
backup_runrow, a tofu apply pushing state — all run to their own completion.
"Safe to reboot" is the conjunction the CLI computes for you: cordoned AND nothing still
in flight. A queued-but-unclaimed job does not block a reboot (it is swept to
interrupted and re-drivable); a running job does.
Driving it
drain controls a running daemon, so it goes over the operator API, the same channel
--remote uses for every other write (Driving a remote daemon). It needs
a selected remote — the control node targets its own daemon:
exocomp --remote self drain
cordoned: yes in-flight: 2 safe to reboot: not yet
Cordon and wait for the two in-flight jobs to finish, then reboot:
exocomp --remote self drain --wait
waiting: 2 job(s) in flight
waiting: 1 job(s) in flight
cordoned: yes in-flight: 0 safe to reboot: yes
--wait polls until safe or --timeout (default 10m) elapses; a timeout exits non-zero, so
a reboot script does not proceed on a daemon still finishing work. --status reports
readiness without cordoning. Lift the cordon (a reboot postponed, say) with --resume:
If the goal is to restart the daemon itself rather than reboot the host,
exocomp restart does the same drain and then
re-execs the on-disk binary in place, so no reboot and no systemd restart is
involved.
exocomp --remote self drain --resume
cordon lifted; the daemon is taking work again
Every verb honours --output json; the readiness envelope is maintenance.readiness, and a
script branches on its safe_to_reboot field:
exocomp --remote self drain --output json
{"schema_version":1,"kind":"maintenance.readiness","data":{"cordoned":true,"in_flight":0,"safe_to_reboot":true}}
A cordon is in-memory daemon state: a reboot clears it, so the daemon comes back up taking work normally. There is nothing to un-set after the reboot.
After the reboot: interrupted jobs
A reboot cuts off whatever was still running, and sweeps whatever was still queued. Both land
in interrupted — a terminal state that is never auto-retried (the sweep).
List them to see what to re-drive:
exocomp --remote self drain interrupted
ID STATE RUNBOOK KIND TRIGGER EXIT QUEUED
41 interrupted nightly backup schedule - 2026-08-22 03:00:00Z
drain interrupted reads them newest-first. It works locally too (reading the store
directly), for the common case of an operator on the freshly-rebooted box. Re-drive the ones
that still matter with exocomp jobs redrive (dry-run first, then --yes);
nothing re-runs on its own. See Recovering jobs a restart cut
off.
The mechanism, and why not a signal
SIGHUP reloads schedules and SIGTERM drains-and-stops, but a signal cannot carry the
in-flight count back, so it cannot answer "safe yet?". The cordon is therefore an
operator-scoped API action on the Decision 0009 listener, gated exactly like plan/apply: a
read-only token is refused, and a browser session may not drive it. It is deliberately not
a queue job — a cordon is control-plane state, not a run, so it has no ledger entry.