Skip to content

Outdated runs, and deploys that don't wait for them (#397, #390) - #406

Merged
winding-lines merged 4 commits into
mainfrom
outdated-signal
Nov 12, 2026
Merged

winding-lines merged 4 commits into
mainfrom
outdated-signal

Conversation

@winding-lines

Copy link
Copy Markdown
Contributor

First slice of #397 and #390: deploys stop waiting on long runs. A run may last months; a deploy doesn't wait for it and morca never stops it. Morca tells the run it's outdated, the run ignores that by default, and its code decides when to end.

The signal, through the heartbeat (morca.handover)

  • Each run's heartbeat thread (PgBeat, which already beats orca.runs and runs the cancel/timeout watchdog) also asks, on the same connection and beat, whether the run is outdated (outdated_sql). A worker's run gets the same answer in the reply to its beat store call (HttpBeat); a UI from before this says nothing, and the run is never outdated. The answer goes into a process-wide atomic (_get_global), so morca.outdated() is a load.
  • A running run is outdated when its pipeline is registered with other code (orca.pipelines.version vs orca.runs.version, compared by commit, dirtiness and registry hash) or its scheduler instance drains (draining_since/drain_asked, while it lists the run). No migration: both are in the schema.
  • SDK: morca.outdated() -> Bool, morca.code_version(), morca.desired_code_version(), morca.outdated_why(). False/empty until the first beat and for runs without a heartbeat (memory, SQLite).
  • Nothing is killed, cancelled or queued again.

The code version is the orca.pipeline_versions row that morca register and every run already record (morca.source): the commit describe reports (-D MORCA_COMMIT), its dirtiness and the registry hash; shown as the commit's first 12 characters (+dirty), or code <hash> without a commit. Not the registry hash alone (orca.code): it covers the DAG, not step bodies, so a fix to a poll loop wouldn't change it. Not the image: a registration's image is the remote-run --image, a run's the one it ran in, so they differ for the same code. Builds without a commit are told apart by registry hash, and during an overlap by the draining condition (which also covers a redeploy of the same code with new settings).

Shown

The run page (Status card, "Version": <code> outdated (newer version X is leading)), its JSON and every running row of the pipeline JSON (code_version, desired_code_version, outdated), and morca status <pipeline> ((outdated: newer version X is leading)). There is no morca runs command; morca status <pipeline> is the run list.

Deploys don't wait

  • MORCA_DRAIN_SECONDS unset now means no drain limit (was 3600, which killed runs after an hour).
  • morca-stack apply --overlap renames the old scheduler scheduler-old-<UTC time> (not scheduler-old), so a deploy while one still drains overlaps again, each old one under its own name; none shadows another. can-overlap no longer says no for a draining old scheduler (the binary's floor check still covers every live instance, draining ones included).
  • morca-stack drain-old asks every old scheduler to drain and returns, leaving a reaper (morca-stack reap-old NAME, nohup/setsid, $STATE/reap-NAME.log) that removes the container once it exits after its last run. --wait waits as before; --timeout waits and stops.
  • deploy.sh calls drain-old without waiting; it returns once the new scheduler leads and is registered.
  • morca-stack check reports info scheduler-old-<time>: draining N run(s) (outdated), not a failure (a scheduler-next left behind still fails); status lists them too. apply still removes exited ones (a legacy scheduler-old counts).
  • The non-overlap path (quiesce) is left as it is, and documented why: it's taken only when the schedulers can't overlap, and then the scheduler is replaced by stopping its container, whose children are every local run, so not waiting kills them; for a floor-raising migration the old binary's runs can't go on over the new schema at all.

Not in this slice (follow-ups)

Tests

  • tests/outdated.sh (new, pixi run test-outdated, in test-scheduler-ui): three real instances on one Postgres. A's two runs (commit aaaa…) aren't outdated; B leads and commit bbbb… is registered: the heartbeat says so, the run that hands over returns and succeeds, the one that ignores it runs on (heartbeat fresh, A still up), shown outdated in morca status, the pipeline JSON, the run JSON and page, and succeeds once it returns with one start and one attempt, no audit rows, nothing killed; A exits only then. C joins with the same code: B's run is outdated because B drains.
  • tests/hetzner.sh: an overlapping deploy returns while the old scheduler drains (check info, status, reaper started, not stopped); a second deploy while it drains overlaps beside it with its own name; each reaper removes its container once it exits; drain-old --timeout 0 stops one; a legacy scheduler-old still draining doesn't block the next overlap; check passes beside a draining old scheduler and fails on a scheduler-next.
  • tests/overlap.sh unchanged and passing (it sets MORCA_DRAIN_SECONDS=3 for its run-out case).

Docs: docs/overlap.md (Outdated runs, Deploys that don't overlap, Not in this slice, the stack), docs/hetzner.md (overlapping schedulers, the check, the dev checklist), README ("Long runs across deploys", with the poll-loop example).

Refs #397, #390.

🤖 Generated with Claude Code

https://claude.ai/code/session_017TycszSbyw92Efu4ZeCYL6

winding-lines and others added 3 commits November 12, 2026 10:17
A run's heartbeat asks whether a newer version leads (its pipeline is
registered with other code, or its scheduler drains) and keeps the answer
for morca.outdated(), code_version() and desired_code_version(); the run
page, its JSON and morca status mark it. Nothing stops it: the drain has no
limit unless MORCA_DRAIN_SECONDS is set. With overlap, deploy.sh returns
once the new scheduler leads: old schedulers drain as
scheduler-old-<time>, each removed by a reaper once it exits, check
reports them as information, and the next deploy overlaps beside them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TycszSbyw92Efu4ZeCYL6
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TycszSbyw92Efu4ZeCYL6
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TycszSbyw92Efu4ZeCYL6
@winding-lines
winding-lines merged commit 69ee293 into main Nov 12, 2026
32 checks passed
@winding-lines
winding-lines deleted the outdated-signal branch November 12, 2026 21:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant