fix(api): a track with no track_cid is not streamable, plus a repair job - #1032
fix(api): a track with no track_cid is not streamable, plus a repair job#1032dylanjeffers wants to merge 2 commits into
Conversation
An upload whose track_cid never made it onto the track entity has audio sitting on the content node that no reader can address. The stream link was already left nil for these rows, and /stream already 404s, but is_streamable kept reporting true - so every client believed the track was healthy. The player spins on a dead URL, and mobile's share-to-story feeds that URL to ffmpeg, which fails with a generic "Sorry, something went wrong" instead of saying the track has no audio. Split the predicate in two. IsAudioAllowed keeps the old meaning - the track is not deleted and its owner is still active - and gates downloads and previews. IsStreamable now also requires a cid to stream. Downloads deliberately stay on IsAudioAllowed: they fall back to orig_file_cid, which a row missing its track_cid still has, so losing is_streamable must not cost the artist their downloads. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
track_cid is taken verbatim from the uploader's metadata at index time. The client is supposed to poll the content node until the transcode finishes and then include the resulting cid when it writes the track; when that handshake falls through, the track is indexed with a NULL track_cid. The audio is fine and sitting on the content node - there is just no cid on the row pointing at it. The track looks normal, collects favorites and reposts, and never records a single play. The content node still holds the upload record keyed by the track's audio_upload_id, so reconcile from there, the same way RepairAudioAnalysesJob recovers bpm / musical_key. Unlike that job, this one requires two distinct nodes to agree on the cid before writing it. bpm fills in a display field; track_cid decides which bytes every listener receives for the track. Upload records are replicated across mirrors, so agreement is cheap, and it means a single misbehaving or stale node cannot repoint a track's audio on its own. Tracks that cannot reach quorum, or whose nodes disagree, are logged and left for the next pass rather than repaired on one node's word. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Blast radius, measured — and it's two failure modes, not oneI sampled the 250 most recent public tracks via 3 of 250 (1.2%) have no Checking each against 5 content nodes for the upload record changes the picture:
So there are two distinct ways a track ends up dead, and this job only fixes the first:
Two things worth flagging out of that:
At ~1.2% of uploads, if that rate is representative this is on the order of several dead tracks a day, and it's been running at least since Aug 31. I could not query prod directly to confirm the historical total — SELECT count(*) FILTER (WHERE audio_upload_id IS NOT NULL) AS repairable_candidates,
count(*) AS total
FROM tracks
WHERE is_current AND NOT is_delete AND track_cid IS NULL; |
What happened
Michael reported that one specific link failed with "Sorry, something went wrong" when sharing to an Instagram story, while every other link worked.
That track has no audio.
track_cidis null on the indexed row, so the stream endpoint 404s:Mobile's share-to-story builds a video out of the track's audio: it resolves the stream URL and hands it to ffmpeg as an input. ffmpeg gets a 404 JSON body instead of an mp3, exits non-zero, and the user gets the generic toast.
Storage did its job — the mediorum upload record for this track is
status: done,transcode_progress: 1, with a valid 320kbps cid. The track entity was written 89 seconds later without that cid, and nothing backfills it server-side.The worse part is that the track is completely unplayable: 0 plays against 18 favorites and 16 reposts since Aug 31. The artist has exactly one track and it's dead, and nothing in the product tells them, because
is_streamablestill returnstrue.What this changes
is_streamablestops lying. The stream link was already left nil for cidless rows and/streamalready 404s — only the flag disagreed. Split in two:IsAudioAllowedkeeps the old meaning (not deleted, owner still active) and gates downloads and previews;IsStreamablenow also requires a cid to stream.Downloads deliberately stay on
IsAudioAllowed. A download falls back toorig_file_cid, which a row missing itstrack_cidstill has, so losingis_streamablemust not cost the artist their downloads.v1_track_download.goswitched to the new guard for that reason; the playlist m3u8 builder correctly stays onIsStreamable, since it needs a stream URL.A repair job for the rows already in this state.
jobs/repair_track_cids.gois modeled on the existingRepairAudioAnalysesJob: find current, undeleted tracks with a NULLtrack_cidand anaudio_upload_id, ask content nodes for the upload record, writeresults["320"]. Runs every 15 minutes.One deliberate departure from the job it copies: two distinct nodes must agree on the cid before it is written. bpm fills in a display field;
track_ciddecides which bytes every listener receives for the track. Upload records are replicated across mirrors, so agreement is cheap to obtain, and it means a single stale or misbehaving node cannot repoint a track's audio on its own. Tracks that can't reach quorum, or whose nodes disagree, are logged and left for the next pass. The write also re-checkstrack_cid IS NULL, so a real indexer write always wins a race.Worth knowing before merging
This widens what
is_streamable: falsemeans, and the clients already act on it —isTrackUnavailablefeeds the web and mobile track pages, so cidless tracks will render the "no longer available" screen instead of a dead player. I think that's the right call, but it's a visible change beyond the reported bug. For repairable tracks it's transient until the job runs.This limits the damage; it does not stop it happening again. The origin — why the upload client wrote the track entity 89s after transcode finished without attaching the cid — is client-side and not addressed here.
Testing
./api/...,./jobs/and./indexer/...all green. New coverage: a cidless track reportsis_streamable: falsewith a null stream, the same track is still downloadable viaorig_file_cid, quorum repairs, a single node cannot repair, disagreeing nodes leave the row alone, unreachable nodes don't block a repair the reachable ones agree on, andapplyTrackCidnever overwrites an existing cid.Related
🤖 Generated with Claude Code