fix(video): contain a timestamp leap that escapes the timeline rebase (#369) - #372
Conversation
…superuser404notfound#369) Three containment gaps, one field trace (a 2^33 wrap on a sequential origin), each of which independently prolonged the failure: - resolveVideoSampleDuration returned the look-behind delta unbounded, so across the wrap it handed movenc the wrap itself as a sample duration (device: 8226410192 ticks, rejected as 'Application provided duration ... is invalid') and the packet was silently lost - the write rc was dropped on the floor. The delta is now capped at the discontinuity threshold (a sample longer than a discontinuity is definitionally invalid; live rebases and the cap share one definition), and the first rejected write is logged. - SegmentCache.noteFolded discarded fold runs wider than 64 indices as 'a restart or a seek', but the widest folds are the ones most certain to make a consumer request a folded index; dropping the 312-index field run left every counter at 0 and disarmed both superuser404notfound#358 recovery arms. Wide runs now count (one Int per index, bounded by plan size) and the fold site classifies them as discontinuity-scale in the log. - the advance-path backpressure park released only on a consumer fetch of head - bufferAheadSegments, but a sequential append playlist advertises only what THIS pump's finalize reports have fed it, so a target beyond that frontier waits for oneself (field: parked at target=364 while the playlist ended at seg61, freezing it for good). The park is skipped while the target lies beyond the advertisable frontier; the disk budget stays the resource bound and normal parking resumes as the frontier catches up. Negative startup targets are exempt - they release instantly and are no deadlock. Deliberately unchanged: OutputTimestampSanitizer keeps latching. movenc latches monotonicity on its own once a wrapped packet is accepted, so a sanitizer reset would only convert garbage timestamps into rejected writes, and it would weaken the SSAI seam protection the class exists for. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… device trace The duration cap is pinned with the exact wrap values (363524400 -> 2^33, delta 8226410192), the fold-run counting with the field fold (indices 2...313, repeat-across-restart increment, stored-index exemption), and the park predicate with the field deadlock (target=364 vs frontier=61) plus the negative-startup-target exemption. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1ca9656
into
superuser404notfound:main
|
Merged, thanks for the trace-anchored writeup, the three containments sit where the field log points and the pinned wrap values made the duration cap reviewable in one read. Full suite green on the merge result (1838 Swift Testing tests in 268 suites, 0 failures); the CHANGELOG conflict against the #368 follow-up was resolved by keeping both Unreleased entries. Two follow-ups pushed on top (71b27b1, merged in 833e152), both in the same spirit as the PR: The cap now also covers the duration the container DECLARES. The skipped park now hands its wedge detection to the disk park. This one is worth a note on the reasoning in section 3. The advance park is what catches a consumer that stopped advancing, and the #207 disk park documents in so many words that it needs no wedge breaker because "the extras eviction that follows the advancing playhead releases it". Skipping the advance park moves the pump straight into that assumption with the consumer possibly frozen: it races to the retention budget (2 GiB, so ~an hour of a 5 Mbps timeshift) and then holds in an unbreakable park for good. The skip is right, so the fix is to carry the detector across rather than to park: One correction to the rationale, measured rather than argued, because it changes which path does the work:
The issue stays open for the device retest; the release will bundle this with #368 and, if it lands, #370. |
…ransport owns CI on the #372 merge (run 31749495994) failed the short-body test at `read > 0`: zero bytes reached the reader before the drop surfaced. Same class as the 2026-08-11 flake that already relaxed `>= 1 MiB` to `> 0`. The amount URLSession hands over before it reports a cut body is its own to decide, and under load it can be nothing at all, so no floor is honest. What the reader owns is the ceiling (never more than the server sent) and the error it ends on, which is what this test now asserts. The engine contract held in the failing run: `last == -5` passed there, the amount was the only issue recorded. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HX5zV3Fcf7Nzq4DYGdXvQE
Fixes #369. Companion to #368's rebase: defense in depth for a timestamp leap that escapes it (corrupt stream, a jump inside one chunk, future regressions). Today such a leap turns the session into a multi-minute zombie; with these three containments it either keeps playing sanely or fails cleanly within seconds onto the designed
onVODSourceFailedsurface.The three containments
resolveVideoSampleDurationgets a cap. The look-behind delta across a 2^33 wrap IS the wrap (device: 8226410192 ticks ≈ 91404 s), which movenc rejects — and the write rc was dropped on the floor, so the packet vanished silently. The inferred delta now falls back (existing duration, then fallback) when it exceeds the discontinuity threshold: a sample longer than a discontinuity is definitionally invalid, and reusingdiscontinuityThresholdSecondskeeps one shared definition. The first rejected muxer write is now logged.SegmentCache.noteFoldedcounts wide runs. ThemaxFoldRunLengthguard dropped runs wider than 64 indices as "a restart or a seek" — but the field case was a discontinuity-scale cut leap (312 indices), and dropping it left every fold counter at 0, which is exactly what disarmed both VOD keyframe-aligned plan: the playlist advertises segment indices the producer never muxes, and the wedge recovery rebuilds the same hole #358 recovery arms for the folds most certain to trigger them. Wide runs now count (one Int per folded index, bounded by plan size); the constant survives as a log-classification threshold, and the fold site emits a distinct discontinuity-scale line. On a sequential origin the armed escalation reachesrequestRestart→ the sequential refusal →onVODSourceFailedwithin seconds (the client's shifted-window re-request is the designed recovery); on seekable VOD the reanchor genuinely works.The advance park cannot wait on an unadvertisable index. The park releases on a consumer fetch of
head − bufferAheadSegments, but a sequential append playlist advertises only what this pump's OWN finalize reports have fed it — a release target beyond that frontier is waiting for oneself (field: parked at target=364 while the playlist ended at seg61, freezing it for good). The park is skipped while the target lies beyond the frontier (sequentialParkWouldSelfDeadlock, pure); the disk budget stays the resource bound, normal parking resumes as the frontier catches up, and negative startup targets are exempt (they release instantly and are no deadlock).Deliberately out of scope
OutputTimestampSanitizerkeeps latching after a huge jump. movenc latches monotonicity on its own once a wrapped packet is accepted, so a sanitizer re-latch would only convert garbage timestamps into rejected writes until the next muxer rotation — no better — and it would weaken the SSAI seam protection the class exists for. With #368 the sanitizer never sees the leap; without it, containment 2 fails the session cleanly.Tests
VideoSampleDurationTests: the cap pinned with the exact wrap values; existing Native DV Profile 8.1 (HEVC) shows transient video glitches on the loopback-HLS path; not reproducible on replay #92 telescoping semantics unchanged under a production-scale cap; at-cap delta still telescopes.SegmentCacheFoldRunTests(new): the 312-index field run counts, repeats across a producer restart increment (the VOD keyframe-aligned plan: the playlist advertises segment indices the producer never muxes, and the wedge recovery rebuilds the same hole #358 signal), stored indices stay exempt, narrow runs unchanged.SequentialParkFrontierTests(new): the field deadlock (target=364 vs frontier=61), at/below-frontier parks normally, negative startup target exempt.Test plan
swift test(full suite, counts above).Applies cleanly with or without the #368 PR; the combination is integration-tested locally (full suite green on the merged tree).