diff --git a/quest/m1/README.md b/quest/m1/README.md index 360826fa74b..00de5a4e3ac 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -200,7 +200,6 @@ per-frame CPU growth since #5926. - [Ordered reads can skip owed holes, and snapshot latest() does](/quest/m1/ordered-owed-holes.md) - moq-net's `Ordered` gains a read that skips past an owed hole to the newest buffered group, while keeping the reader's bounds - [A Rust relay never replays from group 0 to a lite-03/04 subscriber](/quest/m1/relay-inflight-replay.md) - A Rust relay or publisher serving a lite-03 or lite-04 SUBSCRIBE from a track copy that is still resolving starts at the newest group once the copy resolves - [The Python wrapper publish can be retried by hand](/quest/m1/release-py-dispatch.md) - `.github/workflows/release-py.yml` has a `workflow_dispatch` trigger, so a maintainer can retry a failed `moq-rs` publish from `release` without a new `moq-ffi-v*` tag or a path-matching push -- [A returning route serves past the cap its old copy was given](/quest/m1/returning-route-cap.md) - when a resumed reader's route returns, the reused serving copy asks with the reader's current resolution, not the capped resolution it was left with when it was replaced - [The resume model passes its long run again](/quest/m1/resume-model-7542.md) - fix #7542's regression that drops groups a long-budget reader can still get - [A resume more than one group behind never hangs](/quest/m1/ietf-resume-behind.md) - a resumed read gets every group or a drop on every wire version - [moq-quic's idle connections keep no burst-sized state](/quest/m1/quic-readable-dedupe.md) - readable events are deduplicated and a quiet connection keeps no per-peak-stream bytes diff --git a/quest/m1/returning-route-cap.md b/quest/m1/returning-route-cap.md deleted file mode 100644 index d52cf30a157..00000000000 --- a/quest/m1/returning-route-cap.md +++ /dev/null @@ -1,44 +0,0 @@ -# [S] A returning route serves past the cap its old copy was given - -## Goal - -When a resumed reader's route returns, the reused serving copy asks with the -reader's current resolution, not the capped resolution it was left with when -it was replaced. A range ending at a relative anchor, or at a time that has -already resolved, keeps delivering past the old cap on the returning route, -as it would on a fresh route. Rust first; JS only if the same reuse exists -there. - -## Plan - -Decided 2026-10-31 (user, /quest-plan of the follow-ups from the 2026-10-31 -/quest-complete run), from -[moq-dev/moq#7364](https://github.com/moq-dev/moq/pull/7364). - -- Prompt: "The capped serving copy on a returning route (pre-existing on - main): fix now, or leave it to the resume redesign?" ✅ Own S quest, m1. - Not chosen: leave it to [resume as producer replacement](/quest/m2/resume-redesign.md), - which is blocked until after the cut. -- Fix via the reader's resolution, consistent with [moq-dev/moq#7364](https://github.com/moq-dev/moq/pull/7364)'s widened-backfill - fix: there a widened backfill asks with the reader's resolution, because - the copy's own resolution ends at its earlier cap and would pin a range - ending relative to the latest group. -- The bug: returning to a route reuses its newest copy - (`rs/moq-net/src/model/resume.rs:1192` at be39188000), clears `until`, and - calls `copy.update(&self.mirrored)`. `Copy::wire` (`resume.rs:511`) then - takes the `Sub::Ready(sub) if reader.ranges == self.from` arm and re-reads - `sub.resolved_ranges()`, which is the capped wire it was last sent. So the - reused copy stays at its old cap and delivers nothing past it. Re-resolve - the reused copy against the reader's window instead - (`sub.resolve_ranges(&reader.ranges)`), as a new backfill does. -- Test: a range ending at a relative anchor (and one at a resolved time end) - flaps away and back; the returning route delivers groups past the old cap. - Add the shrunk case to the resume model's regressions if the nightly sweep - finds one. -- [moq-dev/moq#7364](https://github.com/moq-dev/moq/pull/7364) (merged) edited the same code first. - -Public API: none. Wire: none. - -## Related - -- [Resume as producer replacement](/quest/m2/resume-redesign.md) - would remove copy reuse altogether diff --git a/rs/moq-net/src/model/resume.rs b/rs/moq-net/src/model/resume.rs index 0fb11665f3a..60a69835d99 100644 --- a/rs/moq-net/src/model/resume.rs +++ b/rs/moq-net/src/model/resume.rs @@ -1238,6 +1238,8 @@ impl Reader { // group's in-flight tail. A fresh copy would replay it and retain one per flap. The // newest is the route's own copy, not a backfill of it. if let Some(index) = self.copies.iter().rposition(|copy| copy.track.same_channel(&track)) { + // The copy's own resolution ends at its old cap; the reader's does not. + let resolved = self.window().resolved(); let mut copy = self.copies.remove(index); copy.generation = generation; copy.replaced = false; @@ -1255,7 +1257,9 @@ impl Reader { self.groups.1.map_or(Bound::Unbounded, Bound::Excluded), )); } - copy.update(&self.mirrored); + copy.resolved = resolved; + let wire = self.mirrored.clamped(copy.floor, None, ©.resolved); + copy.send(wire); self.copies.push(copy); return; } @@ -3081,6 +3085,66 @@ mod test { drop(held); } + #[kio::test] + async fn a_returning_route_keeps_its_relative_end() { + returning_route(Anchor::Relative(991)).await; + } + + #[kio::test] + async fn a_returning_route_keeps_its_resolved_time_end() { + returning_route(Anchor::Time(ts(10))).await; + } + + /// Its old cap bounds a replaced copy's wire, not the reader's resolved end. Reusing + /// that copy must ask past the cap while keeping the reader's range and local bounds. + async fn returning_route(end: Anchor) { + let routes = Producer::new(); + let logical = logical(&routes); + let a = copy(); + // Both anchors resolve the reader's exclusive end at 11. + for sequence in 2..=11 { + write(&a, sequence); + } + write(&a, 1001); + routes.serve(a.consume()); + let subscription = Subscription::default() + .with_delay(Delay::default().with_max(HOUR)) + .with_ranges([track::Range { + start: Anchor::Absolute(2), + end: Some(end), + }]); + let mut sub = logical + .consume() + .subscribe(subscription) + .await + .unwrap() + .with_range(2..3); + let held = recv(&mut sub); + assert_eq!(held.sequence, 2); + assert_eq!(sub.next_in_range(11), None, "the reader resolved its end"); + sub.set_range(4..9); + + let b = copy(); + routes.serve(b.consume()); + write(&b, 4); + assert_eq!(drain(&mut sub, false), [4]); + assert_eq!(copies(&routes), 2, "the held group keeps A's capped copy"); + + routes.serve(a.consume()); + for sequence in [1, 12] { + write(&a, sequence); + } + let mut resumed = drain(&mut sub, false); + resumed.sort_unstable(); + assert_eq!(resumed, [5, 6, 7, 8], "end={end:?}"); + // Widening the local cursor still respects the reader's resolved end. + sub.set_range(4..); + let mut widened = drain(&mut sub, false); + widened.sort_unstable(); + assert_eq!(widened, [9, 10], "end={end:?}"); + drop(held); + } + /// A returning route resumes where a fresh copy would, and what it buffered while /// replaced below what another route already handed out comes through the backfill: /// the late group once, and nothing handed out again.