From 09d85fa5a65c91b7a1bddb475cbc99b611ebc1a0 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 6 Oct 2026 10:03:00 -0700 Subject: [PATCH 1/5] quest: plan follow-ups from the 2026-10-05 PR sweep Adds quests for deadline-indexed expiry wakes (m0, the cause of the red Python/Go interop rows), SUBSCRIBE_UPDATE routing on drafts 14-16, the JS request window, relay session limits, request churn with held subscriptions, wide varint tests, the moq-cli import catalog-finish test, mTLS on tls://, and one transport adapter copy. Notes the redundant SETUP cap in the lite-07 cut. Co-Authored-By: Claude Opus 5.5 --- quest/m0/README.md | 1 + quest/m0/expiry-wakes.md | 43 +++++++++++++++++++ quest/m1/README.md | 5 +++ quest/m1/ietf-subscribe-update-route.md | 17 ++++++++ quest/m1/js-request-window.md | 17 ++++++++ quest/m1/lite07-finalize.md | 3 ++ quest/m1/relay-session-limits.md | 21 +++++++++ quest/m1/session-churn-held.md | 22 ++++++++++ quest/m1/test-flakes-2/README.md | 1 + .../m1/test-flakes-2/import-catalog-finish.md | 15 +++++++ quest/m1/varint-test-gaps.md | 20 +++++++++ quest/m2/README.md | 2 + quest/m2/tls-listener-mtls.md | 16 +++++++ quest/m2/transport-adapter-dedup.md | 17 ++++++++ 14 files changed, 200 insertions(+) create mode 100644 quest/m0/expiry-wakes.md create mode 100644 quest/m1/ietf-subscribe-update-route.md create mode 100644 quest/m1/js-request-window.md create mode 100644 quest/m1/relay-session-limits.md create mode 100644 quest/m1/session-churn-held.md create mode 100644 quest/m1/test-flakes-2/import-catalog-finish.md create mode 100644 quest/m1/varint-test-gaps.md create mode 100644 quest/m2/tls-listener-mtls.md create mode 100644 quest/m2/transport-adapter-dedup.md diff --git a/quest/m0/README.md b/quest/m0/README.md index 412d2f94947..a01ff6a620d 100644 --- a/quest/m0/README.md +++ b/quest/m0/README.md @@ -43,6 +43,7 @@ a published `@moq/watch` break. ## Required +- [Expiry wakes](/quest/m0/expiry-wakes.md) - an appended group wakes only the parked reads it expires, so a 2.5 ms-frame publisher with 2 s of parked serves stays near idle and the Python and Go interop rows pass - [Request caps](/quest/m0/request-caps.md) - lite message sizes, IETF request IDs, and per-session announces and subscriptions are bounded - [qmux credit](/quest/m0/qmux-credit.md) - qmux returns connection credit for dropped and stopped streams and delivers its close frame, on both lines - [Shared fronts](/quest/m0/shared-fronts.md) - viewer sessions share a front, so fronts scale with peers, not viewers, and viewer churn no longer leaks fronts diff --git a/quest/m0/expiry-wakes.md b/quest/m0/expiry-wakes.md new file mode 100644 index 00000000000..616a6fa1eb0 --- /dev/null +++ b/quest/m0/expiry-wakes.md @@ -0,0 +1,43 @@ +# [M] An appended group wakes only the parked reads it expires + +## Goal + +A group appended to a track wakes only the parked group serves whose expiry +it crosses, not every parked serve. A publisher sending one 2.5 ms Opus frame +per group to a subscriber with a 2 s max age (about 800 parked serves) keeps +its runtime thread near idle, and `just test interop --all` passes its +Python and Go publisher rows again. + +## Plan + +Root cause, found 2026-10-06 while chasing the red Interop nightly: since +#4162 raised the browser's audio max age to 2 s, an FFI publisher's +subscription parks up to ~800 group serves. Every parked serve registers on +track-state changes and on the next unstamped group past the edge +(`GroupExpiry::is_expired` in `rs/moq-net/src/model/track.rs`, reached from +`group::Consumer::poll_expired_while_pending` in the lite and IETF +publishers' `GroupServe`). Each append wakes all N, so the single moq-ffi +runtime thread sits at ~110% CPU, QUIC idles out, and subscribers see +"internal error" or RESET_STREAM. #4892 made each check O(log N) but kept the +N-wide wake per append. It depends on load and build profile, so it fails on +unrelated branches. + +Decided 2026-10-06: deadline-indexed wakes. A parked read goes stale only +once the live edge passes its reach plus its budget, so keep parked reads in +a track-level map ordered by that point and wake only those the advancing +edge crosses. Keep the register-before-judge order that prevents lost wakes. +Rejected: skipping expiry checks while a serve waits for stream credit +(stale serves linger, lost-wakeup risk, fan-out stays elsewhere), capping +backlog serves per subscription (changes delivery scheduling), and lowering +the browser audio max age or using 20 ms frames in the interop clients +(hides the bug). + +Tests: + +- A mocked-time regression test that counts wakes: N parked serves, one + append, and only the crossed serves wake. It fails today. +- Sweep `track_parked_read` in `rs/moq-net/benches/track.rs` over parked + readers, so a per-append cost that grows with N shows up as a slope. +- `just test interop --all` passes the Python and Go publisher rows on CI. + +Public API: none. Wire: none. diff --git a/quest/m1/README.md b/quest/m1/README.md index 849bcbd842a..b5b7528811b 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -85,6 +85,11 @@ blocks. The quests that gated m0 lines moved under them. - [Datagrams are live-only](/quest/m1/datagram-unfetchable.md) - no FETCH, replay to a new subscriber, or cache fill ever returns a datagram group - [JavaScript FETCH](/quest/m1/js-fetch.md) - browser publishers answer IETF FETCH through the JS ranges request surface - [Bounded HLS playlists](/quest/m1/hls-bounded.md) - `moq-hls` renders a capped sliding window for a fresh viewer, with a stable media sequence, gap slots, and no video before a sync point +- [SUBSCRIBE_UPDATE routing](/quest/m1/ietf-subscribe-update-route.md) - on drafts 14 to 16 an update reaches the subscription it names, not its own request ID +- [JS request window](/quest/m1/js-request-window.md) - @moq/net grants MAX_REQUEST_ID as requests close, so a peer never stalls after ~21k requests +- [Relay session limits](/quest/m1/relay-session-limits.md) - moq-relay sets per-session request limits, tighter for clients than peers, and the bindings name a refused request +- [Churn with held subscriptions](/quest/m1/session-churn-held.md) - opening and closing a request costs the same with 1 or 1,024 held subscriptions +- [Wide varint tests](/quest/m1/varint-test-gaps.md) - wide sequences and both varint forms are tested where they differ - [Archive](/quest/m1/archive/README.md) - record selected tracks to any object_store and replay them over FETCH or derived HLS; the catalog entry and format may break in place, since no archives exist - [In-band auth](/quest/m1/auth/README.md) - a session tells its peer what it may publish and subscribe to, unions tokens presented in band, and fails loud on an out-of-scope publish - [Dropped sources](/quest/m1/dropped-sources.md) - track consumers see the producer's real error on every end path, never `Dropped` diff --git a/quest/m1/ietf-subscribe-update-route.md b/quest/m1/ietf-subscribe-update-route.md new file mode 100644 index 00000000000..88d0d32e537 --- /dev/null +++ b/quest/m1/ietf-subscribe-update-route.md @@ -0,0 +1,17 @@ +# [S] SUBSCRIBE_UPDATE reaches its subscription on drafts 14 to 16 + +## Goal + +On moq-transport drafts 14 to 16, a SUBSCRIBE_UPDATE changes the +subscription it names. Today `rs/moq-net/src/ietf/adapter.rs` routes it by +its own new request ID (its first field), so the update never reaches its +subscription. + +## Plan + +Found while landing request caps (#4820); the bug predates it. Route a +drafts 14 to 16 SUBSCRIBE_UPDATE by the subscription request ID it carries, +and keep the newer drafts' routing as it is. Land a regression test that +sends an update on each affected draft and fails without the fix. + +Public API: none. Wire: behaviour within the drafts; no format change. diff --git a/quest/m1/js-request-window.md b/quest/m1/js-request-window.md new file mode 100644 index 00000000000..2defd62b8b8 --- /dev/null +++ b/quest/m1/js-request-window.md @@ -0,0 +1,17 @@ +# [S] @moq/net grants request IDs as requests close + +## Goal + +A Rust peer can make any number of requests over one JS session on +moq-transport drafts 14 to 16. Today `js/net` advertises +`MAX_REQUEST_ID = 42069` (`js/net/src/connection/accept.ts`) and never grants +more, so the peer stalls after about 21k requests. + +## Plan + +Mirror Rust's window from request caps (#4820): advertise a finite window +and send MAX_REQUEST_ID to grant more as requests close. Refuse an incoming +ID past the window with the draft's error, as Rust does. A mocked-time test +drives more than 21k requests through one session. + +Public API: none. Wire: behaviour within the drafts; no format change. diff --git a/quest/m1/lite07-finalize.md b/quest/m1/lite07-finalize.md index b012e1e9302..03eb468ee90 100644 --- a/quest/m1/lite07-finalize.md +++ b/quest/m1/lite07-finalize.md @@ -44,6 +44,9 @@ The cut: rather than misparse; no compatibility shim. - The draft's lite-07 changelog matches the wire and `just drafts check` passes. +- The SETUP section's own 65,536-byte cap sentence is dropped if the general + Message Length cap from request caps (#4820) has landed, since it then + repeats that rule. Rust's lite-07 varints already carry the full 64 bits the draft specifies, so no codec work waits on the cut. diff --git a/quest/m1/relay-session-limits.md b/quest/m1/relay-session-limits.md new file mode 100644 index 00000000000..9efa54b8560 --- /dev/null +++ b/quest/m1/relay-session-limits.md @@ -0,0 +1,21 @@ +# [S] The relay configures per-session request limits + +## Goal + +`moq-relay` sets moq-net's `session::Limits` from its config, with a tighter +default for client sessions than for cluster peers, and documents it in +`doc/bin/relay/`. The bindings report a refused request as its own error +kind instead of mapping `TooManyRequests` to Unknown. + +## Plan + +Request caps (#4820) put generous defaults in moq-net (100,000 announces and +10,000 subscriptions per session), sized for relay meshes, so viewer +sessions get loose limits. The relay knows which sessions are peers, so it +picks the limit per session. Propose the flag and TOML names in the PR. + +Add the error kind to `rs/moq-ffi` and every wrapper per the Cross-Package +Sync table. + +Public API: a relay config field and flag, and a new error kind in moq-ffi +and the bindings. Wire: none. diff --git a/quest/m1/session-churn-held.md b/quest/m1/session-churn-held.md new file mode 100644 index 00000000000..7324855d1aa --- /dev/null +++ b/quest/m1/session-churn-held.md @@ -0,0 +1,22 @@ +# [M] Request churn costs the same with many held subscriptions + +## Goal + +Opening and closing a request on a session costs the same whether the +session holds 1 or 1,024 subscriptions. The `session_churn_held` bench added +by request caps (#4820) is flat in held subscriptions. + +## Plan + +Measured with #4820: about 70 µs per churn at held=1 and 10 to 20 ms at +held=1024 across 4 sessions, equally on lite-06 and draft-22. Neither has a +request window, so the slope predates request caps. Profile first, find what +scans the held subscriptions on each churn, and make that path proportional +to what it touches. Keep the bench swept over both sessions and held +subscriptions. + +Public API: none. Wire: none. + +## Related + +- [Reproducible relay CPU and allocation profiles](/quest/m1/performance-profiles.md) - the profiling setup this can use diff --git a/quest/m1/test-flakes-2/README.md b/quest/m1/test-flakes-2/README.md index 16de2758ed0..2ee803d03b5 100644 --- a/quest/m1/test-flakes-2/README.md +++ b/quest/m1/test-flakes-2/README.md @@ -33,3 +33,4 @@ Public API: none. Wire: none. - [Subscription cut by disconnect](/quest/m1/test-flakes-2/subscription-cut.md) - a publisher disconnect never ends a subscription clean - [Media late join](/quest/m1/test-flakes-2/media-late-join.md) - `just test media` late join stays within one GOP, or the regression is fixed - [Rejoin after idle](/quest/m1/test-flakes-2/rejoin-idle-race.md) - an IETF rejoin never gets the stale warm cache first, because the copy goes idle before the cancel +- [Import catalog finish](/quest/m1/test-flakes-2/import-catalog-finish.md) - `moq-cli`'s EOF catalog-finish import test runs on a paused clock or in-memory fixture diff --git a/quest/m1/test-flakes-2/import-catalog-finish.md b/quest/m1/test-flakes-2/import-catalog-finish.md new file mode 100644 index 00000000000..9d5411669b5 --- /dev/null +++ b/quest/m1/test-flakes-2/import-catalog-finish.md @@ -0,0 +1,15 @@ +# [XS] Import catalog finish + +## Goal + +`moq-cli`'s `import_delivers_the_catalog_finish_at_eof` +(`rs/moq-cli/tests/import.rs`) passes under a loaded `just check`. It runs +8.8 s alone against a 10 s timeout and fails with "announce timed out" under +load. + +## Plan + +Move it to a paused clock or an in-memory fixture, following this line's +rules; never raise the timeout. + +Public API: none. Wire: none. diff --git a/quest/m1/varint-test-gaps.md b/quest/m1/varint-test-gaps.md new file mode 100644 index 00000000000..30f4b11087a --- /dev/null +++ b/quest/m1/varint-test-gaps.md @@ -0,0 +1,20 @@ +# [S] Wide varints are tested where the two forms differ + +## Goal + +The plain-`u64` varint codec (#4437) has tests for the cases its first round +left open: + +- The wide-sequence relay test asserts the error a lite-06 reader sees for a + group of `1 << 62` from a lite-07 publisher, not any error. Today the reader + only sees `Stream(Internal)` over the wire, so decide what it should see. +- A subscription whose groups turn wide partway through fails only that + subscription, and the session and other tracks keep going. +- Announce compression is tested where the QUIC and leading-ones forms size + the same value differently. + +## Plan + +Extend `rs/moq-net/tests/wide_sequence.rs` and the lite announce encode tests. + +Public API: none. Wire: none. diff --git a/quest/m2/README.md b/quest/m2/README.md index 6d071d88ae0..587c1db04a3 100644 --- a/quest/m2/README.md +++ b/quest/m2/README.md @@ -60,6 +60,8 @@ waiting on an upstream release waits in [m4](/quest/m4/README.md). - [Bench coverage](/quest/m2/bench-coverage.md) - Criterion targets for moq-pattern matching first, then the stats producer, moq-mux containers, the hang catalog, and moq-auth - [Signed priority](/quest/m2/signed-priority.md) - every API priority is an `i8` with 0 as the unset midpoint, and hang's built-ins sit above it - [SEI separation study](/quest/m2/sei.md) - measure whether separating SEI saves enough, or has a metadata-only consumer, to justify a split +- [mTLS on tls://](/quest/m2/tls-listener-mtls.md) - a `tls://` listener can identify a cluster peer by its client certificate +- [One transport adapter](/quest/m2/transport-adapter-dedup.md) - the poll transport adapter exists once, and its read cap is tested - [AV1 metadata separation](/quest/m2/av1-metadata.md) - retain metadata OBUs inline while evaluating separate delivery - [Catalog track identity](/quest/m2/catalog-tracks.md) - a changed track configuration becomes a new track name or epoch, never a mutated definition - [Catalog colour model](/quest/m2/color-catalog.md) - the catalog describes a rendition's colour and HDR properties once a renderer consumes them diff --git a/quest/m2/tls-listener-mtls.md b/quest/m2/tls-listener-mtls.md new file mode 100644 index 00000000000..36e7f702e22 --- /dev/null +++ b/quest/m2/tls-listener-mtls.md @@ -0,0 +1,16 @@ +# [M] tls:// peers can authenticate by certificate + +## Goal + +A relay's `tls://` (qmux over TLS on TCP) listener can identify a cluster +peer by its client certificate, as the QUIC listener does, so an upstream +link doesn't need a token. + +## Plan + +#4816 added `tls://` with no client certificate request, so peers present a +token. Requesting one means owning the TLS accept instead of +`qmux::tls::Server`. Keep the token path working on the same listener, and +keep pinned peers and CA roots meaning the same as on QUIC. + +Public API: relay config may gain a listener option. Wire: none. diff --git a/quest/m2/transport-adapter-dedup.md b/quest/m2/transport-adapter-dedup.md new file mode 100644 index 00000000000..155c8a13c09 --- /dev/null +++ b/quest/m2/transport-adapter-dedup.md @@ -0,0 +1,17 @@ +# [S] One copy of the poll transport adapter + +## Goal + +The poll transport adapter exists once. #4709 copied the same ~310-line +adapter into `rs/moq-ffi`, `rs/moq-uring` and `rs/moq-wasm` +(`src/transport/adapter.rs`), and moq-tokio repeats its error and stats +wrappers. The 64 KiB cap on the default `poll_read_buf` has a unit test. + +## Plan + +The transport-seam quest ruled out a new shared adapter crate. Revisit that +here: share one copy from an existing crate (for example moq-net behind a +feature, or one backend re-exporting it to the others), or record why the +copies stay. Add the `poll_read_buf` cap test either way. + +Public API: possibly a feature-gated module. Wire: none. From 83253cf140e13ec644b2dcaa740fe3e506cb2ed3 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 6 Oct 2026 10:26:22 -0700 Subject: [PATCH 2/5] quest: clarify follow-up prerequisites and coverage Co-Authored-By: GPT-6 --- quest/m1/README.md | 2 +- quest/m1/ietf-subscribe-update-route.md | 21 +++++++++++-------- quest/m1/js-request-window.md | 10 +++++---- quest/m1/lite07-finalize.md | 7 ++++--- quest/m1/relay-session-limits.md | 14 +++++++++---- quest/m1/session-churn-held.md | 13 ++++++++---- quest/m1/test-flakes-2/README.md | 2 +- .../m1/test-flakes-2/import-catalog-finish.md | 11 ++++++++-- quest/m2/README.md | 2 +- quest/m2/transport-adapter-dedup.md | 15 ++++++++----- 10 files changed, 63 insertions(+), 34 deletions(-) diff --git a/quest/m1/README.md b/quest/m1/README.md index b5b7528811b..4f65a05df03 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -85,7 +85,7 @@ blocks. The quests that gated m0 lines moved under them. - [Datagrams are live-only](/quest/m1/datagram-unfetchable.md) - no FETCH, replay to a new subscriber, or cache fill ever returns a datagram group - [JavaScript FETCH](/quest/m1/js-fetch.md) - browser publishers answer IETF FETCH through the JS ranges request surface - [Bounded HLS playlists](/quest/m1/hls-bounded.md) - `moq-hls` renders a capped sliding window for a fresh viewer, with a stable media sequence, gap slots, and no video before a sync point -- [SUBSCRIBE_UPDATE routing](/quest/m1/ietf-subscribe-update-route.md) - on drafts 14 to 16 an update reaches the subscription it names, not its own request ID +- [Request update routing](/quest/m1/ietf-subscribe-update-route.md) - drafts 14 and 15 SUBSCRIBE_UPDATE and draft 16 REQUEST_UPDATE reach the request named by their second field - [JS request window](/quest/m1/js-request-window.md) - @moq/net grants MAX_REQUEST_ID as requests close, so a peer never stalls after ~21k requests - [Relay session limits](/quest/m1/relay-session-limits.md) - moq-relay sets per-session request limits, tighter for clients than peers, and the bindings name a refused request - [Churn with held subscriptions](/quest/m1/session-churn-held.md) - opening and closing a request costs the same with 1 or 1,024 held subscriptions diff --git a/quest/m1/ietf-subscribe-update-route.md b/quest/m1/ietf-subscribe-update-route.md index 88d0d32e537..4d8877d9337 100644 --- a/quest/m1/ietf-subscribe-update-route.md +++ b/quest/m1/ietf-subscribe-update-route.md @@ -1,17 +1,20 @@ -# [S] SUBSCRIBE_UPDATE reaches its subscription on drafts 14 to 16 +# [S] Request updates reach their target on drafts 14 to 16 ## Goal -On moq-transport drafts 14 to 16, a SUBSCRIBE_UPDATE changes the -subscription it names. Today `rs/moq-net/src/ietf/adapter.rs` routes it by -its own new request ID (its first field), so the update never reaches its -subscription. +On moq-transport drafts 14 and 15, a SUBSCRIBE_UPDATE reaches its +Subscription Request ID. On draft 16, a REQUEST_UPDATE reaches its Existing +Request ID. Today `rs/moq-net/src/ietf/adapter.rs` routes both by the update's +own new Request ID (the first field), so it never reaches its target. ## Plan -Found while landing request caps (#4820); the bug predates it. Route a -drafts 14 to 16 SUBSCRIBE_UPDATE by the subscription request ID it carries, -and keep the newer drafts' routing as it is. Land a regression test that -sends an update on each affected draft and fails without the fix. +Found while working on request caps (#4820); the bug predates it and can be +fixed independently. Route updates by their second field on drafts 14 to +16, and keep the newer drafts' routing as it is. The codec currently stores +that field as `subscription_request_id` on all three affected drafts. +Land a regression test for each affected draft with distinct update and +target IDs, proving the update reaches the named request and failing without +the fix. Public API: none. Wire: behaviour within the drafts; no format change. diff --git a/quest/m1/js-request-window.md b/quest/m1/js-request-window.md index 2defd62b8b8..e36356877ef 100644 --- a/quest/m1/js-request-window.md +++ b/quest/m1/js-request-window.md @@ -9,9 +9,11 @@ more, so the peer stalls after about 21k requests. ## Plan -Mirror Rust's window from request caps (#4820): advertise a finite window -and send MAX_REQUEST_ID to grant more as requests close. Refuse an incoming -ID past the window with the draft's error, as Rust does. A mocked-time test -drives more than 21k requests through one session. +Advertise a finite window and send MAX_REQUEST_ID to grant more as requests +close. Refuse an incoming ID past the window with the draft's error. This +can start before request caps (#4820), since the JS bug exists on main; use +the drafts as the contract and align with Rust's landed window when +available. A mocked-time test drives more than 21k requests through one +session. Public API: none. Wire: behaviour within the drafts; no format change. diff --git a/quest/m1/lite07-finalize.md b/quest/m1/lite07-finalize.md index 03eb468ee90..9882e56957b 100644 --- a/quest/m1/lite07-finalize.md +++ b/quest/m1/lite07-finalize.md @@ -44,9 +44,10 @@ The cut: rather than misparse; no compatibility shim. - The draft's lite-07 changelog matches the wire and `just drafts check` passes. -- The SETUP section's own 65,536-byte cap sentence is dropped if the general - Message Length cap from request caps (#4820) has landed, since it then - repeats that rule. +- Preserve SETUP's MUST reject rule for a Message Length above 65,536 bytes. + The general cap proposed by request caps (#4820) only permits rejection + (MAY), so it does not replace SETUP's stronger requirement. Remove the + SETUP sentence only if the general rule requires the same rejection. Rust's lite-07 varints already carry the full 64 bits the draft specifies, so no codec work waits on the cut. diff --git a/quest/m1/relay-session-limits.md b/quest/m1/relay-session-limits.md index 9efa54b8560..5406d9874aa 100644 --- a/quest/m1/relay-session-limits.md +++ b/quest/m1/relay-session-limits.md @@ -9,13 +9,19 @@ kind instead of mapping `TooManyRequests` to Unknown. ## Plan -Request caps (#4820) put generous defaults in moq-net (100,000 announces and -10,000 subscriptions per session), sized for relay meshes, so viewer -sessions get loose limits. The relay knows which sessions are peers, so it -picks the limit per session. Propose the flag and TOML names in the PR. +Blocked until request caps (#4820) lands. That PR proposes generous +defaults in moq-net (100,000 announces and 10,000 subscriptions per session), +sized for relay meshes, so viewer sessions would get loose limits. Confirm +the landed API and defaults before implementing this quest. The relay knows +which sessions are peers, so it picks the limit per session. Propose the flag +and TOML names in the PR. Add the error kind to `rs/moq-ffi` and every wrapper per the Cross-Package Sync table. Public API: a relay config field and flag, and a new error kind in moq-ffi and the bindings. Wire: none. + +## Required + +- [Request caps](/quest/m0/request-caps.md) - the session limits and TooManyRequests error exist on main diff --git a/quest/m1/session-churn-held.md b/quest/m1/session-churn-held.md index 7324855d1aa..1c891c0864d 100644 --- a/quest/m1/session-churn-held.md +++ b/quest/m1/session-churn-held.md @@ -3,13 +3,14 @@ ## Goal Opening and closing a request on a session costs the same whether the -session holds 1 or 1,024 subscriptions. The `session_churn_held` bench added -by request caps (#4820) is flat in held subscriptions. +session holds 1 or 1,024 subscriptions. The `session_churn_held` benchmark +proposed by request caps (#4820) is flat in held subscriptions. ## Plan -Measured with #4820: about 70 µs per churn at held=1 and 10 to 20 ms at -held=1024 across 4 sessions, equally on lite-06 and draft-22. Neither has a +Blocked until request caps (#4820) lands with the benchmark. Measured on +that PR: about 70 µs per churn at held=1 and 10 to 20 ms at held=1024 across +4 sessions, equally on lite-06 and draft-22. Neither has a request window, so the slope predates request caps. Profile first, find what scans the held subscriptions on each churn, and make that path proportional to what it touches. Keep the bench swept over both sessions and held @@ -17,6 +18,10 @@ subscriptions. Public API: none. Wire: none. +## Required + +- [Request caps](/quest/m0/request-caps.md) - the held-subscription churn benchmark exists on main + ## Related - [Reproducible relay CPU and allocation profiles](/quest/m1/performance-profiles.md) - the profiling setup this can use diff --git a/quest/m1/test-flakes-2/README.md b/quest/m1/test-flakes-2/README.md index 2ee803d03b5..02b607fc8ed 100644 --- a/quest/m1/test-flakes-2/README.md +++ b/quest/m1/test-flakes-2/README.md @@ -33,4 +33,4 @@ Public API: none. Wire: none. - [Subscription cut by disconnect](/quest/m1/test-flakes-2/subscription-cut.md) - a publisher disconnect never ends a subscription clean - [Media late join](/quest/m1/test-flakes-2/media-late-join.md) - `just test media` late join stays within one GOP, or the regression is fixed - [Rejoin after idle](/quest/m1/test-flakes-2/rejoin-idle-race.md) - an IETF rejoin never gets the stale warm cache first, because the copy goes idle before the cancel -- [Import catalog finish](/quest/m1/test-flakes-2/import-catalog-finish.md) - `moq-cli`'s EOF catalog-finish import test runs on a paused clock or in-memory fixture +- [Import catalog finish](/quest/m1/test-flakes-2/import-catalog-finish.md) - `moq-cli`'s subprocess EOF catalog-finish test holds up under load with event-based fixture coordination diff --git a/quest/m1/test-flakes-2/import-catalog-finish.md b/quest/m1/test-flakes-2/import-catalog-finish.md index 9d5411669b5..c7494c4d1e1 100644 --- a/quest/m1/test-flakes-2/import-catalog-finish.md +++ b/quest/m1/test-flakes-2/import-catalog-finish.md @@ -9,7 +9,14 @@ load. ## Plan -Move it to a paused clock or an in-memory fixture, following this line's -rules; never raise the timeout. +Preserve the real `moq` subprocess coverage: closing stdin must finish the +catalog and let the process exit successfully while the subscriber still +receives a clean finish. Pausing the parent's Tokio clock cannot control the +child process, and the real QUIC sockets can race an auto-advancing clock. +Reproduce the slowdown, fix its cause, and coordinate the fixture through +observable readiness and completion events rather than elapsed-time +assumptions. Use an in-memory fixture for separable protocol assertions if +helpful, while retaining the subprocess EOF regression. Never raise the +timeout or add a retry. Public API: none. Wire: none. diff --git a/quest/m2/README.md b/quest/m2/README.md index 587c1db04a3..1017c00b72e 100644 --- a/quest/m2/README.md +++ b/quest/m2/README.md @@ -61,7 +61,7 @@ waiting on an upstream release waits in [m4](/quest/m4/README.md). - [Signed priority](/quest/m2/signed-priority.md) - every API priority is an `i8` with 0 as the unset midpoint, and hang's built-ins sit above it - [SEI separation study](/quest/m2/sei.md) - measure whether separating SEI saves enough, or has a metadata-only consumer, to justify a split - [mTLS on tls://](/quest/m2/tls-listener-mtls.md) - a `tls://` listener can identify a cluster peer by its client certificate -- [One transport adapter](/quest/m2/transport-adapter-dedup.md) - the poll transport adapter exists once, and its read cap is tested +- [One transport adapter](/quest/m2/transport-adapter-dedup.md) - the poll transport adapter exists once, and moq-net's default poll_read_buf cap is tested - [AV1 metadata separation](/quest/m2/av1-metadata.md) - retain metadata OBUs inline while evaluating separate delivery - [Catalog track identity](/quest/m2/catalog-tracks.md) - a changed track configuration becomes a new track name or epoch, never a mutated definition - [Catalog colour model](/quest/m2/color-catalog.md) - the catalog describes a rendition's colour and HDR properties once a renderer consumes them diff --git a/quest/m2/transport-adapter-dedup.md b/quest/m2/transport-adapter-dedup.md index 155c8a13c09..9d36c1d515a 100644 --- a/quest/m2/transport-adapter-dedup.md +++ b/quest/m2/transport-adapter-dedup.md @@ -2,16 +2,21 @@ ## Goal -The poll transport adapter exists once. #4709 copied the same ~310-line -adapter into `rs/moq-ffi`, `rs/moq-uring` and `rs/moq-wasm` +The poll transport adapter exists once. #4709 proposes copying the same +~310-line adapter into `rs/moq-ffi`, `rs/moq-uring` and `rs/moq-wasm` (`src/transport/adapter.rs`), and moq-tokio repeats its error and stats -wrappers. The 64 KiB cap on the default `poll_read_buf` has a unit test. +wrappers. The 64 KiB cap on moq-net's default `poll_read_buf` has a unit test. ## Plan -The transport-seam quest ruled out a new shared adapter crate. Revisit that -here: share one copy from an existing crate (for example moq-net behind a +Blocked until transport seam (#4709) lands; confirm the landed adapter +shape first. The transport-seam quest ruled out a new shared adapter crate. +Revisit that here: share one copy from an existing crate (for example moq-net behind a feature, or one backend re-exporting it to the others), or record why the copies stay. Add the `poll_read_buf` cap test either way. Public API: possibly a feature-gated module. Wire: none. + +## Required + +- [Transport seam](/quest/m1/transport-seam.md) - the owned transport traits and backend adapters exist on main From 6c07cba4b143bf954aa677c286c6ada8c0c7f73e Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 6 Oct 2026 10:33:48 -0700 Subject: [PATCH 3/5] quest: an export failure is not the broadcast ending Co-Authored-By: Claude Opus 5.5 --- quest/m1/README.md | 1 + quest/m1/export-ts-linger.md | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 quest/m1/export-ts-linger.md diff --git a/quest/m1/README.md b/quest/m1/README.md index 4f65a05df03..249a0bb2416 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -90,6 +90,7 @@ blocks. The quests that gated m0 lines moved under them. - [Relay session limits](/quest/m1/relay-session-limits.md) - moq-relay sets per-session request limits, tighter for clients than peers, and the bindings name a refused request - [Churn with held subscriptions](/quest/m1/session-churn-held.md) - opening and closing a request costs the same with 1 or 1,024 held subscriptions - [Wide varint tests](/quest/m1/varint-test-gaps.md) - wide sequences and both varint forms are tested where they differ +- [Export linger](/quest/m1/export-ts-linger.md) - `moq export ts --linger` tells an export failure apart from the broadcast ending - [Archive](/quest/m1/archive/README.md) - record selected tracks to any object_store and replay them over FETCH or derived HLS; the catalog entry and format may break in place, since no archives exist - [In-band auth](/quest/m1/auth/README.md) - a session tells its peer what it may publish and subscribe to, unions tokens presented in band, and fails loud on an out-of-scope publish - [Dropped sources](/quest/m1/dropped-sources.md) - track consumers see the producer's real error on every end path, never `Dropped` diff --git a/quest/m1/export-ts-linger.md b/quest/m1/export-ts-linger.md new file mode 100644 index 00000000000..b4de46ac171 --- /dev/null +++ b/quest/m1/export-ts-linger.md @@ -0,0 +1,16 @@ +# [XS] An export failure is not the broadcast ending + +## Goal + +`moq export ts --linger` keeps waiting for a live broadcast when the export +itself fails, instead of treating that failure as the broadcast ending and +starting the linger countdown (`rs/moq-cli/src/subscribe.rs`). + +## Plan + +Reported by t0ms while grading #4645, who offered to do the CLI side. Tell +an export error apart from a finished broadcast: the error is reported and +fails the run as before, and only a real end starts the linger. A test drives +each case. + +Public API: none. Wire: none. From f9b0b9940b3083e490b371e977742616b9eaa0e8 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 6 Oct 2026 10:33:59 -0700 Subject: [PATCH 4/5] quest: state the export-linger goal as a failure, not a wait Co-Authored-By: Claude Opus 5.5 --- quest/m1/export-ts-linger.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/quest/m1/export-ts-linger.md b/quest/m1/export-ts-linger.md index b4de46ac171..2e7b48c6739 100644 --- a/quest/m1/export-ts-linger.md +++ b/quest/m1/export-ts-linger.md @@ -2,9 +2,9 @@ ## Goal -`moq export ts --linger` keeps waiting for a live broadcast when the export -itself fails, instead of treating that failure as the broadcast ending and -starting the linger countdown (`rs/moq-cli/src/subscribe.rs`). +`moq export ts --linger` reports an export failure while the broadcast is +still live as a failure, instead of treating it as the broadcast ending and +waiting out the linger (`rs/moq-cli/src/subscribe.rs`). ## Plan From 8ca0be423b06c4647e630e273178512a4e1328af Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 6 Oct 2026 11:59:11 -0700 Subject: [PATCH 5/5] quest: fold Grok review findings into the sweep follow-ups - export-ts-linger: classify by broadcast state, not the error; a killed publisher still lingers. Resized to [S] for the close race. - js-request-window: connect.ts advertises 42069 too. - ietf-subscribe-update-route: rewrite the existing classify test. - lite07-finalize: quote SETUP's MUST; changelog names the rejection. - tls-listener-mtls: with_tls already takes a ServerConfig; the gap is the peer certificate after qmux's handshake. Co-Authored-By: Claude Opus 5.5 --- quest/m1/README.md | 2 +- quest/m1/export-ts-linger.md | 23 ++++++++++++++++++----- quest/m1/ietf-subscribe-update-route.md | 4 +++- quest/m1/js-request-window.md | 5 +++-- quest/m1/lite07-finalize.md | 11 +++++++---- quest/m2/README.md | 2 +- quest/m2/tls-listener-mtls.md | 10 +++++++--- 7 files changed, 40 insertions(+), 17 deletions(-) diff --git a/quest/m1/README.md b/quest/m1/README.md index b145941538e..95416c93064 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -88,7 +88,7 @@ blocks. The quests that gated m0 lines moved under them. - [Relay session limits](/quest/m1/relay-session-limits.md) - moq-relay sets per-session request limits, tighter for clients than peers, and the bindings name a refused request - [Churn with held subscriptions](/quest/m1/session-churn-held.md) - opening and closing a request costs the same with 1 or 1,024 held subscriptions - [Wide varint tests](/quest/m1/varint-test-gaps.md) - wide sequences and both varint forms are tested where they differ -- [Export linger](/quest/m1/export-ts-linger.md) - `moq export ts --linger` tells an export failure apart from the broadcast ending +- [Export linger](/quest/m1/export-ts-linger.md) - `moq export ts --linger` fails on an export error while the broadcast is live, and lingers once it closes or is replaced - [Archive](/quest/m1/archive/README.md) - record selected tracks to any object_store and replay them over FETCH or derived HLS; the catalog entry and format may break in place, since no archives exist - [In-band auth](/quest/m1/auth/README.md) - a session tells its peer what it may publish and subscribe to, unions tokens presented in band, and fails loud on an out-of-scope publish - [Dropped sources](/quest/m1/dropped-sources.md) - track consumers see the producer's real error on every end path, never `Dropped` diff --git a/quest/m1/export-ts-linger.md b/quest/m1/export-ts-linger.md index 2e7b48c6739..d0fd7a703c7 100644 --- a/quest/m1/export-ts-linger.md +++ b/quest/m1/export-ts-linger.md @@ -1,4 +1,4 @@ -# [XS] An export failure is not the broadcast ending +# [S] An export failure is not the broadcast ending ## Goal @@ -8,9 +8,22 @@ waiting out the linger (`rs/moq-cli/src/subscribe.rs`). ## Plan -Reported by t0ms while grading #4645, who offered to do the CLI side. Tell -an export error apart from a finished broadcast: the error is reported and -fails the run as before, and only a real end starts the linger. A test drives -each case. +Reported by t0ms while grading #4645, who offered to do the CLI side. + +The error alone can't tell the cases apart: a publisher drop or SIGKILL +usually surfaces as a track `Err` too, and that must keep lingering (see the +comment above the linger in `run_ts`). Classify by the broadcast's state +instead. If it is still announced and live, the export failed: report it and +fail the run as before. If it closed or was replaced, it ended, clean or not, +and the linger starts. The track error can arrive before the broadcast's close +does, so the check has to settle that race rather than read the state once. + +A test drives each case: an export failure on a live broadcast fails the run, +a clean finish lingers, and a killed publisher still lingers and resumes when +the broadcast returns. Public API: none. Wire: none. + +## Related + +- [#4645 report](https://github.com/moq-dev/moq/pull/4645#issuecomment-6013366214) - the motivating `missed a decode deadline` failure exists only on #4645's branch diff --git a/quest/m1/ietf-subscribe-update-route.md b/quest/m1/ietf-subscribe-update-route.md index 4d8877d9337..5f28579c211 100644 --- a/quest/m1/ietf-subscribe-update-route.md +++ b/quest/m1/ietf-subscribe-update-route.md @@ -15,6 +15,8 @@ fixed independently. Route updates by their second field on drafts 14 to that field as `subscription_request_id` on all three affected drafts. Land a regression test for each affected draft with distinct update and target IDs, proving the update reaches the named request and failing without -the fix. +the fix. Rewrite the existing `test_classify_subscribe_update_followup` the +same way rather than keep it beside them: its body carries a single ID, so it +locks in the current routing. Public API: none. Wire: behaviour within the drafts; no format change. diff --git a/quest/m1/js-request-window.md b/quest/m1/js-request-window.md index e36356877ef..e873acdf245 100644 --- a/quest/m1/js-request-window.md +++ b/quest/m1/js-request-window.md @@ -4,8 +4,9 @@ A Rust peer can make any number of requests over one JS session on moq-transport drafts 14 to 16. Today `js/net` advertises -`MAX_REQUEST_ID = 42069` (`js/net/src/connection/accept.ts`) and never grants -more, so the peer stalls after about 21k requests. +`MaxRequestId = 42069` in SETUP, as a server (`js/net/src/connection/accept.ts`) +and as a client (`js/net/src/connection/connect.ts`), and never grants more, +so the peer stalls after about 21k requests. ## Plan diff --git a/quest/m1/lite07-finalize.md b/quest/m1/lite07-finalize.md index 9882e56957b..c783cabfb8e 100644 --- a/quest/m1/lite07-finalize.md +++ b/quest/m1/lite07-finalize.md @@ -44,10 +44,13 @@ The cut: rather than misparse; no compatibility shim. - The draft's lite-07 changelog matches the wire and `just drafts check` passes. -- Preserve SETUP's MUST reject rule for a Message Length above 65,536 bytes. - The general cap proposed by request caps (#4820) only permits rejection - (MAY), so it does not replace SETUP's stronger requirement. Remove the - SETUP sentence only if the general rule requires the same rejection. +- Preserve SETUP's rule: "a receiver MUST treat a longer SETUP as a protocol + violation and MAY reject it based on the length prefix alone" (65,536 + bytes). The general cap proposed by request caps (#4820) only permits + rejection (MAY), so it does not replace SETUP's stronger requirement. + Remove the SETUP sentence only if the general rule requires the same + rejection. If SETUP keeps its own rule, the lite-07 changelog still names + that rejection, not only the cap. Rust's lite-07 varints already carry the full 64 bits the draft specifies, so no codec work waits on the cut. diff --git a/quest/m2/README.md b/quest/m2/README.md index 1017c00b72e..3739a62be31 100644 --- a/quest/m2/README.md +++ b/quest/m2/README.md @@ -61,7 +61,7 @@ waiting on an upstream release waits in [m4](/quest/m4/README.md). - [Signed priority](/quest/m2/signed-priority.md) - every API priority is an `i8` with 0 as the unset midpoint, and hang's built-ins sit above it - [SEI separation study](/quest/m2/sei.md) - measure whether separating SEI saves enough, or has a metadata-only consumer, to justify a split - [mTLS on tls://](/quest/m2/tls-listener-mtls.md) - a `tls://` listener can identify a cluster peer by its client certificate -- [One transport adapter](/quest/m2/transport-adapter-dedup.md) - the poll transport adapter exists once, and moq-net's default poll_read_buf cap is tested +- [One transport adapter](/quest/m2/transport-adapter-dedup.md) - the poll transport adapter exists once, and the 64 KiB cap in moq-net's default `poll_read_buf` (not in the adapter) has a test - [AV1 metadata separation](/quest/m2/av1-metadata.md) - retain metadata OBUs inline while evaluating separate delivery - [Catalog track identity](/quest/m2/catalog-tracks.md) - a changed track configuration becomes a new track name or epoch, never a mutated definition - [Catalog colour model](/quest/m2/color-catalog.md) - the catalog describes a rendition's colour and HDR properties once a renderer consumes them diff --git a/quest/m2/tls-listener-mtls.md b/quest/m2/tls-listener-mtls.md index 36e7f702e22..74c7a843c85 100644 --- a/quest/m2/tls-listener-mtls.md +++ b/quest/m2/tls-listener-mtls.md @@ -9,8 +9,12 @@ link doesn't need a token. ## Plan #4816 added `tls://` with no client certificate request, so peers present a -token. Requesting one means owning the TLS accept instead of -`qmux::tls::Server`. Keep the token path working on the same listener, and -keep pinned peers and CA roots meaning the same as on QUIC. +token. `moq_tokio::tcp::Listener::with_tls` (`rs/moq-tokio/src/tcp.rs`) +already takes an `Arc`, so requesting a certificate is +config. The real gap is getting the peer certificate out afterwards: +`qmux::tls::Server::accept` finishes the handshake and returns a `Session` +without it. Either expose it upstream in qmux or own the TLS accept here. +Keep the token path working on the same listener, and keep pinned peers and +CA roots meaning the same as on QUIC. Public API: relay config may gain a listener option. Wire: none.