Repository navigation
fix(net): a group withheld for its first frame holds the end #5167
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1348,12 +1348,19 @@ impl TrackState { | |
| // `sealed` also ends a locally closed receive track without a declared end. | ||
| // An abort still wins unless that end had already settled: a group below it was still open. | ||
| let reached = self.sealed | ||
| || self | ||
| .final_sequence | ||
| .is_some_and(|fin| self.max_sequence.map_or(0, |max| max.saturating_add(1)) >= fin); | ||
| || self.final_sequence.is_some_and(|fin| { | ||
| self.max_sequence.map_or(0, |max| max.saturating_add(1)) >= fin && !self.withholds(fin) | ||
| }); | ||
| reached && (self.abort.is_none() || self.settled) | ||
| } | ||
|
|
||
| /// Whether a group below `fin` is still withheld from readers until its first frame | ||
| /// lands (see [`Producer::receive_group`]): the end is not reached before it shows, or | ||
| /// readers would end without it. | ||
| fn withholds(&self, fin: u64) -> bool { | ||
| self.lookup.range(..fin).any(|(_, slot)| slot.pending) | ||
| } | ||
|
|
||
| /// Whether the declared end is reached and every cached group below it finished, | ||
| /// so nothing the end promised is still in flight. | ||
| fn is_settled(&self) -> bool { | ||
|
|
@@ -6448,6 +6455,72 @@ mod test { | |
| assert!(matches!(result, Poll::Ready(Ok(None))), "the head is stale: {result:?}"); | ||
| } | ||
|
|
||
| /// A route can declare the end, then land every group's header before its first frame. | ||
| /// Those groups are withheld until their frames land, so the end is not reached before | ||
| /// they show: a reader ending at the boundary would never see them. | ||
| #[test] | ||
| fn a_withheld_group_holds_the_end() { | ||
| let mut producer = track_producer("test", None); | ||
| let mut subscriber = producer.subscribe(Subscription::default().with_max_delay(Duration::from_secs(60))); | ||
| producer.finish_at(2).unwrap(); | ||
| let mut received: Vec<_> = (0..2) | ||
| .map(|sequence| producer.receive_group(group::Info { sequence }).unwrap()) | ||
| .collect(); | ||
| assert!( | ||
| subscriber.recv_group().now_or_never().is_none(), | ||
| "the end waits for the withheld groups" | ||
| ); | ||
|
|
||
| for group in &mut received { | ||
| group | ||
| .write_frame(Timestamp::ZERO, bytes::Bytes::from_static(b"x")) | ||
| .unwrap(); | ||
| producer.reveal_group(group); | ||
| } | ||
| assert_eq!(subscriber.assert_group().sequence, 0); | ||
| assert_eq!(subscriber.assert_group().sequence, 1); | ||
| assert!(subscriber.recv_group().now_or_never().unwrap().unwrap().is_none()); | ||
| } | ||
|
|
||
| /// A withheld group whose stream ends with no frame, cleanly or reset, shows when it | ||
| /// ends, which wakes a reader parked on the end. | ||
| #[moq_net_sim::test] | ||
| async fn a_withheld_group_that_ends_empty_releases_the_end() { | ||
| for abort in [false, true] { | ||
| let mut producer = track_producer("test", None); | ||
| let mut subscriber = producer.subscribe(Subscription::default().with_max_delay(Duration::from_secs(60))); | ||
| producer.finish_at(1).unwrap(); | ||
| let group = producer.receive_group(group::Info { sequence: 0 }).unwrap(); | ||
| let handle = group.clone(); | ||
|
|
||
| let first = { | ||
| let mut next = std::pin::pin!(subscriber.recv_group()); | ||
| assert!( | ||
| futures::poll!(next.as_mut()).is_pending(), | ||
| "the end waits for the group" | ||
| ); | ||
| match abort { | ||
| true => group.abort(Error::Cancel).unwrap(), | ||
| false => group.finish().unwrap(), | ||
| } | ||
| producer.reveal_group(&handle); | ||
| moq_net_sim::timeout(Duration::from_secs(1), next) | ||
| .await | ||
| .expect("the reveal wakes the reader") | ||
|
Comment on lines
+6506
to
+6509
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P3] Assert the wake before polling the reader again
|
||
| .unwrap() | ||
| }; | ||
|
|
||
| match abort { | ||
| // A reset group shows nothing, so the reader ends. | ||
| true => assert!(first.is_none()), | ||
| false => { | ||
| assert_eq!(first.expect("the empty group").sequence, 0); | ||
| assert!(subscriber.recv_group().now_or_never().unwrap().unwrap().is_none()); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// The ordinary live case, at the default real-time budget: 2s GOPs produced one at | ||
| /// a time and read as they arrive. The budget must take the live edge without | ||
| /// shortening the group the reader is already on, so every frame of every group is | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the final boundary has been reached, each subscriber's final
recv_grouppoll callsis_complete; in the normal case where no groups remain pending, thisanymust traverse every retained group before returning false. Ending N subscribers on a track with M cached groups therefore regresses from O(N) to O(N*M), even though only the pending-group state is relevant. Track the pending count or sequences as groups are inserted, revealed, replaced, or removed instead of rescanning the cache for every subscriber.AGENTS.md reference: AGENTS.md:L36-L37
Useful? React with 馃憤聽/ 馃憥.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Keeping the scan, as the OpenAI review also recommends. It runs only once the edge has reached the declared end, which is a subscriber's last poll or two on a finished track. It covers groups still cached below the end, which the cache bounds. A pending counter would have to be kept right across insert, reveal, sequence replacement, eviction, expiry and the cache close, and a missed path would hang readers at the end. That's a worse failure than the scan's cost on this cold path.
(Written by Claude Opus 5.5)