fix(archiver): replace stale jobs so retry can escape a wedged download - #84
Open
dylanjeffers wants to merge 1 commit into
Open
fix(archiver): replace stale jobs so retry can escape a wedged download#84dylanjeffers wants to merge 1 commit into
dylanjeffers wants to merge 1 commit into
Conversation
Job ids are deterministic (`base64url("{userId}-{trackId}")`), and
getOrCreateStemsArchiveJob only replaced a job whose state was `failed`.
Any job parked in a non-terminal state was therefore handed straight back
to every subsequent retry from that user for that track.
That is what made the 2026-07-28 outage unrecoverable from the client. The
worker lost its Redis lock when archiver-redis was recreated, kept all five
concurrency slots, and jobs sat in `waiting` — never `failed`. The "Try
again" link in DownloadTrackArchiveModal is the only recovery affordance
the UI offers, and it was a no-op: POST returned the same dead job id, the
client re-polled it, and timed out again 15 minutes later.
Cancel-then-retry could not work around it either. cancelStemsArchiveJob
removes a job from the queue only when it is completed; otherwise it aborts
via an in-memory AbortController, and a job stranded in `waiting` has none,
so it falls through to 'Stems archive job not found' and removes nothing.
Now a job that has been non-terminal past `staleJobSeconds` (default 15m,
`ARCHIVER_STALE_JOB_SECONDS`) is removed and recreated. Completed jobs are
still returned as-is so a ready archive is downloaded rather than rebuilt,
and jobs that are still progressing are still rejoined, which is what
dedupes double-clicks and multiple tabs.
The threshold is kept in step with STEMS_ARCHIVE_POLL_TIMEOUT_MS in the
client's useDownloadTrackStems: past that point the client has already
given up, so the old job's output would never be collected anyway.
Staleness is measured from `processedOn ?? timestamp`, so a job that never
started — the case that actually broke — is covered.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Closes #81.
Problem
Job ids are deterministic —
base64url("{userId}-{trackId}")— andgetOrCreateStemsArchiveJobonly replaced a job whose state wasfailed:So any job parked in a non-terminal state is handed back to every subsequent retry from that user for that track.
This is what made the 2026-07-28 outage (#77) unrecoverable from the client. When
archiver-rediswas recreated the worker lost its locks, kept all five concurrency slots, and jobs sat inwaiting— neverfailed. The "Try again" link inDownloadTrackArchiveModalis the only recovery affordance the UI offers, and it was a no-op: POST returned the same dead job id, the client re-polled it, and timed out again 15 minutes later. 24 requests across 15 tracks were stuck this way for ~7 hours.Cancel-then-retry from the client doesn't work around it either —
cancelStemsArchiveJobremoves a job from the queue only when it's completed; otherwise it aborts via an in-memoryAbortController, and a job stranded inwaitinghas none, so it falls through to'Stems archive job not found'and removes nothing.Change
A job that has been non-terminal past
staleJobSecondsis now removed and recreated. Behavior by state:completedfailedStaleness is measured from
processedOn ?? timestamp, so a job that never started — the case that actually broke — is covered, not just one that stalled mid-run.New config
staleJobSeconds(ARCHIVER_STALE_JOB_SECONDS, default 900s). Chosen to matchSTEMS_ARCHIVE_POLL_TIMEOUT_MSin the client'suseDownloadTrackStems: past that point the client has already given up on the job, so its output would never be collected anyway. The coupling is called out in a comment on both sides.Notes for review
failedpath already did.processJobreuses/tmp/audius-archiver/{jobId}if it exists and stem filenames are deterministic, so a replacement overwrites rather than colliding. Minting a fresh id would leave the old temp dir behind untilcleanupOrphanedFilesswept it, and would break dedupe.cancelStemsArchiveJobremove queued/active jobs. It's a real gap (noted in archiver: retry can never escape a stuck job — getOrCreate only replaces failed jobs #81) but a separable change; this PR stays on the retry path.Testing
New
src/jobs/createStemsArchive.test.ts, 11 cases:isStaleJobboundaries (processedOnpreferred overtimestamp, fallback for never-started jobs, missing timestamps treated as fresh) and eachgetOrCreateStemsArchiveJobbranch, including the exact wedged-in-waitingcase from the outage.tsc --noEmitclean. ESLint could not be run from a git worktree —eslint-plugin-turbothrowsCannot convert undefined or null to objectoutside a turbo workspace root, identically on unmodifiedmain, so it's environmental and left to CI.🤖 Generated with Claude Code