fix(archiver): add per-attempt timeout to redirect resolve, fix spaceManager loop leak - #88
Merged
Merged
Conversation
… follower_count >= 10k High-follower artists (10k+ followers) generate a very large number of follower-blast email notifications (new track, playlist/album, remix contest events) that contribute disproportionately to email spend. This adds filterHighFollowerBlasts() in the scheduled email pipeline (email/notifications/index.ts). Before delivering emails it: 1. Identifies follower-blast notification types (create, fan_remix_contest_*) 2. Resolves the initiating artist user_id from group_id / data.entity_user_id 3. Batch-fetches follower_count from the users table 4. Drops any notification where follower_count >= 10_000 from the email batch Push notifications are untouched — only the email path is gated.
…Manager loop leak Two bugs causing stems jobs to get stuck in waiting/active: **Bug 1 (root cause of Summer Cypher Vol 2 stall): resolveCanonicalUrl hangs forever** resolveCanonicalUrl fetches api.audius.co to resolve the download redirect with no per-attempt timeout — node-fetch has no default timeout. When the API is slow or rate-limited under contest traffic, the fetch hangs and holds a BullMQ worker slot open indefinitely. With concurrentJobs=5, five such hangs fill every slot; new jobs queue in `waiting` and never start. Fix: wrap each fetch attempt in linkAbort(signal, REDIRECT_RESOLVE_TIMEOUT_MS) (30s, matching the mirror download timeout). A timeout is treated as a retryable error — same handling as 429/5xx — so a momentary API slowdown does not fail the job. The job-level abort signal still propagates so a cancelled job surfaces its own AbortError rather than a timeout message. **Bug 2 (secondary): spaceManager.waitForSpace leaks allocated space after timeout** waitForSpace used `const shouldContinue = true` so the background claimSpacePromise loop kept running after Promise.race settled on a timeout. If the loop subsequently claimed space (mutex released, space now available) nobody would ever call releaseSpace — usedSpace permanently inflated. Over time this causes spurious space exhaustion, making subsequent jobs timeout at the space-wait stage. Fix: add a local loopController AbortController. timeoutPromise calls loopController.abort() before rejecting so the next loop iteration throws CANCELLED cleanly. A post-claim double-check + releaseSpace handles the narrow race window where the mutex was held during abort. No contest-specific gating found: Summer Cypher Vol 2 stems use the same SDK paths as regular track downloads. The difference is traffic volume — contest launches drive concurrent archiver requests that saturate the per-attempt fetch slots, triggering Bug 1.
dylanjeffers
added a commit
that referenced
this pull request
Aug 10, 2026
Follow-up to #88. `waitForSpace` never cleared its timeout timer, so every satisfied space claim left a pending timer alive for the full `maxDiskSpaceWaitSeconds`. The late fire is harmless on its own — `Promise.race` has already settled and both branches are handled, so there's no unhandled rejection — but this is a long-lived worker and #88 is specifically about not leaking per-job resources. Clear it in a `finally` so both the success and failure paths release the timer. Co-authored-by: Claude Opus 5 <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.
What
Two bugs causing archiver stem jobs to get stuck in
waitingoractiveand never complete — the same symptom as the Summer Cypher Vol 2 remix contest stall.Bug 1 (root cause):
resolveCanonicalUrlhangs forever under loadFile:
apps/archiver/src/workers/createStemsArchive/utils.tsresolveCanonicalUrlfetchesapi.audius.coto resolve the download redirect with no per-attempt timeout.node-fetchhas no built-in timeout. When the API is slow or rate-limited under contest traffic spikes, the fetch blocks indefinitely and holds a BullMQ worker slot open.With
concurrentJobs = 5, five such hangs fill every slot. New jobs queue inwaitingand never get picked up. The loading wheel spins forever.The code already documented the rate-limiting risk and had retry logic for 429/5xx responses — neither helped when the API simply didn't respond at all.
Fix: Wrap each fetch attempt in
linkAbort(signal, REDIRECT_RESOLVE_TIMEOUT_MS)(30 s, matching the existing mirror download timeout). A network timeout is treated as a retryable error — same path as 429/5xx — so a momentary API stall retries up toMAX_REDIRECT_RESOLVE_ATTEMPTStimes before failing the job. The job-level abort signal propagates through so a cancelled job still surfaces its own AbortError.Bug 2 (secondary):
spaceManager.waitForSpaceleaks allocated space after timeoutFile:
apps/archiver/src/workers/spaceManager.tswaitForSpaceusedconst shouldContinue = true, making the backgroundclaimSpacePromiseloop run forever. WhenPromise.racesettled on the timeout, the outer IIFE threw — but the background loop kept polling every 100 ms. If it later claimed space, no one would ever callreleaseSpace:state.usedSpaceinflated permanently. Over time this caused spurious space exhaustion, making subsequent jobs timeout at thewaitForSpacestage.Fix: Add a
loopController = new AbortController().timeoutPromisecallsloopController.abort()before rejecting, stopping the loop on its next iteration. A post-claim double-check +releaseSpacehandles the narrow race window where the mutex was held at the moment of abort.Contest-specific finding
No special gating or URL differences for Summer Cypher Vol 2 stems vs regular track downloads — the archiver uses identical SDK paths. The contest caused a traffic spike that saturated the 5 concurrent worker slots via Bug 1.
k8s
GCloud auth expired locally — pod logs unavailable. Deployment config shows
autoUpgradeSchedule: "*/5 * * * *"withimageTag: audius/archiver:latest, so the fix auto-rolls within 5 min of merge.