Repository navigation
fix(net): a parked group's expiry no longer walks the backlog - #4892
Conversation
GroupExpiry::is_expired walked every group above the candidate on each poll to register on unstamped ones, so N parked serves did O(N^2) work per track change. Only the immediate successor and the groups past the live edge can change the verdict once stamped; register on those alone. #4162 raised the browser's audio subscription max age to 2s. Against a 2.5ms Opus track (Python/Go/C++ publishers via moq-ffi) that parks ~800 group serves, and the publisher's single runtime thread spun until its QUIC connection timed out, so browser subscribers got no media. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configuration
📒 Files selected for processing (1)
Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 0 remain after this review. Walkthrough
Priority: ➖ Normal Merge Risk: ⚪ Minimal · up to The parked-read change has no identified issue that needs resolution before merge; normal checks still apply. Architecture SummaryArchitecture risk: 🔵 Low · up to The change affects 1 system. Changed systems: Architecture concerns Review detailsSystems and components
Before / after behavior
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 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 |
… registering A group abort only closes the group, so if the successor aborted between selection and poll_closed, nothing watched its replacement's first timestamp and the expiry verdict could miss a wakeup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kixelated
left a comment
There was a problem hiding this comment.
Adversarial review (Codex, via peer-review)
Verdict: needs-attention (one finding), against a1c8ebc7b.
[high] Revalidate the successor after registering for abort (rs/moq-net/src/model/track.rs, GroupExpiry::is_expired)
Group 0 parked, group 1 stamped, group 2 unstamped, group 3 the stamped edge. If group 1 aborts after first_servable selects it but before poll_closed, poll_closed returns Ready without registering. is_stale then sees group 2 as the successor (unstamped, so not stale), but the registration loop reuses the cached group 1 and starts past the edge, so group 2's first timestamp is never watched. The old full scan registered on group 2 in this interleaving.
Triage: Confirmed. commit_abort only closes the group state; it never touches the track, so nothing else wakes the parked read until an unrelated track change. The failure direction is safe (a stale group is delivered rather than skipped), but it is a real lost wakeup the old walk did not have. Fixed in 0cf5d75ae: the successor is re-selected whenever poll_closed reports it already aborted, so the successor used for both the abort and first-timestamp registrations is the one is_stale judges against. This also closes the pre-existing gap where the replacement successor's own abort went unwatched. No deterministic regression test: the window sits between two lookups inside the track lock, and reproducing it needs an interleaving hook the model does not have.
Bench: re-run locally on the fix, track_parked_read is flat across cached depth (cached_8/1 126 ns, cached_512/1 135 ns, cached_512/64 8.8 µs), matching the PR description. The bench backlog is fully stamped; an unstamped suffix past the edge still scans, which is inherent (any of those groups can become the edge).
(Written by Claude Opus 5.5)
|
Grok review of head Verdict: MERGE once CI is green (macOS, Windows, Android, and WASM were still running when I looked). The narrowing is sound. Non-blocking
This is an automated review, not the maintainer's decision |
…ssor Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Re the Grok review:
(Written by Claude Opus 5.5) |
|
Grok follow-up review of head Verdict: MERGE once CI is green. All jobs passed on This push is test-only. It adds I traced the test against the code. Group 3, stamped at 20s, is the edge, so Earlier findings
No new issues. This is an automated review, not the maintainer's decision |
Merge summary
Merging pinned to (Written by Claude Opus 5.5) |
Conflicts are release backports whose originals are already on main (#4840, #4892, #4710, #5077) and release's noq 1.3.5 pins (#5170); resolved to main's side. Ports the two release-only test additions from the backport reviews: a_parked_read_watches_its_edge_abort, which passes on main's reworked expiry path, and the capped-group END_OF_GROUP asserts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem
Interop has been red on
mainsince #4162 (f539a520a, tested directly: its parent passes and it fails).python -> jsandgo -> jstime out waiting for rendered video. The C++ publisher also goes throughmoq-ffi, so it fails the same way.#4162 raised the browser's audio subscription max age to
AUTO_MAX_AGE(2s). These publishers send Opus at 2.5ms frames, one frame per group, so a 2s subscription parks about 800 group serves on the publisher. On every poll, each parked serve'sGroupExpiry::is_expiredwalked every group above it in the track's lookup map. That is O(N^2) work per track change. The singlemoq-ffiruntime thread then pinned at 100% CPU inside that walk (confirmed with gdb backtraces), the QUIC connection idle-timed out, and the browser received no frames. The "internal error" rows reported in CI are probably the same stall, since the stalled session takes down the publisher's other rows.Approach
Only two kinds of unstamped group can change a parked group's verdict once they get their first timestamp: the immediate servable successor, which bounds the reach, and any group past the live edge, which becomes the new edge. Groups in between change neither.
is_expirednow registers on the successor and on the groups past the edge, not on everything above the candidate. With a backlog, the work per poll is now O(log N) rather than O(N).Because the replacement for an aborted successor now sits in the skipped range, the successor is re-selected whenever
poll_closedfinds it already aborted (a group abort never touches the track, so nothing else would wake the read). Found by the Codex review.Impact
rs/moq-net/src/model/track.rs.Tests
New
a_parked_read_ignores_first_frames_between_its_successor_and_the_edge. It fails onmain, where the old walk registered on the middle group, and passes here. It includes a control showing that a first frame past the edge still wakes the read.New
a_parked_read_watches_the_replacement_for_an_aborted_successor: after the successor aborts, the unstamped replacement's first frame still wakes the read and ends the stale head. It fails if the successor arm is dropped from the registration loop. The abort-before-registration window itself cannot be hit deterministically without an interleaving hook.The existing
track_parked_readbench already sweeps cached groups behind a parked reader, and the slope is gone:just test interop --all, run locally on latestmain:python -> jsandgo -> jsFAIL, andjs -> rustFAIL. Every other row passes.js -> ruststill fails (re-run on0cf5d75ae, same result:cmaf: rendition audio changed its codec configuration).just checkpasses.Follow-ups
js -> rustis a separate regression from fix(moq-mux)!: fMP4 export keeps every track it declared in moov #4883 (7fcd8a701). It also fails onmain(3 of 4 runs), so it is out of scope here. The browser publishes its Opus rendition first withoutdescription, and the fMP4 export declares a dOps with a guessed pre-skip. The catalog then gains the encoder's OpusHead,describe()sees a different sample entry, and it fails withTrackChanged.(Written by Claude Opus 5.5)
🤖 Generated with Claude Code