fix(video): recover from a VOD producer that dies mid-session at the tail (#169)#198
Merged
Merged
Conversation
…tail (#169) rrgomes' round-2 trace (4K DV P8.1 MKV over LAN, 5.18.4): the producer died mid-session before writing the final segment (source read error after reconnect churn; memprobes show avioFetchedMB/packetsWritten frozen across the whole wedge), and the request for seg719 parked in the forward-wait branch forever: cache miss after 30005ms (cache=46 restarted=false) x11 into -12889. Their earlier "fixed on 5.16.2" was their host-side near-end workaround firing, not the engine. Three defects lined up: 1. handlePumpFinished had NO recovery arm for a VOD readError exit that had already produced media. #126 only surfaced the nothing-ever-produced case as fatal and assumed the scrub/wedge arms covered the rest; the wedge detector died with the pump and the provider escalation (2) never fired. 2. VideoSegmentProvider's forward-wait branch judged "will this arrive?" by index distance alone (index > activeMarchFront + forwardWaitWindow). A dead producer freezes the front just below the request, so the 30 s backpressure wait re-armed indefinitely with no escalation. 3. Even an escalated restart would have been vetoed: the restart loop's producerCovers guard reads the dead producer's still-installed base. Fixes, defense in depth: - Event-driven arm: a mid-session VOD readError exit now gets a bounded revive (own gate, 2 attempts) mirroring the #99 muxerFailed arm, aimed at the pending seek target or AVPlayer's real position. The demuxer whose read just threw is marked suspect-dead, so performRestart replaces it via the #79 fresh-demuxer path instead of seeking the failed connection. - Provider-side liveness: a FINISHED pump skips the forward wait entirely and restarts immediately. A pump that never finishes (blocked in a read) escalates when a full backpressure wait elapsed with zero march-front progress for the same index; an advancing front keeps the #141/#93 patience. Both bypass the producerCovers veto (marchProvenDead). VOD only; live keeps its pump watchdogs and reopen machinery. Suite 965/965, strict-concurrency build and tvOS-Sim build clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NAPCg4tCDdSqkK6tPkNptw
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.
Context
Round-2 reopen of #169 by rrgomes with a decisive trace: the earlier "fixed on 5.16.2" verdict was their host-side near-end workaround firing, not the engine. With the workaround removed, the tail failure is unchanged on 5.18.4, and it is a different mechanism than the 5.16.2 symptoms: the producer provably stops mid-session before writing the final segment (seg719 never produced; avioFetchedMB/packetsWritten frozen across four 30 s memprobes), and the request for seg719 re-arms a 30 s backpressure wait forever (
cache miss after 30005ms (cache=46 restarted=false)x11) until AVPlayer dies with -12889.Root cause (three defects lined up)
handlePumpFinishedonly surfaced the nothing-ever-produced case as fatal (Unknown-length HTTP MP4 enters streaming mode and returns ready with zero packets; the same URL plays in VLC #126) and assumed "the scrub/wedge arms cover recovery" for the rest. Both arms are unreachable here: the Presented frame drifts ~6 s ahead of currentTime/sourceTime after a rapid bidirectional seek burst (loopback-HLS native path) — frameAhead/producerShift/hostShift all report 0 #65 wedge detector runs inside the pump and died with it, and the provider's restart escalation (2) never fires.r.1 < index <= r.1 + forwardWaitWindowbranch,needsRestart = index > activeMarchFront + forwardWaitWindowanswers "will this arrive?" by index distance alone. A dead producer freezes the front just below the request, so the same 30 s wait re-arms indefinitely with no escalation. The final segment is maximally exposed: there is nothing beyond it for the march to advance into.producerCoversguard reads the dead producer's still-installed base, which covers the requested index.Fix (defense in depth, VOD only)
performRestartreplaces it via the existing Piece-B seek-deadline recovery (#65 4.2.2) re-anchors producer to a stale coalesced seek target, not AVPlayer's rendered position → permanent wedge after a rapid scrub burst #79 fresh-demuxer path instead of seeking the connection that just failed. This recovers the trace's shape within seconds of the pump exit.Live sessions are untouched: they keep their pump watchdogs (#167/#177) and reopen machinery, and the escalation is gated
!isLive.Retest signatures (for the reporter)
#169 VOD pump died mid-session (readError N); rebuilding producer on a fresh demuxer at ...followed by seg719 producing and serving.seg719: #169 forward-wait march dead (front=718 frozen across a full wait); escalating to restart.Verification
Issue169DeadProducerEscalationTests(7 tests): immediate restart on finished pump (including the producerCovers bypass), second-miss escalation on a frozen front, patience preserved while the front advances, live never escalates, pure-decision matrices forforwardWaitMarchDeadandshouldReviveVODAfterReadError.swift build -Xswiftc -strict-concurrency=completeclean; tvOS Simulator build green.Refs #169 (leaving the issue to the reporter's device retest; not auto-closing).
🤖 Generated with Claude Code
https://claude.ai/code/session_01NAPCg4tCDdSqkK6tPkNptw