Skip to content

fix(sdk): don't accept a transcode as done without its cid - #14585

Open
dylanjeffers wants to merge 1 commit into
mainfrom
fix/upload-requires-transcode-cid
Open

fix(sdk): don't accept a transcode as done without its cid#14585
dylanjeffers wants to merge 1 commit into
mainfrom
fix/upload-requires-transcode-cid

Conversation

@dylanjeffers

Copy link
Copy Markdown
Contributor

The root cause behind the dead-track incident

Michael reported that one link failed when sharing to an Instagram story. The track behind it has no audio at all: track_cid is null on the indexed row, /stream 404s, and share-to-story hands that URL to ffmpeg, which fails. AudiusProject/api#1032 makes the API honest about it and repairs the rows; #14584 stops mobile offering a story for a track it cannot build a video from. This PR is why the rows exist in the first place.

pollProcessingStatus returned the moment a storage node reported status: 'done':

if (resp?.status === 'done') {
  return resp
}

It never checked that the result it is polling for is on the response. Upload rows replicate across storage nodes and getProcessingStatus talks to whichever node storageNodeSelector hands back — falling over to others on error — so a mirror can legitimately answer done from a row it has not finished catching up on, with an empty results map.

populateTrackMetadataWithUploadResponseV2 then does:

trackCid: audioResponse.results['320'],

which is undefined, and the track entity is written without a cid. Nothing errors. The upload reports success, the track page loads, the artwork renders, people favorite and repost it — and there is no cid on the row pointing at the audio, so it can never be played and never records a play.

That matches the failing track exactly: duration, bpm, musical_key, orig_file_cid and audio_upload_id were all populated from the upload response, so the response was there and carried probe and audio_analysis_results — only results['320'] was missing. The result: 0 plays against 18 favorites and 16 reposts.

The change

Require the '320' result before treating an audio poll as finished. A node that really is done will have it on the next pass three seconds later. An upload genuinely stuck in that state now times out with an error naming the cause — Upload reported done but no transcode result appeared within... — instead of silently publishing unplayable audio, which is a strictly better failure: the artist finds out at upload time rather than never.

Image templates are untouched; they have no '320' to wait for.

Testing

Storage.test.ts: a node reporting done with no results keeps polling and picks up the cid on the retry; a response that already has the cid returns on the first call with no extra poll; image templates return immediately on an empty results map. All 5 tests in the file pass.

Not reproduced against a live node — the race needs replication lag between storage nodes to trigger.

Related

🤖 Generated with Claude Code

pollProcessingStatus returned the moment a storage node reported
status: done, without checking that the transcode result it is polling
for is actually on the response. Upload rows replicate across nodes and
getProcessingStatus talks to whichever node is selected, so a mirror can
answer done from a row it has not finished catching up on.

populateTrackMetadataWithUploadResponse then reads results['320'] off
that response and gets undefined, and the track entity is written with no
trackCid. The upload reports success. The track page loads, the artwork
renders, people favorite and repost it - and there is no cid on the row
pointing at the audio, so it can never be played and never records a
single play. That is how DEZZE's only track ended up with 18 favorites,
16 reposts and 0 plays, and why share-to-story failed on it: the flow
hands the 404ing stream URL to ffmpeg.

Require the '320' result before treating an audio poll as finished. A
node that really is done will have it on the next pass three seconds
later, and an upload genuinely stuck in that state now times out with an
error naming the cause instead of silently publishing unplayable audio.
Image templates are unaffected - they have no '320' to wait for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Sep 3, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 9f3e198

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@dylanjeffers

Copy link
Copy Markdown
Contributor Author

Scope check: this fixes one of two failure modes

I sampled the 250 most recent public tracks (a 9-hour window). 3 have no track_cid — all 404 on /stream, all hours old, all with 0 plays. Checking their upload records against 5 content nodes splits them:

  • 2 of 3: transcode done on all 5 nodes, all agreeing on the same cid. This is the race this PR closes — the cid existed, the client just didn't carry it onto the track. fix(api): a track with no track_cid is not streamable, plus a repair job api#1032's repair job recovers these.
  • 1 of 3 (plus one more in the sample): transcode busy on all 5 nodes, no results at all. transcode_progress: 0 since 17:57 in one case, stalled at 91.8% since 13:08 in the other. No error, no error_count, rows untouched for hours.

The second mode is not explained by this change, and it raises a question I could not answer from the client alone: with the transcode still busy, pollProcessingStatus should never have returned, so how did a track entity get written at all? Either something publishes the track without waiting on the upload, or those uploads came through a path that doesn't use this poll.

One concrete adjacent bug while I was in here, which I have deliberately not changed in this PR to keep it reviewable:

if (template === 'audio' && resp.transcode_progress) {
  // ... no-progress stall detection lives in here
}

transcode_progress of exactly 0 is falsy, so a transcode that never starts skips the MAX_TRACK_TRANSCODE_NO_PROGRESS_TIMEOUT check entirely and polls for the full hour instead of failing after 20 minutes. That is exactly the state 9mmd0zj is in. Happy to fold the fix in here or split it out — say which you'd prefer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant