Repository navigation
feat: Resume sparkeks and ecs jobs on another agent instead of failing them as AWOL (AWOL plan, phase 3) - #162
Open
prasadlohakpure wants to merge 1 commit into
Open
prasadlohakpure wants to merge 1 commit into
prasadlohakpure wants to merge 1 commit into
Conversation
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Critical recovery defects can block ECS reattachment, orphan running tasks after failed resume, and prevent recovery of completed ECS or Spark work when remote task records or applications are gone.
4 open issues
What changed in this PR
Implements phase 3 of the rollout recovery plan: another agent can resume stale sparkeks and ecs jobs instead of failing them and stopping their remote work.
Changes:
- Adds an optional
plugin.Resumerinterface and dispatches reclaimed running jobs to it. - Releases stale resumable jobs while preserving existing handling for other jobs.
- Adds SparkApplication and ECS task reattachment, shared monitoring paths, and janitor classification tests.
| File | Description |
|---|---|
pkg/plugin/plugin.go |
Defines the optional resumption interface. |
internal/pkg/object/command/sparkeks/sparkeks.go |
Reattaches to existing SparkApplications and shares monitoring logic. |
internal/pkg/object/command/ecs/ecs.go |
Discovers existing tasks and adopts their task definition for resumed monitoring. |
internal/pkg/janitor/queries/active_jobs_release.sql |
Clears agent ownership and heartbeat for released jobs. |
internal/pkg/janitor/job.go |
Separates resumable jobs from those requiring cleanup. |
internal/pkg/janitor/job_test.go |
Tests which jobs qualify for release. |
internal/pkg/heimdall/jobs_async.go |
Preserves running status and identifies resumed claims. |
internal/pkg/heimdall/job.go |
Calls Resume and handles plugins that cannot resume. |
Review effort: Balanced
prasadlohakpure
force-pushed
the
awol/phase-3-resume
branch
2 times, most recently
from
November 6, 2026 12:41
6c57db3 to
cb1df8c
Compare
prasadlohakpure
force-pushed
the
awol/phase-3-resume
branch
from
November 6, 2026 12:43
cb1df8c to
79f1f8e
Compare
prasadlohakpure
force-pushed
the
awol/phase-3-resume
branch
from
November 6, 2026 13:36
79f1f8e to
50ad0b2
Compare
…g them as AWOL (AWOL plan, phase 3) - plugin.Resumer: optional interface; Resume reattaches to the work Execute started. - Janitor releases stale running jobs whose plugin implements Resumer (claim cleared, status stays running, no Cleanup); others are failed as AWOL as before. - A claimed job that is already running keeps that status and runJob calls Resume; a failed resume runs Cleanup. - sparkeks: Resume calls Execute, which attaches to spark-sql-job-<job_id> if it exists, otherwise submits it. - ecs: Resume finds the newest task per startedBy (running, then stopped), seeds the retry count from the tasks already started, and polls them with Execute's code. - ecs: RunTask passes a client token per task and attempt; token-based resume follows in a separate PR. - stale_job default 45 s to 3600 s.
prasadlohakpure
force-pushed
the
awol/phase-3-resume
branch
from
November 6, 2026 13:36
50ad0b2 to
4a4729f
Compare
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.

