From 304fc1fa4551ee9ab2811d6c2d6d0034b76c0019 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Thu, 22 Oct 2026 11:20:26 -0700 Subject: [PATCH] chore(quest): plan Rust resume gaps from the failover audit Two Rust same-epoch resume gaps confirmed on main: a route copy with a different compress is not refused, and a group read from a single route is never recorded for backup retention. Co-Authored-By: Claude Opus 5.5 --- quest/m1/README.md | 2 ++ quest/m1/resume-compress.md | 31 +++++++++++++++++++++++++++ quest/m1/resume-handed-out.md | 40 +++++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+) create mode 100644 quest/m1/resume-compress.md create mode 100644 quest/m1/resume-handed-out.md diff --git a/quest/m1/README.md b/quest/m1/README.md index 441122740e..ae3779263e 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -101,6 +101,8 @@ lite-07 stream type here. - [Resumed close](/quest/m1/resume-closed.md) - a resumed JS track keeps its declared end across copies, and its `closed` and `largest()` settle on a clean end - [Replica resume](/quest/m1/replica-resume.md) - a lagging same-epoch replica resumes with every frame exactly once and in order, on lite-07 and on moq-transport - [Rust resume holds a declared end](/quest/m1/resume-rust-end.md) - a resumed Rust track keeps a declared end across copies, dedupes flapping copies, and judges retention inside the reader's window +- [Rust resume checks compression](/quest/m1/resume-compress.md) - a same-epoch route copy with a different `compress` is refused instead of corrupting framing +- [Rust resume backs single-route groups](/quest/m1/resume-handed-out.md) - a group first read from one route keeps the later copy that holds it, with a passthrough benchmark - [WebGPU on Safari](/quest/m1/webgpu-safari.md) - the WebGPU renderer is verified on Safari 26 for macOS and iOS - [Interop finish](/quest/m1/interop-finish.md) - the interop matrix proves a clean broadcast finish across implementations - [Exporters follow a rendition to a new track](/quest/m1/export-rendition-rebind.md) - the fMP4, TS, MKV and FLV exporters rebuild a rendition whose resolved `track` or `broadcast` changes under the same ID diff --git a/quest/m1/resume-compress.md b/quest/m1/resume-compress.md new file mode 100644 index 0000000000..9c8de03471 --- /dev/null +++ b/quest/m1/resume-compress.md @@ -0,0 +1,31 @@ +# [XS] Rust resume refuses a route copy with different compression + +## Goal + +A same-epoch route whose track declares a different `compress` than the +logical track's first answer is refused before any of its groups are +forwarded, as JS already does. Today a compressed route A replaced by an +uncompressed route B under the same epoch keeps A's metadata while serving B's +groups, so a downstream lite-07 reader fails on the next group with a wrong +frame size. + +## Plan + +Found by an external audit on 2026-10-22, confirmed on main: `Front::track_info` +(`rs/moq-net/src/model/front.rs`) compares timescale, max age, and priority, +not `compress`. JS's `compatible()` (`js/net/src/resume.ts`) compares all four. + +- Add `compress` to the comparison. A mismatch takes the existing refusal + path, with the same classification as the other mismatched fields. +- `refresh` stays unchecked on both sides: it is a join heuristic, not + framing. +- Regression test through mocked sessions in + `rs/moq-net/tests/integration/route_change.rs`: compressed A serves a group, + uncompressed B takes over under the same epoch, and the reader never sees + B's groups decoded under A's metadata. Matching compression keeps resuming. + +Public API: behavior only. Wire: none. + +## Related + +- [Rust resume holds a declared end](/quest/m1/resume-rust-end.md) - the other Rust copy-compatibility check diff --git a/quest/m1/resume-handed-out.md b/quest/m1/resume-handed-out.md new file mode 100644 index 0000000000..8810f407cb --- /dev/null +++ b/quest/m1/resume-handed-out.md @@ -0,0 +1,40 @@ +# [S] Rust resume keeps a backup for a group read from a single route + +## Goal + +A Rust `resume::Reader` keeps a replaced copy that holds any group it handed +out and is still reading, including a group first read while only one copy +existed. Today such a group is never recorded, so the copy that later holds it +is dropped and the read waits out the reader's `max_delay` before ending with +`Error::Old`, even though the missing frames were available. + +## Plan + +Found by an external audit on 2026-10-22, confirmed on main. `hand_out` +(`rs/moq-net/src/model/resume.rs`) pushes onto `out` only when +`copies.len() > 1`, a guard from +[#6141](https://github.com/moq-dev/moq/pull/6141). Reproduction: read the +first frame of group 0 from A; swap to B, which holds all of group 0; B's +route fails, keeping its finished group; swap to C, which lacks group 0, and +read group 1; A's partial group 0 fails. B was dropped, so group 0 stalls. + +- Decided 2026-10-22 (maintainer): record every handed-out group, dropping the + guard, as [#6136](https://github.com/moq-dev/moq/pull/6136) does in JS. The + existing sweep keeps the list at the live group count. +- Decided 2026-10-22 (maintainer): add a passthrough benchmark in + `rs/moq-net/benches` that reads groups through a resumed track with a + single copy, so the per-group record's cost shows up. +- Regression test on a mocked clock for the reproduction above. + +Lands after [#6224](https://github.com/moq-dev/moq/pull/6224), which edits the +same reader, so it does not widen a PR under review. + +Public API: behavior only. Wire: none. + +## Required + +- [Rust resume holds a declared end](/quest/m1/resume-rust-end.md) - lands first; it edits the same reader + +## Related + +- [Resumed close](/quest/m1/resume-closed.md) - carries the JS half of this fix