fix(scheduler): abort the in-flight program cleanly on shutdown - #279
fix(scheduler): abort the in-flight program cleanly on shutdown#279phsilva wants to merge 1 commit into
Conversation
|
Heads up: this overlaps substantially with #255 in Same bug, both PRs, different mechanisms:
Unique to #255 (already merged with current master, suite green): STOPPING state with OFF gated on actual worker death + heartbeat, duplicate-worker guard, start_at ordering, stale-queue re-check, expired programs, inline tracking stop, an end-to-end stop regression rig. Unique here (and wanted!): the whole preamble-under-one-try + BusDead/ObjectNotFound taxonomy (#255's own tests just reproduced exactly that unhandled-preamble death when master's Proposed order: #280 first (CLI-only, orthogonal) → #255 (7 field-tested commits, currently mergeable) → this PR rebased on the result, where it shrinks to its unique items with two adaptations: the new exception paths use One semantic split worth stating explicitly in the merged result: STOP waits for the worker indefinitely with a heartbeat (an operator asked; OFF must be honest — on 2026-07-25 a premature OFF hid 52 min of exposures on a closed dome), while SHUTDOWN joins bounded at 10 s with daemon threads (the process must be able to exit). They're different paths and compose fine. Also happy to rename #255's Happy to do the #255-side adaptations; review of the stop semantics there welcome. |
Four independent bugs made shutdown-while-observing messy: - the machine's run loop condition skipped the SHUTDOWN branch when the state changed while it slept in BUSY, so executor.stop() never ran and the exposure was left to die with the bus. Restructure to one state snapshot per iteration with SHUTDOWN checked first. - a finishing worker could overwrite SHUTDOWN with IDLE/OFF and resurrect the machine; SHUTDOWN is now sticky. - the worker's pre-try preamble (site clock, program wait loop) ran outside any handler, so a proxy dying there killed the thread unhandled. The whole body now sits under one try, and shutdown-type exceptions get distinct semantics: BusDeadException always aborts the program and stops the schedule; ObjectNotFoundException aborts only during STOP/SHUTDOWN and is an ordinary program error otherwise, so a schedule referencing an undeployed controller keeps going as before. - Scheduler.__stop__ joined nothing; it now joins the machine and the program worker (10s each) so the abort round-trip completes before instruments stop. Handlers re-raise BusDeadException/ObjectNotFoundException instead of wrapping them as ProgramExecutionException; executor.stop() always arms must_stop and survives an abort round-trip failing; the must_stop arm/clear moved to the machine thread so an abort can never race the worker to the flag. Machine.sleep() re-checks state under the condition lock, closing a missed-wakeup race.
6d4243a to
1748f70
Compare
Second of two stacked PRs fixing the Ctrl+C shutdown cascade. Stacked on
#280 — the first commit here is that PR; review only the last commit
(
fix(scheduler): abort the in-flight program cleanly on shutdown). I'llrebase this branch once #280 lands, which will drop the shared commit.
Problem
With #280 in place the bus stays alive through shutdown, but the scheduler
still mishandled it: the running exposure was never aborted, the program was
recorded as an ordinary error, and the machine marched on to start the next
program mid-teardown. Four independent bugs:
changed while it slept in BUSY, so
executor.stop()never ran; the loop nowtakes one state snapshot per iteration with SHUTDOWN checked first
machine; SHUTDOWN is now sticky
program_begin) ranoutside any exception handler, so a dying proxy killed the thread unhandled;
the whole body now sits under one try
Scheduler.__stop__joined nothing; it now joins the machine and the programworker (10s each, warns on timeout) so the abort round-trip completes before
instruments stop
Plus supporting changes: handlers re-raise
BusDeadException/ObjectNotFoundExceptioninstead of wrapping them as program errors;executor.stop()always armsmust_stopand survives a failing abortround-trip; the arm/clear moved to the machine thread so an abort can never
race the worker to the flag;
Machine.sleep()re-checks state under thecondition lock, closing a missed-wakeup race.
Exception semantics:
BusDeadExceptionalways aborts the program and stopsthe schedule.
ObjectNotFoundExceptionaborts only during STOP/SHUTDOWN —otherwise it stays a program ERROR and the schedule continues, so a schedule
referencing an undeployed controller (e.g. sample-sched.yaml's autofocus /
autoguide actions) behaves exactly as before.
Interrupted programs keep
finished=Falseand re-run from their first actionon the next start — no DB/schema changes.
Verification
tests/chimera/controllers/test_scheduler_machine.py,including regressions for the skipped-SHUTDOWN-branch bug and the
next-program-on-dead-bus march; full suite green locally (270 passed,
9 skipped)
program marked ABORTED, instruments stopped in order, bus last, exit 0 with
zero tracebacks; SIGINT while idle → exit 0; double SIGINT → forced exit 130
Notes for reviewers (out of scope here)
sample-sched.yamlPRG01 has autofocus/pointverify/autoguide actions whosecontrollers aren't in the default config → those actions fail as program
ERRORs (pre-existing behavior, preserved)
sequential.done()logs every error with a full traceback, so even a cleanabort prints a
ProgramExecutionAbortedstack — cosmetic, could be toneddown later
Scheduler.__stop__may want to be configurablefor slow-readout cameras