Repository navigation
fix(moq-mux): keep the TS export hold across a rewind - #4618
Conversation
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
|
Verdict: MERGE (reviewed Fixes #4613: under loss, a source skip is a rewind, and renewing the interleave hold on every rewind stacks BlockingNone. Non-blocking
This is an automated review, not the maintainer's decision |
kixelated
left a comment
There was a problem hiding this comment.
Automated review by review (OpenAI)
Reviewed #4618 at 617f20a9283acd6fe30de00612f30d25771740e9.
One lifecycle regression noted inline. Directionally, preserving the existing budget is the smaller root-cause fix: it avoids another timeout knob and retains PCR/PSI discontinuity handling. Keep that behavior for in-broadcast rewinds, but give replacement subscriptions their own initial budget. No public API or wire-format shape changes.
Validation: static review of both changed files, consumer skip semantics, and the resume path. I did not run Rust tests or reproduce the netem measurements. Check and Platform CI were queued when checked.
(Written by OpenAI)
| // The stall carries across with its budget, and so does a held frame's | ||
| // `arrived` ([`Self::pick_next_track`]). |
There was a problem hiding this comment.
[P2] Reset the stall when resuming a replacement broadcast
rewind() is also called by resume() (lines 1268–1287), which discards every pending frame and replaces the subscriptions. If the old catalog or a track errors while stall is expired, poll_next() returns through ? without clearing it. After resume(), resubscribe() marks the replacement tracks unfinished, but their first leading frame now inherits that expired deadline in pick_next_track() and is muxed immediately while its peer is still within the new max_age window. Thus a recovered broadcast can lose media-time interleaving solely because the previous broadcast stalled. Clear stall in resume() while keeping it across ordinary source discontinuities, and cover a two-track drop/resume with staggered first-frame arrivals; the existing resume tests use a single track and cannot exercise this.
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (2)
Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 2 remain after this review. Walkthrough
Priority: ➖ Normal Severity of issue fixed: Medium Merge Risk: ⚪ Minimal · up to Rewinds retain the remaining interleave wait, and replacement broadcasts start with fresh timing. No actionable merge-blocking risk was identified; merge after normal checks pass. Security Architecture ReviewSecurity architecture risk: 🔵 Low · up to The change is confined to stream timing and replacement-broadcast cleanup. No introduced security issue was identified in the inspected transitions, but production integration and mixed-track frame provenance are not fully demonstrated. Retained concerns Security review detailsSecurity Blast Radius
Trust Boundaries and Controls
Resilience and Maintainability Implications
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches✨ Simplify code
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
resume() rewinds too, so with the stall carried across rewinds a replacement broadcast inherited the ended one's expired hold and went around a track still within its own budget. Clear the stall and hold in resume(); a rewind within one broadcast still carries them. Also pin a rewind part way through a hold: it neither ends nor renews it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kixelated
left a comment
There was a problem hiding this comment.
Automated review by review (OpenAI)
Reviewed #4618 at 9b96a061fe357ca8782d0ac4078633478531b07b, the one-commit delta from 617f20a9283acd6fe30de00612f30d25771740e9.
No new actionable bugs found. The previous P2 is addressed: resume() clears both stall and hold at rs/moq-mux/src/container/ts/export.rs:1289–1291, while ordinary rewinds retain their original budget. The paused-time tests at export_test.rs:5655–5710 exercise a two-track drop/resume with staggered arrivals; export_test.rs:3974–3998 checks that a partial hold neither ends early nor restarts across a rewind.
Direction looks sound: the reset belongs at broadcast replacement, preserving the root-cause fix for repeated in-broadcast skips without another timeout knob. No public API or wire-format shape changes.
Verification: static review of the delta and surrounding resume, resubscribe, fill and interleave paths. I did not run Rust tests or reproduce the netem measurements. Check and Platform CI were still in progress when checked.
(Written by OpenAI)
|
Took this over and pushed
The remaining follow-up (a rewind clears every (Written by Claude Opus 5.5) |
|
Merging.
(Written by Claude Opus 5.5) |
Problem
Since #4001,
moq export tsdelivers a fraction of a live feed under random packet loss (#4613). The hold isn't the problem on its own.rewind()renews it.Under loss a source now and then skips a group, and skipping over a hole bumps its
discontinuity(). The exporter takes that as a new generation and callsrewind(), which resetsstall, refreshes the held frame'sarrivedand clears every track'stimeline. The sparse data PIDs, and video while it waits on a retransmit, can't show after that, so the next frame is held for the fullmax_ageagain. That's the same budget the sources skip on. After the hold every source sits a budget behind the newest content, so its next stall skips at once, which is another rewind and another full hold.With logging on
pick_next_trackand the consumer's skip paths, a 120 s run at 10% loss showed 56 holds. 2 ended with the lagging track catching up, and a rewind restarted the rest at the full 2 s. The sources fell behind the newest content steadily, to 25 s after about 35 s and 105 s by the end.Approach
rewind()no longer resetsstallor refreshes a held frame'sarrived. A stall still ends only once every track has shown where its next frame lands. A rewind neither ends nor renews it.rewind()still clears eachtimeline. The change is to a rewind taken while a track is already being muxed around: output keeps going around it until it catches up, as it already does within a generation.resume()clears the stall and its timer before it rewinds: a replacement broadcast gets its own budget.arrived, which is its pull time, even when no stall is running. So a leftover pending frame can start a stall that is already partly spent, as it can within a generation.stallandpick_next_trackdocs say so.Testing
rewind_restarts_the_stallandrewind_restarts_a_held_frameasserted the renewal. They becomerewind_keeps_an_expired_stallandrewind_keeps_a_held_frame_waiting_time.repeated_rewinds_hold_onceruns eight rewinds behind a quiet track and expects each generation out without a fresh budget. All three fail on main.rewind_keeps_a_partial_stallpins a rewind part way through a hold, andresume_does_not_inherit_an_expired_stallcovers a two-track drop and resume (it fails without theresume()change).cargo test -p moq-mux: 929 pass.just checkpasses on the host toolchain (MOQ_ALLOW_HOST=1).moq export tsloses most of the feed under random packet loss since #4001 #4613: two network namespaces on one host, 10%netemloss plus 25 ms each way on the relay → subscriber leg, a real ~10 Mb/s broadcast TS paced withtsp -P regulate,--max-age 2s, 120 s over noq. Every role runs one build, main plus the logging, with an env override on the hold budget for the comparison arms:With the PR, the first 15–20 s run below rate while the early holds play out. After that, delivery holds at the source rate.
Alternatives
max_age. At 200 ms it still loses about 15%, because every rewind renews it.discontinuity()doesn't say which kind of playhead event it counted, and the rest ofrewind()(PCR discontinuity, PSI resend) applies to all of them.Follow-ups
timeline, so a sparse track can't show until its next unit. Under sustained loss the stall then stays expired and the mux runs in arrival order. That's the pre-fix(moq-mux): order TS export by media time, not arrival #4001 behaviour rather than a loss, but it means one order across exporters holds only on a clean path.Fixes #4613.
Codex skips fork PRs — could you request a Codex review?
(Takeover changes written by Claude Opus 5.5)