fix(spurd): redeliver completion reports across a controller outage - #521
Open
yansun1996 wants to merge 1 commit into
Open
fix(spurd): redeliver completion reports across a controller outage#521yansun1996 wants to merge 1 commit into
yansun1996 wants to merge 1 commit into
Conversation
The agent reported job completion with only 3 retries over ~3s, then dropped the report permanently. A controller unreachable longer than that window (Raft leader failover, restart, or a network blip) lost the completion, leaving the job stuck in COMPLETING until the complete_wait_secs force-finish timer (default 300s) — during which the node could be handed a new job it would reject. Retry delivery with capped exponential backoff (1s doubling to 30s) until the controller acks or returns a permanent error, and spawn the delivery so a slow controller no longer stalls the monitor loop's detection of other completions. Controller-side handling is idempotent (duplicate/late reports for a terminal or superseded run are acked), so unbounded retry is safe. A NotFound is now retryable to cover the brief cold-start window before a restarted controller replays the job's log entry.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #521 +/- ##
==========================================
+ Coverage 73.72% 73.84% +0.13%
==========================================
Files 158 158
Lines 55327 55417 +90
==========================================
+ Hits 40785 40921 +136
+ Misses 14542 14496 -46 🚀 New features to boost your workflow:
|
Contributor
There was a problem hiding this comment.
Pull request overview
This PR hardens spurd’s job-completion reporting so transient controller outages (restart/failover/network blip) don’t permanently drop a terminal ReportJobStatus, preventing jobs from getting stranded in COMPLETING until complete_wait_secs fires.
Changes:
- Replace the fixed 3-attempt completion report with an ack-or-permanent-error retry loop using capped exponential backoff, and treat
NotFoundas retryable. - Decouple completion delivery from the monitor loop by spawning delivery tasks so controller unavailability doesn’t stall completion detection/cleanup.
- Add coverage: unit tests for retry/backoff/classification and a new native-host e2e that restarts the controller mid-run and asserts prompt completion.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| crates/spurd/src/agent_server.rs | Spawned completion delivery plus a capped-backoff retry loop and expanded retryability (incl. NotFound) with unit tests. |
| tests/native_host/e2e/test_completion_recovery.py | New e2e test validating completion report redelivery across a controller restart. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+42
to
+46
| # Stop the controller while the job is still running so the completion is | ||
| # generated during the outage and must be redelivered after restart. | ||
| cluster.stop_controller() | ||
| time.sleep(8) | ||
| cluster.restart_controller() |
yansun1996
marked this pull request as ready for review
July 29, 2026 22:12
yansun1996
requested review from
powderluv,
sajmera-pensando,
sgopinath1 and
shiv-tyagi
as code owners
July 29, 2026 22:12
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this fixes
Closes #501.
When a job finishes, the node agent (
spurd) reports completion to the controller via aReportJobStatuscall. That report was best-effort: it retried only 3 times over ~3 seconds, then dropped permanently. If the controller was unreachable longer than that window — a routine Raft leader failover, aspurctldrestart, or a brief network blip — the completion was lost forever. The job then hung inCOMPLETINGuntil thecomplete_wait_secsforce-finish timer fired (default 300s), during which the node could be handed a new job it would reject as "resource unavailable" (the reported 2–5 minute node hold).Approach
Completion delivery is now durable against a controller outage:
AlreadyTerminal/StaleReport), so unbounded retry is safe.NotFoundis retryable. A just-restarted controller may not have replayed a job's log entry yet when the report arrives; since jobs are never purged from the controller's map, retrying resolves once replay finishes rather than dropping the report.No proto change — the wire message and RPC contract are unchanged, so old/new agents and controllers interoperate without version coordination.
Slurm parity
For reference, Slurm delivers a completion (
REQUEST_COMPLETE_BATCH_SCRIPTfor batch jobs,REQUEST_STEP_COMPLETEfor steps) fromslurmstepdwith an unbounded in-process retry loop at a fixed 15s interval, and its controller stays robust through a second, independent layer: node-registration reconcile. On each node re-registration (~SlurmdTimeout/3, ~100s),slurmctldcompares the jobs the node reports running against what it believes is running and actively finalizes any it no longer sees (validate_node_specs/_purge_missing_jobs→job_complete()). Notably, Slurm's retry queue is in-memory on theslurmstepdstack, not on disk — and Slurm has no time-based force-finish timer (CompleteWaitonly gates scheduling new work, it does not finalize a stuck job).This PR brings the delivery path to parity (and slightly beyond — backoff vs. Slurm's flat 15s) for the common transient-outage case. The remaining parity gap is the two deferred items below.
Known limitations (follow-up)
spurditself crashes or is upgraded mid-retry, the pending completion is lost and the job falls back to the force-finish timer. (Slurm is no stronger here — its queue is also in-memory — so a persistent on-disk agent queue would actually exceed Slurm, not just match it.)running_jobsfield exists onHeartbeatRequestbut the agent sends it empty and the controller ignores it. Wiring that up (node reports what it is actually running; controller finalizes jobs the node no longer runs) is the higher-value follow-up — it recovers both the controller-outage case and the agent-restart case [1] leaves open, and matches how Slurm stays robust without durable agent state.Testing
NotFoundnow retryable), and that the built request carries the drain fields.test_completion_recovery.py): submit a job, stop the controller while it is running so the completion is generated during the outage, restart the controller, and assert the job reachesCDwell undercomplete_wait_secs— proving the redelivered report finalized it, not the force-finish timer.