AWOL-on-rollout plan
This PR is Phase 3. Phases 1 and 2 merged in #161.
execin entrypoint; ECSstopTimeout120s in deploy repo)plugin.Resumerforsparkeksandecs)awol_timeoutfor stale heartbeats and never-claimed jobs)Problem
Since #161, a rollout waits up to
shutdown_timeoutfor async jobs to finish. Jobs that run longer, and every job on a task that crashes, still lose their heartbeat. Afterstale_jobseconds the janitor fails them withawol: job is stale, marking failedand calls the plugin'sCleanup. Forsparkeksandecsthat deletes a SparkApplication or stops ECS tasks that were usually still running fine.Both plugins name their remote work after the job ID (
spark-sql-job-<job_id>, and ECSstartedByheimdall-job-<job_id>-<n>), so another agent can find it and keep waiting on it.Changes
plugin.Resumer(pkg/plugin/plugin.go): an optional interface, likeResultStreamer.Resumereattaches to the workExecutestarted and waits for it asExecutewould.Janitor (
internal/pkg/janitor/job.go,queries/active_jobs_release.sql): a stale job with statusrunningwhose plugin implementsResumeris released instead of failed. Itsactive_jobsrow getsagent_nameandlast_heartbeatset back to null, its status staysrunning, andCleanupisn't called. Every other stale or canceling job is handled exactly as before.Claim and run (
internal/pkg/heimdall/jobs_async.go,job.go): the claim query already takes unclaimed jobs regardless of status, so no SQL change is needed. A claimed job that is alreadyrunningkeeps that status instead of going toaccepted, andrunJobcallsResumeinstead ofExecute. If its plugin no longer implementsResumer, it fails with the sameawol: job is stale, marking failederror as before. WhenResumefails for any reason, the worker calls the plugin'sCleanup, since the janitor skipped it on release and nothing else would stop the remote work.sparkeks:ResumecallsExecute. Before submitting,Executenow looks upspark-sql-job-<job_id>: if it exists, it attaches to it; otherwise it submits it. So a resumed job waits on the application that is still running instead of starting a second one.ecs:Resumefinds each task by itsstartedByand polls it with the same code asExecute(waitForTasks, split out ofExecute). It lists tasks withListTasksbystartedByand desired status, running first, then stopped, and takes the newest. The task definition of that task is reused for restarts, so no new revision is registered. The number of tasks found seeds the retry count, somax_fail_countholds across agents. A task that can't be found fails the resume.ecsclient token:RunTasknow passesheimdall-job-<job_id>-<n>-<attempt>asClientToken, so a retried call returns the task it already started. Because resume seeds the retry count from the tasks already started, a restart after resume uses a token the earlier agent hasn't used. Resume doesn't rely on the token to find tasks (tasks started before this release have none); token-based resume is a follow-up PR.stale_jobdefault 45 s to 3600 s (cmd/heimdall/heimdall.go): a job is now treated as stale only after an hour without a heartbeat. heimdall-config doesn't setstale_job, so this applies in prod. Without it, a brief database or network hiccup longer than 45 s would fail every non-resumable job and release every resumable one.No drain change is needed. A job still running at the drain deadline is cut off, its heartbeat goes stale, and the janitor releases it like a crashed one.
What happens to a job whose agent dies
sparkeksorecs, statusrunningsparkeksorecs, statusaccepted(claimed, not started)CleanupCleanup, marks it canceledLimits
stale_jobseconds after its last heartbeat (now 1 hour by default), on the janitor's next pass (up toclean_interval, 60 s). The remote work keeps running meanwhile, but a job orphaned by a rollout finishes up to an hour later than its remote work does. Non-resumable jobs on a dead agent showrunningfor up to an hour before they're failed, instead of 45 s.sparkeksre-collects driver logs from the pods.ecsrestarts its timeout on resume.sparkeksrestarts its 5-hour monitor timeout and re-uploads the samequery.sql.stale_jobat an hour, a task that stopped soon after its agent died is usually gone by the time the job is resumed, and the job fails. If some earlier failed tasks are gone but a later one isn't, the retry count comes out low and the job may restart a task more times thanmax_fail_countallows.Executesubmits a new one and the Spark job runs again. That happens when the agent died after deleting a finished application but before writing the final status (sparkeksdeletes before fetching results, as before), or when someone deleted it by hand. Jobs that write (INSERT, MERGE) would write twice.running. Phase 4 adds theawol_timeoutceiling for that.stale_jobseconds, its job is released and another agent resumes it while the first is still waiting on it. Today the same outage fails the job and deletes its remote work, so this is no worse. The first agent's final status write can still race with the second's.Testing
go build,go vetandgo test ./internal/... ./pkg/...pass.TestSplitResumable: onlyrunningjobs whose plugin implementsResumerare released.acceptedandcancelingjobs, plugins withoutResumer, and commands no longer loaded go down the old path.stale_job: 15and a throwaway plugin whoseExecutetakes 120 s and whoseResumetakes 5 s (not committed):shelljob runningsleep 120. Both reachedrunning.kill -9the agent, then started a new one.shelljob was failed withawol: job is stale, marking failed. In the same janitor pass the resumable job was released, claimed by the new agent under its own agent name, resumed, and finishedsucceededwith resultresumed.stale_job: 60, killing the agent that held the job withkill -9:succeeded. No second task.startedByand handled its exit asExecutewould.exit 1,max_fail_count: 3): killed after restart 1. The other agent attached, counted two tasks, restarted withretry=2(noConflictException), and failed the job at "failed 3 times (max: 3)".Attached to existing Spark applicationand monitored it; no second submission. (The job itself failed on a Ranger permission for the local user, unrelated to resume.)active_jobs: afterstale_jobthe row moved to the other agent with a fresh heartbeat, and was removed when the job finished.