From 4d2eaad234c0d503ca4b084c8ff6034a11f28151 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 20 Oct 2026 11:46:54 -0700 Subject: [PATCH 1/6] quest: plan load flakes, GOAWAY follow-ups, and the moq-quic transport direction Co-Authored-By: Claude Opus 5.5 --- quest/m0/README.md | 7 +-- quest/m0/namespace-advert-error.md | 38 +++++++++++ quest/m0/qmux-credit-backport-upstream.md | 12 ---- quest/m0/qmux-credit-backport.md | 22 ------- quest/m0/qmux-credit-upstream.md | 12 ---- quest/m0/qmux-credit.md | 63 ------------------- quest/m1/README.md | 9 ++- quest/m1/audit-deps.md | 3 +- quest/m1/goaway-coverage.md | 36 ----------- quest/m1/js-request-owner.md | 26 ++++++++ quest/m1/native-noq-feature.md | 18 ++++++ quest/m1/qmux-no-wait.md | 11 +++- quest/{m2 => m1}/quic-qmux.md | 28 ++++++--- quest/m1/quic/group-reset.md | 2 +- quest/m1/resume-closed.md | 4 ++ quest/m1/rs-request-credit.md | 7 ++- quest/m1/test-flakes-2/README.md | 10 +++ quest/m1/test-flakes-2/draft16-cancel-race.md | 28 +++++++++ quest/m1/test-flakes-2/relay-bench-holder.md | 24 +++++++ quest/m1/test-flakes-2/tokio-failed-listen.md | 23 +++++++ quest/m1/test-flakes-2/uring-host-clock.md | 22 +++++++ quest/m1/tokio-redirect-loop.md | 41 ++++++++++++ quest/m1/ts-qmux.md | 32 ++++++++++ quest/m2/README.md | 4 +- quest/m2/drop-web-transport-trait.md | 15 +++-- quest/m2/probe/native.md | 2 - quest/m2/quic-ack-hook.md | 2 +- quest/m2/quic-deadline.md | 1 - quest/m2/quic-keep-alive.md | 4 -- ...q-socket-close.md => quic-socket-close.md} | 0 quest/m2/tls-listener-mtls.md | 2 +- quest/m2/wire-stats/qmux.md | 16 +++-- quest/m3/quic-careful-resume.md | 4 -- quest/m3/quic-ecn.md | 4 -- 34 files changed, 331 insertions(+), 201 deletions(-) create mode 100644 quest/m0/namespace-advert-error.md delete mode 100644 quest/m0/qmux-credit-backport-upstream.md delete mode 100644 quest/m0/qmux-credit-backport.md delete mode 100644 quest/m0/qmux-credit-upstream.md delete mode 100644 quest/m0/qmux-credit.md delete mode 100644 quest/m1/goaway-coverage.md create mode 100644 quest/m1/js-request-owner.md create mode 100644 quest/m1/native-noq-feature.md rename quest/{m2 => m1}/quic-qmux.md (78%) create mode 100644 quest/m1/test-flakes-2/draft16-cancel-race.md create mode 100644 quest/m1/test-flakes-2/relay-bench-holder.md create mode 100644 quest/m1/test-flakes-2/tokio-failed-listen.md create mode 100644 quest/m1/test-flakes-2/uring-host-clock.md create mode 100644 quest/m1/tokio-redirect-loop.md create mode 100644 quest/m1/ts-qmux.md rename quest/m2/{noq-socket-close.md => quic-socket-close.md} (100%) diff --git a/quest/m0/README.md b/quest/m0/README.md index 3405b0db8e0..c0cdced1ea7 100644 --- a/quest/m0/README.md +++ b/quest/m0/README.md @@ -83,15 +83,10 @@ Each lands its fixes in its own PR and plans only what needs a decision. ## Required -- [qmux 0.5 upstream](/quest/m0/qmux-credit-backport-upstream.md) - old-release publication, independent of the main fix - -- [qmux release backport](/quest/m0/qmux-credit-backport.md) - adopt the 0.5 fix independently of the next breaking cut - - [CI cost and latency](/quest/m0/ci/README.md) - cloud CI runs only in the merge queue as one `just ci` pass, expensive checks run nightly and gate `release`, caches move to R2, and runners are chosen last - [Draft-22 release notices](/quest/m0/release-22/README.md) - tell testers and reporters which published releases contain their fixes - [Release builds link](/quest/m0/release-link.md) - the Linux zigbuild release binaries and moq-gst packages link again after the 2026-10-11 nixpkgs bump, so the next release cut can publish -- [web-transport releases the qmux fixes](/quest/m0/qmux-credit-upstream.md) - waiting on #412 to publish the patched 0.6.x that main adopts -- [qmux credit](/quest/m0/qmux-credit.md) - main adopts qmux 0.6 credit and close fixes; the 0.5 backport is independent +- [Namespace advert errors](/quest/m0/namespace-advert-error.md) - one PUBLISH_NAMESPACE stream failing withdraws only that namespace instead of closing the Rust session - [Broadcast epochs](/quest/m0/broadcast-epoch/README.md) - every first-party publisher that can restart mints a fresh route epoch, the newest wins a path, and only routes with the same epoch resume a subscription - [Performance audit](/quest/m0/audit-perf.md) - benchmarks that regressed since the last run are explained, fixed, or planned, and unbenched hot paths get coverage - [Compile time audit](/quest/m0/audit-compile-time.md) - build-time hotspots are measured against a baseline and the worst are fixed or planned diff --git a/quest/m0/namespace-advert-error.md b/quest/m0/namespace-advert-error.md new file mode 100644 index 00000000000..4b1213cc534 --- /dev/null +++ b/quest/m0/namespace-advert-error.md @@ -0,0 +1,38 @@ +# [S] One namespace's stream error ends only its own advertisement + +## Goal + +On moq-transport, a Rust publisher whose PUBLISH_NAMESPACE request stream +fails while it waits for the first answer withdraws just that namespace, as +JS does, and keeps advertising and withdrawing every other namespace. +Today `advertise_namespace` propagates the error with `?` out of +`run_publish_namespaces` (`rs/moq-net/src/ietf/publisher.rs`), and the +session races that loop, so one reset PUBLISH_NAMESPACE stream closes the +whole session and aborts every track on it. When the error is a +`ProtocolViolation`, moq-tokio treats it as terminal and stops reconnecting. + +## Plan + +Found by [#5998](https://github.com/moq-dev/moq/pull/5998), which handles +GOAWAY throughout IETF request lifetimes; the bug predates it. Decided +2026-10-20 (maintainer): m0, since a single hostile or buggy peer stream ends +the session on main. JS already contains the failure per namespace +(`#advertise` in `js/net/src/ietf/publisher.ts`); match it. + +- Separate a request's own failure (its stream reset or ended, a refusal) + from a session failure (the control stream, a protocol violation that + closes the session at the source). The first ends that namespace's request + and leaves the loop running; the second still ends the loop. +- Re-offer a namespace whose request failed on the same backoff #5998 added + for a draft-18+ stream that ends while idle (100 ms, doubling to 5 s), so a + peer that resets every advertisement gets backed-off re-offers, not a + request loop. +- Regression test: two namespaces advertised, the peer resets the first's + request stream before answering; the second is still answered and + withdrawn normally, and the first is offered again after the wait. + +Public API: none. Wire: none. + +## Related + +- [Drain cleanup](/quest/m1/drain-cleanup.md) - what a drain does to sessions at its deadline diff --git a/quest/m0/qmux-credit-backport-upstream.md b/quest/m0/qmux-credit-backport-upstream.md deleted file mode 100644 index 2c28a78b8bc..00000000000 --- a/quest/m0/qmux-credit-backport-upstream.md +++ /dev/null @@ -1,12 +0,0 @@ -# [XS] web-transport releases the qmux 0.5 credit and close fixes - -## Goal - -Waiting on moq-dev/web-transport: #413 merges into qmux-v0.5.x and ships in -a qmux 0.5.x release after 0.5.2. Check -`gh pr view 413 -R moq-dev/web-transport` and crates.io. Delete this condition -once shipped; the 0.6 publication can complete independently. - -## Related - -- [Release pin](/quest/m0/qmux-credit-backport.md) - adopts the published fix diff --git a/quest/m0/qmux-credit-backport.md b/quest/m0/qmux-credit-backport.md deleted file mode 100644 index 15005455932..00000000000 --- a/quest/m0/qmux-credit-backport.md +++ /dev/null @@ -1,22 +0,0 @@ -# [XS] Backport the qmux credit and close pin - -## Goal - -The current release uses a patched qmux 0.5.x that returns unread stream -credit and delivers its close frame. It stays on web-transport-trait 0.4. - -## Plan - -Once the upstream 0.5 fix ships, update the release-line pin and run the -WebSocket and TLS transport checks. Follow the backport workflow. This is -independent of the 0.6 main pin and does not gate the next breaking cut. - -Public API: none. Wire: none. - -## Required - -- [qmux 0.5 publication](/quest/m0/qmux-credit-backport-upstream.md) - the release-line patch is available - -## Related - -- [qmux credit](/quest/m0/qmux-credit.md) - shared diagnosis and the main pin diff --git a/quest/m0/qmux-credit-upstream.md b/quest/m0/qmux-credit-upstream.md deleted file mode 100644 index ee8f7111a40..00000000000 --- a/quest/m0/qmux-credit-upstream.md +++ /dev/null @@ -1,12 +0,0 @@ -# [XS] web-transport releases the qmux 0.6 credit and close fixes - -## Goal - -Waiting on moq-dev/web-transport: #412 merges and ships in a qmux 0.6.x -release after 0.6.2. Check `gh pr view 412 -R moq-dev/web-transport` and -the published qmux versions on crates.io. Delete this condition once shipped. -The independent 0.5 publication does not block this condition. - -## Related - -- [qmux credit](/quest/m0/qmux-credit.md) - bumps main after publication diff --git a/quest/m0/qmux-credit.md b/quest/m0/qmux-credit.md deleted file mode 100644 index 87a3db9eb67..00000000000 --- a/quest/m0/qmux-credit.md +++ /dev/null @@ -1,63 +0,0 @@ -# [XS] qmux returns every byte's credit and sends its close frame - -## Goal - -A qmux session returns connection-level credit for every byte it receives, -whether the app reads it, drops the stream unread, or it arrives after -STOP_SENDING, so a long-lived session never stalls on MAX_DATA. `close()` -delivers its APPLICATION_CLOSE frame before the transport drops, so a TCP or -WebSocket peer sees the close code whenever the transport stays writable -within the close bound. This quest updates the 0.6 line `main` uses; the -0.5 release-line backport is independent. - -## Plan - -Facts from moq-dev/web-transport `rs/qmux/src/session.rs` (0.5.1 and main): - -- Credit returns only through `RecvStream::report_consumed` on reads. - `Drop for RecvStream` sends STOP_SENDING and returns stream-count credit, - but never consumes bytes still buffered or queued in `inbound_data`. -- On STOP_SENDING the writer removes the recv entry, so later STREAM data is - dropped before the connection-credit check, and a later RESET_STREAM - returns early, so its final-size gap is never consumed. The peer counts all - of it against MAX_DATA. -- A new stream's entry is inserted only after `accept_uni`/`accept_bi` hands - it over, so a stream dropped in that window leaves a stale entry that keeps - charging the connection window. -- `close()` queues APPLICATION_CLOSE and marks the session closed at once. A - writer that is mid-write treats that as interrupted and drops the frame. - 0.6 made `close()` idempotent and keeps the first reason, but not the order. - -Work, drafted upstream as -[web-transport#412](https://github.com/moq-dev/web-transport/pull/412) (0.6, -targets `main`) and -[web-transport#413](https://github.com/moq-dev/web-transport/pull/413) (0.5, -targets `qmux-v0.5.x`): - -- Consume credit for unread bytes on drop, for STREAM data and the RESET - final-size gap after STOP_SENDING (keep enough of a retired stream's state - to account for its final size), and close the accept gap. -- Mark the session closed only once the writer has sent the close frame, with - a bound so a stalled transport still drops. -- Tests in `rs/qmux`: a session that drops many unread streams keeps - delivering past its initial window, and a peer reads the close code after a - close issued mid-write. - -Remaining here: bump `main` once the patched 0.6.x ships. The independent -[release backport](/quest/m0/qmux-credit-backport.md) stays on 0.5 because -0.6 needs web-transport-trait 0.5, a breaking change. The old-line publication -is not a prerequisite of m1. Sized XS: only the pin bump remains here. - -Why m0: the WebSocket fallback, moq.pro's Voice, and the edge-to-core -`tls://` links (#4816) that moq.pro's cluster sessions move to all run on -qmux, and MoQ drops streams constantly. - -Public API: none. Wire: none. - -## Required - -- [web-transport releases the fixes](/quest/m0/qmux-credit-upstream.md) - #412 merges and ships as a patched 0.6.x - -## Related - -- [qmux on the QUIC stream state machine](/quest/m2/quic-qmux.md) - replaces these stream maps later diff --git a/quest/m1/README.md b/quest/m1/README.md index 0520ca02390..4567b4f6e70 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -49,7 +49,6 @@ lands before the cut. - [Epoch correctness](/quest/m0/broadcast-epoch/README.md) - publisher identity, duplicate suppression, and catalog pinning gate the breaking cut - [Release builds](/quest/m0/release-link.md) - package artifacts can be built -- [qmux credit](/quest/m0/qmux-credit.md) - dropped/stopped streams return credit and close is delivered - [moq-net follows announced instances](/quest/m1/net-announced-follow.md) - the Rust equivalent of JS `request(path, { announced: true })`, replacing the follow loops in moq-ffi and moq-c - [Sink restart follow](/quest/m1/sink-restart-follow.md) - moq-cli's server and push sinks follow an epochless `Restart` where their output can mark it, and fail loud otherwise @@ -57,6 +56,8 @@ lands before the cut. - [Routing guide](/quest/m1/routing-guide.md) - concepts and practical outbound federation setup - [Controlled time](/quest/m1/time/README.md) - every crate and package reads time through moq-clock or @moq/clock, with regression tests for the changed contracts - [Own the QUIC stack](/quest/m1/quic/README.md) - quinn hard-forked in-tree as `moq-quic`, carrying BBR, reliable reset, hierarchical scheduling, peer limits, and endpoint sharding +- [qmux on the QUIC stream state machine](/quest/m1/quic-qmux.md) - qmux runs in-tree over `moq-quic`'s stream state machine, returning credit for every byte and delivering its close frame +- [Drop moq-native's noq feature](/quest/m1/native-noq-feature.md) - the empty default `noq` feature is removed - [Finalize moq-lite-07](/quest/m1/lite07-finalize.md) - when the maintainer cuts it, lite-07 negotiates as `moq-lite-07` and the next release ships it - [moq-transport passthrough](/quest/m1/ietf-extension/README.md) - object IDs, properties, end status, datagram objects, and unknown track properties survive moq-transport -> moq-relay (and a relay cluster) -> moq-transport; moxygen's moq-test sections 1-7 pass nightly, except cases that use a subgroup other than 0 - [Refresh](/quest/m1/refresh/README.md) - a join asks for a fresh group once the current group's deltas exceed a publisher-advertised byte ratio, at every hop; replaces `Demand::used`/`unused` and moq-json's `delta_ratio` @@ -91,11 +92,13 @@ lands before the cut. - [Handshake deadline covers admission](/quest/m1/handshake-admission-deadline.md) - `listen.timeout` bounds a connection through auth admission on both listeners, replacing the hardcoded admission timeout - [Restart reset](/quest/m1/stateless-reset-key.md) - a relay restarted with the same `listen.reset_key` stateless-resets its predecessor's connections, so clients reconnect within an RTT instead of the idle timeout - [Drain cleanup](/quest/m1/drain-cleanup.md) - sessions force-closed at the drain deadline record their lease end and bytes before the relay or CLI exits, while unfinished handshakes stay bounded -- [GOAWAY coverage](/quest/m1/goaway-coverage.md) - every IETF request stream handles GOAWAY, a closed control stream is a violation, and the draft-18 cutoff is honored +- [Rust redirect loops](/quest/m1/tokio-redirect-loop.md) - moq-tokio escalates its backoff when GOAWAY redirects bounce between peers, as JS does +- [JS request owner](/quest/m1/js-request-owner.md) - every JS IETF request stream has one owning handle with an `ended` promise and one closer - [Container reads drop backfill](/quest/m1/read-backfill.md) - the JS container consumer drops groups below its read cursor like Rust - [Rust request credit](/quest/m1/rs-request-credit.md) - a moq-net request fails at once without stream credit instead of waiting - [A stalled subscriber holds bounded state](/quest/m1/egress-held-state.md) - group machines and bytes a credit-withholding downstream makes a publisher hold plateau at what its credit and the latency budget allow, on lite and moq-transport -- [qmux no-wait opens](/quest/m1/qmux-no-wait.md) - @moq/qmux rejects an over-limit create when waitUntilAvailable is false, like Chrome +- [TS qmux in the monorepo](/quest/m1/ts-qmux.md) - `@moq/qmux` moves into `js/` and `js/net` stops depending on moq-dev/web-transport +- [qmux no-wait opens](/quest/m1/qmux-no-wait.md) - the in-tree @moq/qmux rejects an over-limit create when waitUntilAvailable is false, like Chrome - [Timed-out requests clean up](/quest/m1/legacy-request-timeout-cleanup.md) - on drafts 14-16, Rust and JS send PUBLISH_NAMESPACE_DONE or UNSUBSCRIBE_NAMESPACE for a timed-out request before its local reset - [TS fidelity and recovery](/quest/m1/ts-reliability/README.md) - both modes have explicit profiles, automated validation, loss tests, and a dedicated guide - [Resume reorder](/quest/m1/resume-reorder.md) - a group whose stream header arrives after its successor's still resumes on failover; only a dropped or long-gone group fails diff --git a/quest/m1/audit-deps.md b/quest/m1/audit-deps.md index 084e31f277e..ca3ea9ddcc6 100644 --- a/quest/m1/audit-deps.md +++ b/quest/m1/audit-deps.md @@ -72,7 +72,8 @@ duplicates are ecosystem transitions owned upstream: webpki-roots 0.26). winit 0.30 holds rustix 0.38, x11rb 0.13, and objc2 0.5 until 0.31 leaves beta. - Our own crates: web-transport-iroh 0.8.3 holds kio 0.5 (main already takes - 0.6, waiting on a moq-dev/web-transport release), moq-vaapi holds bindgen + 0.6; iroh is an accepted exception to moq taking no new work in + moq-dev/web-transport, so this lasts until that crate is released on its own), moq-vaapi holds bindgen 0.70, libloading 0.8, and thiserror 1, and mp4-atom holds thiserror 1. - Bun has no dedupe, so same-major duplicates (`@babel/types`, `@vue/shared`, `minimatch`, `picomatch`, `yaml`) persist until a parent diff --git a/quest/m1/goaway-coverage.md b/quest/m1/goaway-coverage.md deleted file mode 100644 index caeee3bdf13..00000000000 --- a/quest/m1/goaway-coverage.md +++ /dev/null @@ -1,36 +0,0 @@ -# [M] GOAWAY reaches every IETF request stream - -## Goal - -On moq-transport, every request stream in Rust and JS (publisher and -subscriber side, including FETCH, TRACK_STATUS and SUBSCRIBE_TRACKS) handles -a GOAWAY the way SUBSCRIBE does, a control stream that ends or resets is a -PROTOCOL_VIOLATION, and draft-18's Request ID cutoff answers requests past it -with GOING_AWAY. - -## Plan - -Found by #5800 (2026-10-17 state of main): - -- Only SUBSCRIBE and SUBSCRIBE_NAMESPACE on the publisher side read GOAWAY - on their request streams (Rust `request_stream`, JS `nextMessage` and - `namespaceUpdate`). FETCH, TRACK_STATUS, SUBSCRIBE_TRACKS, and every - subscriber-side request stream end the request on a GOAWAY instead. -- A control stream FIN is treated as a clean end: Rust's draft-17+ GOAWAY - reader returns Ok, the draft 14-16 adapter tears the session down with no - code, and JS closes with no code. Draft 21 section 6.3 makes it a - PROTOCOL_VIOLATION; check the SETUP STOP_SENDING ordering too. -- Draft 18's GOAWAY Request ID is only parity-checked. Requests at or past - the cutoff never get GOING_AWAY, and our own GOAWAY sends a fixed 0 or 1. -- Check JS for a duplicate-GOAWAY race. - -Decided 2026-10-17 (maintainer): m1, beside drain cleanup. Draft 17 stays -low priority; fix it where the shared code makes it free. - -Public API: none expected. Wire: closer conformance on drafts 14-22; no new -fields. - -## Related - -- [Drain cleanup](/quest/m1/drain-cleanup.md) - what a drain does to sessions at its deadline -- #5798 - the legacy drafts' request handling diff --git a/quest/m1/js-request-owner.md b/quest/m1/js-request-owner.md new file mode 100644 index 00000000000..f8fb7bb4f34 --- /dev/null +++ b/quest/m1/js-request-owner.md @@ -0,0 +1,26 @@ +# [S] Every JS request stream has one owner and one closer + +## Goal + +In `js/net`'s IETF adapter, each request stream has a single handle that +owns its reader and exposes a terminal `ended` promise every consumer races, +so no request reader can be left detached, and each request closes through a +single per-request closer, so request tasks stop closing a session or stream +twice with competing codes. + +## Plan + +Follow-ups of [#5998](https://github.com/moq-dev/moq/pull/5998). Today no +reader is detached, but nothing prevents it: each request type wires its own +read loop and close path, and #5998's unit harness has to assert that every +close carries one code because the transport keeps the first. + +- Add the handle internally in `js/net/src/ietf/` and move SUBSCRIBE, FETCH, + TRACK_STATUS, SUBSCRIBE_NAMESPACE, SUBSCRIBE_TRACKS and PUBLISH_NAMESPACE + (both sides) onto it. +- Check whether the Rust request tasks close twice the same way and give + them the same single closer if so; mirror the shape in both languages. +- Internal refactor: deleting the per-request read and close code it + replaces is the measure of success. + +Public API: none. Wire: none. diff --git a/quest/m1/native-noq-feature.md b/quest/m1/native-noq-feature.md new file mode 100644 index 00000000000..71d192d9893 --- /dev/null +++ b/quest/m1/native-noq-feature.md @@ -0,0 +1,18 @@ +# [XS] moq-native drops its empty noq feature + +## Goal + +`rs/moq-native` no longer declares the empty `noq = []` feature it enables +by default, and every reference to it (other crates' feature lists, docs, +CI matrices) is gone. + +## Plan + +Found 2026-10-20 auditing dependencies on the web-transport repo, noq and +quinn after the maintainer decided moq's own QUIC stack, `moq-quic`, powers +everything. The feature gates nothing. + +- Removing a feature is breaking for anyone who names it: add an entry + under `## Unreleased` in moq-native's `MIGRATE.md`. + +Public API: the `noq` Cargo feature is removed. Wire: none. diff --git a/quest/m1/qmux-no-wait.md b/quest/m1/qmux-no-wait.md index da270030d60..234c57fe8a2 100644 --- a/quest/m1/qmux-no-wait.md +++ b/quest/m1/qmux-no-wait.md @@ -5,11 +5,18 @@ `@moq/qmux` (the WebSocket fallback, and so Safari) rejects `createBidirectionalStream` and `createUnidirectionalStream` at once when `waitUntilAvailable` is false and the peer has granted no stream credit, as -Chrome's WebTransport does, instead of queueing the create. Released from -moq-dev/web-transport and bumped in `js/net/package.json`. +Chrome's WebTransport does, instead of queueing the create. The change lands +in the in-tree package. ## Plan Decided 2026-10-07: JS requests pass the flag and otherwise rely on their 10 s timer to bound the queue. Match the rejection Chrome raises so `@moq/net` handles both the same way. + +Decided 2026-10-20 (maintainer): patched in-tree after +[TS qmux](/quest/m1/ts-qmux.md), not upstream in moq-dev/web-transport. + +## Required + +- [TS qmux](/quest/m1/ts-qmux.md) - `@moq/qmux` lives in `js/` diff --git a/quest/m2/quic-qmux.md b/quest/m1/quic-qmux.md similarity index 78% rename from quest/m2/quic-qmux.md rename to quest/m1/quic-qmux.md index 08c4f245f82..835234e7b72 100644 --- a/quest/m2/quic-qmux.md +++ b/quest/m1/quic-qmux.md @@ -6,8 +6,7 @@ qmux lives in the monorepo and uses `moq-quic`'s stream state machine for stream lifecycle, flow control, datagram buffering, priority scheduling, and transport parameters. It owns only record framing, its transport adapters, and the semantics that differ from QUIC, instead of maintaining a parallel -stream implementation. moq-tokio and moq-relay depend on the in-tree crate, -and moq-dev/web-transport no longer carries a Rust qmux. +stream implementation. moq-tokio and moq-relay depend on the in-tree crate. ## Plan @@ -18,8 +17,8 @@ Decided in the 2026-09-30 plan: negotiation, byte-stream framing, and socket stats (about 2.3k lines) onto it. Its session implements moq-net's `transport::poll` traits instead of `web-transport-trait` ([Drop web-transport-trait](/quest/m2/drop-web-transport-trait.md)). The TypeScript `@moq/qmux` - and `@moq/web-socket-stream` stay in moq-dev/web-transport with their own - state machine and become the interop reference. + keeps its own state machine and is the interop reference; it moves into + `js/` in [TS qmux](/quest/m1/ts-qmux.md). - **All four wire versions stay**: draft-02, draft-01, draft-00, and the legacy `webtransport` format, since published clients (and the TS peer) negotiate them. They differ only in record framing, so they share the one @@ -28,8 +27,8 @@ Decided in the 2026-09-30 plan: so it reaches the stream state without widening `moq-quic`'s public API. The tokio session, adapters, and trait impl are the `qmux` crate, published from the monorepo under the existing crates.io name as its next breaking - version. moq-dev/web-transport removes its copy; the external dependents - (atmoq-moq-native, web-transport-ws) can move to the new release on their own schedule. + version. The web-transport repo's copy stays published, and its + dependents can move to the new release on their own schedule. Start from the green [kixelated/quinn#2](https://github.com/kixelated/quinn/pull/2) prototype, @@ -63,8 +62,15 @@ newest/oldest group ordering, cancellation, and blocked-stream behavior. The native scheduler does not wait for this dependent proof. Add the missing wire evidence before release: golden vectors for each of the four versions, -bidirectional interoperability against the last web-transport `qmux` release (0.6.x), and -the TypeScript qmux/WebSocket peer used by `js/net`. Preserve rejection of +bidirectional interoperability against the last web-transport `qmux` release (0.6.x, a +test-only dependency), and the in-tree TypeScript qmux peer used by `js/net`. + +Acceptance carried over from the deleted m0 qmux credit quests (web-transport#412 and #413 +are no longer pursued): a session returns connection-level credit for every byte it +receives, whether the app reads it, drops the stream unread, or it arrives after +STOP_SENDING, so a long-lived session never stalls on MAX_DATA; and `close()` delivers its +APPLICATION_CLOSE frame before the transport drops, whenever the transport stays writable +within the close bound. Test both. Preserve rejection of prohibited QUIC frames, params-first setup, record-size validation, close and reset semantics (the first recorded close wins, as close codes #4262 settled), keep-alive behavior, and bounded flow-control tests. @@ -77,7 +83,11 @@ the uring runtime never unifies two copies of the stream state. Decided in the 2026-09-30 audit: moved to m2, since no m1 quest consumes it; the fork plan the same day kept it there. +Decided 2026-10-20 (maintainer): moved to m1. moq stops depending on the +web-transport repo, so the qmux credit and close fixes ship here instead of +as upstream releases and pin bumps; the bug stays on main until this lands. +The release line keeps its current qmux and gets no backport. + ## Related -- [noq#812](https://github.com/n0-computer/noq/issues/812) - the qmux proposal to n0 - [tls:// peer certificates](/quest/m2/tls-listener-mtls.md) - needs a peer-certificate accessor on qmux's TLS session, upstream or in-tree diff --git a/quest/m1/quic/group-reset.md b/quest/m1/quic/group-reset.md index f24cb454b9d..568c43ac43d 100644 --- a/quest/m1/quic/group-reset.md +++ b/quest/m1/quic/group-reset.md @@ -56,5 +56,5 @@ half did not reach. ## Related -- [qmux on the QUIC stream state machine](/quest/m2/quic-qmux.md) - qmux reuses +- [qmux on the QUIC stream state machine](/quest/m1/quic-qmux.md) - qmux reuses `moq-quic`'s reset state diff --git a/quest/m1/resume-closed.md b/quest/m1/resume-closed.md index 572abc8f58d..4196a29b6ff 100644 --- a/quest/m1/resume-closed.md +++ b/quest/m1/resume-closed.md @@ -71,6 +71,10 @@ Regression tests: - A group-only reader of a finite resumed track sees `closed` settle and the route release its copy. +- The same for a fork: a group-only fork of a finite resumed track sees its + `closed` settle and the route release the fork's copy. Forks became logical + readers on the route in [#6024](https://github.com/moq-dev/moq/pull/6024), + so they share the `Reader.#finish` path this quest fixes. - A swap after copy A declared the end keeps `end()` at that end, and `finished()` resolves with it before B learns it. - `largest()` on a finite resumed track resolves `undefined` once its serving diff --git a/quest/m1/rs-request-credit.md b/quest/m1/rs-request-credit.md index 77ba5612e0d..d8631f99fcd 100644 --- a/quest/m1/rs-request-credit.md +++ b/quest/m1/rs-request-credit.md @@ -26,9 +26,10 @@ session trait. Also note that rather than dropping it, so the next request inherits it; the non-waiting open should avoid that. `quic::web` is in-tree and can ask `moq-quic` directly, but the backends `moq-tokio` wraps through `web-transport-trait` (qmux, -iroh) have no credit query there; prefer adding one upstream in -moq-dev/web-transport over leaving them waiting, and say in the PR which -backends fail fast. +iroh) have no credit query there. Decided 2026-10-20 (maintainer): no new +work in moq-dev/web-transport, so qmux gets its query when it moves in-tree +([qmux on moq-quic](/quest/m1/quic-qmux.md)), and iroh, an accepted +exception, keeps waiting. Say in the PR which backends fail fast. Propose the error variant's name in the PR. Public API: one new `Error` variant, and possibly a `transport::poll` session method. Wire: none. diff --git a/quest/m1/test-flakes-2/README.md b/quest/m1/test-flakes-2/README.md index bcf8a0dad5c..240be3e05ab 100644 --- a/quest/m1/test-flakes-2/README.md +++ b/quest/m1/test-flakes-2/README.md @@ -36,12 +36,22 @@ hit it. The same day's test cost audit added the macOS QUIC connects, and the redundant failover drift first: it may hide a TS export bug and failed two of three loaded `just check` runs. +Added 2026-10-20 (maintainer): four more from that day's merge batch, the +draft-16 cancel race first since it fails on a mocked clock and so may hide +an ordering bug. The failed-listen test was meant to join the redial quest, +which #6032 had already finished, so it reuses #6032's reserved-port fix as +its own child. + Public API: none. Wire: none. ## Required - [Redundant failover drift](/quest/m1/test-flakes-2/redundant-failover-drift.md) - a stitching TS export switches back to the surviving importer behind bufferbloat instead of failing on a stalled clock +- [Draft-16 cancel race](/quest/m1/test-flakes-2/draft16-cancel-race.md) - the cancel-ahead-of-the-answer test settles the same way every run, fixing `run_publish_namespaces` if the order is real - [macOS QUIC connects](/quest/m1/test-flakes-2/macos-quic-connect.md) - moq-tokio's loopback QUIC tests stop timing out on the macOS Platform job +- [Relay bench holder](/quest/m1/test-flakes-2/relay-bench-holder.md) - the foreign-listener bench test fails fast when its holder cannot bind, instead of hanging on a fifo +- [moq-uring host clock](/quest/m1/test-flakes-2/uring-host-clock.md) - the host clock test checks the calibration mapping instead of 1 ms of wall-clock slack +- [moq-tokio failed listen](/quest/m1/test-flakes-2/tokio-failed-listen.md) - the failed-listen test keeps its port reserved from first bind to final check ## Related diff --git a/quest/m1/test-flakes-2/draft16-cancel-race.md b/quest/m1/test-flakes-2/draft16-cancel-race.md new file mode 100644 index 00000000000..8e9546ea9e5 --- /dev/null +++ b/quest/m1/test-flakes-2/draft16-cancel-race.md @@ -0,0 +1,28 @@ +# [S] The draft-16 cancel-ahead test settles deterministically + +## Goal + +`ietf::publisher::tests::a_draft_16_cancel_ahead_of_the_answer_ends_only_its_request` +(`rs/moq-net/src/ietf/publisher.rs`) passes every run. It failed 1 of 30 +stressed runs while [#6051](https://github.com/moq-dev/moq/pull/6051) was +landing, although it runs on `moq_clock::test`, so the cause is an ordering +race rather than wall time. + +## Plan + +Seen during the 2026-10-20 merge batch. Ranked first among the new flakes: +with the clock mocked, a nondeterministic result points at task or message +ordering in `run_publish_namespaces`, which may be a product bug. + +- Reproduce in a loop and capture the failing assertion before changing + anything. +- The test queues the answer and PUBLISH_NAMESPACE_CANCEL as one read, then + swaps the route's auth and waits for the namespace to be offered again. Check + whether the cancel can be read before the update is written, or the re-offer + can race the cancel's withdrawal, and what a real draft-16 peer would see in + that order. +- If the order is legal on the wire, the publisher must handle it the same + way every time; fix the code. If only the harness reorders (`settle()` and + `poll_until` interleaving), make the harness deterministic. + +Public API: none. Wire: none. diff --git a/quest/m1/test-flakes-2/relay-bench-holder.md b/quest/m1/test-flakes-2/relay-bench-holder.md new file mode 100644 index 00000000000..35876c9d272 --- /dev/null +++ b/quest/m1/test-flakes-2/relay-bench-holder.md @@ -0,0 +1,24 @@ +# [XS] The relay bench's foreign-listener test fails fast on a port collision + +## Goal + +`bench/relay.test.py`'s `test_a_foreign_listener_never_proves_relay_readiness` +never hangs. Today its `hold.py` binds the port `start_relay` drew at random; +when that port is already taken, `hold.py` dies with EADDRINUSE before writing +the `.ready` fifo, and the harness blocks reading it until the 15 s +`communicate` timeout. + +## Plan + +Seen during the 2026-10-20 merge batch. + +- A holder that cannot bind must reach the harness at once, never by + leaving a reader blocked on a fifo nobody will open. Either `hold.py` binds + a port it can own (the kernel's choice) and reports it, or its failure + writes to `.ready` so `relay_config` sees it, whichever keeps the test + proving that a foreign listener on the relay's port never counts as + readiness. +- Check `bench/relay.sh`'s real `start_relay` for the same shape: a relay + that exits before readiness must end the wait, not time it out. + +Public API: none. Wire: none. diff --git a/quest/m1/test-flakes-2/tokio-failed-listen.md b/quest/m1/test-flakes-2/tokio-failed-listen.md new file mode 100644 index 00000000000..a3fbbcb68ed --- /dev/null +++ b/quest/m1/test-flakes-2/tokio-failed-listen.md @@ -0,0 +1,23 @@ +# [XS] moq-tokio's failed-listen test keeps its port reserved + +## Goal + +`server::tests::a_failed_listen_binds_nothing` (`rs/moq-tokio/src/server.rs`) +passes under load. It binds a probe to port 0, drops it, and has the server +bind that port; another process can take the port in between, so the tcp bind +fails for the wrong reason or the final rebind check does. + +## Plan + +Seen during the 2026-10-20 merge batch. The same probe-then-release pattern +was fixed in the redial tests by +[#6032](https://github.com/moq-dev/moq/pull/6032), which holds the failing +listener for the test's lifetime. + +- Keep the port owned by the test or the server from first bind to final + check, so no other process can take it in between, while still proving the + server releases its partial tcp bind when the unix bind fails. +- Grep moq-tokio and moq-relay for other tests that probe a port and drop it + before use, and fix them the same way. + +Public API: none. Wire: none. diff --git a/quest/m1/test-flakes-2/uring-host-clock.md b/quest/m1/test-flakes-2/uring-host-clock.md new file mode 100644 index 00000000000..d431c5c3120 --- /dev/null +++ b/quest/m1/test-flakes-2/uring-host-clock.md @@ -0,0 +1,22 @@ +# [XS] moq-uring's host clock test stops racing wall time + +## Goal + +`clock::tests::host_tracks_moq_clock` (`rs/moq-uring/src/clock.rs`) passes +under load. It calibrates `Host` and compares it with `moq_clock::now()` +within 1 ms of wall-clock slack, which a preempted thread exceeds. + +## Plan + +Seen during the 2026-10-20 merge batch. + +- Per the epic's rules, never widen the slack. Test the mapping, not the + scheduler: calibrate from a known `(Instant, monotonic)` pair and assert + that `now()` and `monotonic(at)` translate between the two origins exactly, + so nothing depends on how long the thread waits between reads. +- If the calibration itself can be off by a preemption between its two + reads, that error reaches every deadline moq-uring computes; bound it in the + code (read until two samples agree, or take the narrower bracket) rather + than in the test. + +Public API: none. Wire: none. diff --git a/quest/m1/tokio-redirect-loop.md b/quest/m1/tokio-redirect-loop.md new file mode 100644 index 00000000000..944da5adef0 --- /dev/null +++ b/quest/m1/tokio-redirect-loop.md @@ -0,0 +1,41 @@ +# [S] moq-tokio escalates its backoff on GOAWAY redirect loops + +## Goal + +When two relays redirect a Rust client to each other after each session +outlives `backoff.initial`, moq-tokio's reconnect loop escalates its delay up +to `backoff.max` and gives up at `backoff.timeout` with a redirect-loop error, +instead of bouncing between them with no delay forever. A one-way move and a +same-place GOAWAY (an empty URI, a restart) still migrate at once. + +## Plan + +Found 2026-10-20 checking GOAWAY follow-ups of +[#5998](https://github.com/moq-dev/moq/pull/5998). +[#5959](https://github.com/moq-dev/moq/pull/5959) fixes this shape in JS +(`js/net/src/connection/reload.ts`) and lists Rust as unchecked. In +`rs/moq-tokio/src/connection.rs`, a GOAWAY off a session that lived at least +`backoff.initial` continues with no delay, because `Retry::settle` has +already reset the delay, start and last error for that session; only an +immediate redirect escalates. + +- Mirror #5959's rule once it lands: while migrations are chained, remember + the endpoints that redirected the client elsewhere, and route a redirect + back to one of them through the escalating retry without the + healthy-session reset. Clear the set when a session ends without a GOAWAY + or a new sequence starts. Use the same error name as JS. +- Regression tests on a mocked clock, as #5959's: mutual A/B redirects are + bounded and end at `backoff.timeout`; a one-way move and same-place + GOAWAYs redial at once. + +Decided 2026-10-20 (maintainer): no quest reissues individual requests a +GOAWAY interrupts. Same-epoch subscriptions already resume through route +swaps, namespaces re-advertise on the new session, and epochless requests +end, as instances are never stitched. + +Public API: none. Wire: none. Behavior: a redirect loop now backs off and +can end the connection with an error. + +## Related + +- [#5959](https://github.com/moq-dev/moq/pull/5959) - the JS fix this mirrors diff --git a/quest/m1/ts-qmux.md b/quest/m1/ts-qmux.md new file mode 100644 index 00000000000..805eb9fe57e --- /dev/null +++ b/quest/m1/ts-qmux.md @@ -0,0 +1,32 @@ +# [M] @moq/qmux lives in the monorepo + +## Goal + +The TypeScript qmux (`@moq/qmux`, the WebSocket fallback Safari and every +browser without WebTransport uses) is built, tested and published from +`js/` in this repo. `js/net` and `js/p2p` depend on the workspace package, +and nothing in `js/` depends on moq-dev/web-transport. + +## Plan + +Decided 2026-10-20 (maintainer): moq's transports come from this repo, +with `moq-quic` powering qmux and WebTransport. web-transport stays +published but moq takes no dependency on it, except possibly +web-transport-proto. + +- Import the package with its history if cheap, otherwise as a fresh copy + at the version `js/net` pins (`^0.3.3`), and publish the next version from + here under the same npm name so external users keep upgrading. +- `@moq/web-socket-stream`: fold it into `@moq/qmux` unless something + outside qmux needs it on its own (maintainer: no strong view). +- Keep all four wire versions the Rust qmux negotiates. The Rust + [qmux on moq-quic](/quest/m1/quic-qmux.md) interop tests then run against + this in-tree peer instead of a published copy. +- Wire it into `just check`, CI and the release tooling like the other + `js/` packages. + +Public API: same package name and exports; the source moves. Wire: none. + +## Related + +- [qmux on the QUIC stream state machine](/quest/m1/quic-qmux.md) - the Rust side of the same move diff --git a/quest/m2/README.md b/quest/m2/README.md index 45a9b44a520..847f2d90122 100644 --- a/quest/m2/README.md +++ b/quest/m2/README.md @@ -105,8 +105,6 @@ ranked just below deterministic TS legs, which it requires. - [JS-only @moq/net](/quest/m2/rs2ts-js.md) - adopt the selected Rust-to-JS route after browser parity and measured tradeoffs are accepted - [Per-stream deadlines](/quest/m2/quic-deadline.md) - hopeless retransmits become resets, and a tail loss probe fires early while there is still time -- [qmux on the QUIC stream state machine](/quest/m2/quic-qmux.md) - qmux is a - first-class crate over `moq-quic`'s shared stream state machine - [Drop web-transport-trait](/quest/m2/drop-web-transport-trait.md) - every backend implements moq-net's `transport::poll` traits, and the generic `web_transport_trait` adapters are deleted - [Measure ECN on the backbone](/quest/m2/quic-ecn-measure.md) - a written verdict on marking versus dropping, and whether Linode and OVH keep marks @@ -142,7 +140,7 @@ ranked just below deterministic TS legs, which it requires. - [Realtime uplink congestion control](/quest/m2/quic-realtime/README.md) - native publishers pick `Realtime`, a receive-ts delay-based controller that keeps the uplink queue near empty for teleoperation - [Probes resume on the peer](/quest/m2/quic-outage-probe.md) - after an outage, the next probe follows the peer's first packet instead of the backed-off timer - [Keep-alive from the idle timeout](/quest/m2/quic-keep-alive.md) - the keep-alive interval defaults to a fraction of the negotiated idle timeout; an explicit `quic.keep_alive` overrides -- [Socket close](/quest/m2/noq-socket-close.md) - moq-tokio's QUIC endpoint releases its socket on close, so it drops its wrapper +- [Socket close](/quest/m2/quic-socket-close.md) - moq-tokio's QUIC endpoint releases its socket on close, so it drops its wrapper - [TS health stats](/quest/m2/ts-health-stats.md) - the TS counters ride the stats plumbing beside the media counters - [Windows.Graphics.Capture](/quest/m2/capture-wgc.md) - the WGC display and window backend verified on real Windows hardware - [Windows capture parity](/quest/m2/capture-windows.md) - system audio and a settled app-capture policy diff --git a/quest/m2/drop-web-transport-trait.md b/quest/m2/drop-web-transport-trait.md index 2ecd247b489..d948ca614bf 100644 --- a/quest/m2/drop-web-transport-trait.md +++ b/quest/m2/drop-web-transport-trait.md @@ -49,18 +49,23 @@ each gets its own impl: `#[doc(hidden)]`. Decide whether they are API or should be private. Notes from related quests: -- [qmux on moq-quic](/quest/m2/quic-qmux.md) planned to port qmux's +- [qmux on moq-quic](/quest/m1/quic-qmux.md) planned to port qmux's `web-transport-trait` impl. If it lands first, its impl targets moq-net's `transport::poll` instead. - [poll_acked](/quest/m2/quic-ack-hook.md) already puts its hook on moq-net's trait because moq-net has no `web-transport-trait` dependency. That holds here. -- [Rust request credit](/quest/m1/rs-request-credit.md) prefers adding a - credit query to qmux and iroh upstream in moq-dev/web-transport. Once this - lands, that query belongs on each backend's own moq-net impl. +- [Rust request credit](/quest/m1/rs-request-credit.md) gets qmux's credit + query from the in-tree qmux; iroh keeps waiting. Once this lands, that + query belongs on each backend's own moq-net impl. + +Decided 2026-10-20 (maintainer): moq takes no new dependency on, or work in, +moq-dev/web-transport; `moq-quic` powers qmux and WebTransport. The iroh +(`web-transport-iroh`) and wasm (`web-transport-wasm`) backends stay crates +from that repo as accepted exceptions, and web-transport-proto may stay. ## Related -- [qmux on the QUIC stream state machine](/quest/m2/quic-qmux.md) - the in-tree qmux implements the moq-net traits directly +- [qmux on the QUIC stream state machine](/quest/m1/quic-qmux.md) - the in-tree qmux implements the moq-net traits directly - [poll_acked on moq-net's send stream](/quest/m2/quic-ack-hook.md) - another hook that lives only on moq-net's traits - [Rust requests fail fast without stream credit](/quest/m1/rs-request-credit.md) - its credit query moves to each backend's moq-net impl diff --git a/quest/m2/probe/native.md b/quest/m2/probe/native.md index 0634a731d14..9f2580b8252 100644 --- a/quest/m2/probe/native.md +++ b/quest/m2/probe/native.md @@ -64,5 +64,3 @@ to 17, so sessions on those drafts would keep `Report`. - [Per-stream deadlines](/quest/m2/quic-deadline.md) - decides when in-flight data has expired - [BBR media study](/quest/m2/quic-bbr-natural-drain.md) - separate ProbeRTT policy experiment -- [noq#811](https://github.com/n0-computer/noq/issues/811) - probing while app-limited, proposed to n0 -- [noq#476](https://github.com/n0-computer/noq/issues/476) - the earlier bitrate-probing ask diff --git a/quest/m2/quic-ack-hook.md b/quest/m2/quic-ack-hook.md index 9ca0cba8a9a..29ff9a8e4f0 100644 --- a/quest/m2/quic-ack-hook.md +++ b/quest/m2/quic-ack-hook.md @@ -66,5 +66,5 @@ Decided in the 2026-09-30 audit: moved to m2 with its only consumer, - [Starvation at frame granularity](/quest/m2/starvation-frames.md) - the moq-net consumer -- [qmux on the QUIC stream state machine](/quest/m2/quic-qmux.md) - decides +- [qmux on the QUIC stream state machine](/quest/m1/quic-qmux.md) - decides what acknowledgment means over a reliable transport diff --git a/quest/m2/quic-deadline.md b/quest/m2/quic-deadline.md index 9bae16af5ee..9222b1cd289 100644 --- a/quest/m2/quic-deadline.md +++ b/quest/m2/quic-deadline.md @@ -62,4 +62,3 @@ Decided in the 2026-09-30 audit: moved to m2. No m1 quest consumes it. - [ACK hook](/quest/m2/quic-ack-hook.md) - the same pattern for a moq-net method backed by `moq-quic` - [Native probing](/quest/m2/probe/native.md) - retransmits in-flight data to probe, and must not resend what a deadline has expired -- [noq#813](https://github.com/n0-computer/noq/issues/813) - the per-stream deadline proposal to n0 diff --git a/quest/m2/quic-keep-alive.md b/quest/m2/quic-keep-alive.md index 76203cdc47e..1596200671a 100644 --- a/quest/m2/quic-keep-alive.md +++ b/quest/m2/quic-keep-alive.md @@ -32,7 +32,3 @@ mechanism and stays as is. Tests: the derived interval follows a changed idle timeout, including a peer that advertises a shorter one; an idle connection survives with the default; an explicit value wins; `0s` disables. - -## Related - -- [noq#810](https://github.com/n0-computer/noq/issues/810) - the deadline-driven proposal to n0; flub and matheus23 asked to keep a cap for NAT bindings, which the override covers diff --git a/quest/m2/noq-socket-close.md b/quest/m2/quic-socket-close.md similarity index 100% rename from quest/m2/noq-socket-close.md rename to quest/m2/quic-socket-close.md diff --git a/quest/m2/tls-listener-mtls.md b/quest/m2/tls-listener-mtls.md index 9c5ff1cccb4..6279ef548ee 100644 --- a/quest/m2/tls-listener-mtls.md +++ b/quest/m2/tls-listener-mtls.md @@ -40,4 +40,4 @@ Public API: relay config may gain a listener option. Wire: none. ## Related - [TCP acceptor](/quest/m2/one-port/tcp-demux.md) - carries `tls://` on the shared TCP port, where the certificate request stays on the moq ALPN -- [qmux on the QUIC stream state machine](/quest/m2/quic-qmux.md) - moves qmux in-tree; if it lands first, the peer-certificate accessor lands here instead of upstream +- [qmux on the QUIC stream state machine](/quest/m1/quic-qmux.md) - moves qmux in-tree; if it lands first, the peer-certificate accessor lands here instead of upstream diff --git a/quest/m2/wire-stats/qmux.md b/quest/m2/wire-stats/qmux.md index d5507f67589..0948e874199 100644 --- a/quest/m2/wire-stats/qmux.md +++ b/quest/m2/wire-stats/qmux.md @@ -13,17 +13,21 @@ beside the field. ## Plan -- moq-dev/web-transport: qmux's `TcpStats` already reads `TCP_INFO` for rtt - and delivery rate. Add `tcpi_bytes_sent` and `tcpi_bytes_received` from - the same read. A platform without them (non-Linux, or Linux before 4.19) - reports `None`, which moq-net maps to 0. Upstream source work: - moq-dev/web-transport#435 and #436. +- The in-tree qmux's `TcpStats` (ported from moq-dev/web-transport, which + already reads `TCP_INFO` for rtt and delivery rate): add `tcpi_bytes_sent` + and `tcpi_bytes_received` from the same read. A platform without them + (non-Linux, or Linux before 4.19) reports `None`, which moq-net maps to 0. + Decided 2026-10-20 (maintainer): moq-dev/web-transport#435 and #436 are not + pursued; this lands in-tree after qmux moves. - The transport pushes its counters: the stats tick never polls a session, and there is no per-connection timer. `TCP_INFO` is read on demand, so decide how qmux's I/O path refreshes the `Wire` (for example, after each socket write batch) without a syscall per write, then return it from moq-tokio's `Grouped::wire` for `qmux::Session`, including the relay's `SocketStats` wiring (`rs/moq-relay/src/websocket.rs`). -- Release qmux on every line moq builds against, then bump it here. - Test: a loopback WebSocket session's `sent_bytes` covers at least the payload it wrote. + +## Required + +- [qmux on the QUIC stream state machine](/quest/m1/quic-qmux.md) - qmux lives in-tree, where its `TcpStats` can grow the byte counters diff --git a/quest/m3/quic-careful-resume.md b/quest/m3/quic-careful-resume.md index a99abf4ff4b..6e7c77c0b1e 100644 --- a/quest/m3/quic-careful-resume.md +++ b/quest/m3/quic-careful-resume.md @@ -35,7 +35,3 @@ loss-based traffic when BBR alone does not fill the queue. Random loss may prove fallback correctness, but never grades the jump's performance. Ship it on by default only when the jump never makes the first second worse than slow start. - -## Related - -- [noq#815](https://github.com/n0-computer/noq/issues/815) - the careful-resume proposal to n0 diff --git a/quest/m3/quic-ecn.md b/quest/m3/quic-ecn.md index 405a7981f3e..3584a03af89 100644 --- a/quest/m3/quic-ecn.md +++ b/quest/m3/quic-ecn.md @@ -39,7 +39,3 @@ both `off` and `ect1` need a `moq-quic` change. - [Measure ECN on the backbone](/quest/m2/quic-ecn-measure.md) - the provider verdict this quest acts on - -## Related - -- [noq#814](https://github.com/n0-computer/noq/issues/814) - the L4S proposal to n0 From 7113184740e8bdff29be6977c71b8d6f15eec709 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 20 Oct 2026 11:52:32 -0700 Subject: [PATCH 2/6] quest: drop the fixed uring clock flake, order TS qmux first, record the fold's break Co-Authored-By: Claude Opus 5.5 --- quest/m1/README.md | 2 +- quest/m1/quic-qmux.md | 4 ++++ quest/m1/test-flakes-2/README.md | 6 +++--- quest/m1/test-flakes-2/uring-host-clock.md | 22 ---------------------- quest/m1/ts-qmux.md | 9 +++++++-- 5 files changed, 15 insertions(+), 28 deletions(-) delete mode 100644 quest/m1/test-flakes-2/uring-host-clock.md diff --git a/quest/m1/README.md b/quest/m1/README.md index 4567b4f6e70..4fa9f6862f0 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -56,6 +56,7 @@ lands before the cut. - [Routing guide](/quest/m1/routing-guide.md) - concepts and practical outbound federation setup - [Controlled time](/quest/m1/time/README.md) - every crate and package reads time through moq-clock or @moq/clock, with regression tests for the changed contracts - [Own the QUIC stack](/quest/m1/quic/README.md) - quinn hard-forked in-tree as `moq-quic`, carrying BBR, reliable reset, hierarchical scheduling, peer limits, and endpoint sharding +- [TS qmux in the monorepo](/quest/m1/ts-qmux.md) - `@moq/qmux` moves into `js/` and `js/net` stops depending on moq-dev/web-transport - [qmux on the QUIC stream state machine](/quest/m1/quic-qmux.md) - qmux runs in-tree over `moq-quic`'s stream state machine, returning credit for every byte and delivering its close frame - [Drop moq-native's noq feature](/quest/m1/native-noq-feature.md) - the empty default `noq` feature is removed - [Finalize moq-lite-07](/quest/m1/lite07-finalize.md) - when the maintainer cuts it, lite-07 negotiates as `moq-lite-07` and the next release ships it @@ -97,7 +98,6 @@ lands before the cut. - [Container reads drop backfill](/quest/m1/read-backfill.md) - the JS container consumer drops groups below its read cursor like Rust - [Rust request credit](/quest/m1/rs-request-credit.md) - a moq-net request fails at once without stream credit instead of waiting - [A stalled subscriber holds bounded state](/quest/m1/egress-held-state.md) - group machines and bytes a credit-withholding downstream makes a publisher hold plateau at what its credit and the latency budget allow, on lite and moq-transport -- [TS qmux in the monorepo](/quest/m1/ts-qmux.md) - `@moq/qmux` moves into `js/` and `js/net` stops depending on moq-dev/web-transport - [qmux no-wait opens](/quest/m1/qmux-no-wait.md) - the in-tree @moq/qmux rejects an over-limit create when waitUntilAvailable is false, like Chrome - [Timed-out requests clean up](/quest/m1/legacy-request-timeout-cleanup.md) - on drafts 14-16, Rust and JS send PUBLISH_NAMESPACE_DONE or UNSUBSCRIBE_NAMESPACE for a timed-out request before its local reset - [TS fidelity and recovery](/quest/m1/ts-reliability/README.md) - both modes have explicit profiles, automated validation, loss tests, and a dedicated guide diff --git a/quest/m1/quic-qmux.md b/quest/m1/quic-qmux.md index 835234e7b72..d46cbff4e13 100644 --- a/quest/m1/quic-qmux.md +++ b/quest/m1/quic-qmux.md @@ -88,6 +88,10 @@ web-transport repo, so the qmux credit and close fixes ship here instead of as upstream releases and pin bumps; the bug stays on main until this lands. The release line keeps its current qmux and gets no backport. +## Required + +- [TS qmux](/quest/m1/ts-qmux.md) - the in-tree TypeScript peer this quest's interop acceptance runs against + ## Related - [tls:// peer certificates](/quest/m2/tls-listener-mtls.md) - needs a peer-certificate accessor on qmux's TLS session, upstream or in-tree diff --git a/quest/m1/test-flakes-2/README.md b/quest/m1/test-flakes-2/README.md index 240be3e05ab..bc674a9535d 100644 --- a/quest/m1/test-flakes-2/README.md +++ b/quest/m1/test-flakes-2/README.md @@ -36,11 +36,12 @@ hit it. The same day's test cost audit added the macOS QUIC connects, and the redundant failover drift first: it may hide a TS export bug and failed two of three loaded `just check` runs. -Added 2026-10-20 (maintainer): four more from that day's merge batch, the +Added 2026-10-20 (maintainer): three more from that day's merge batch, the draft-16 cancel race first since it fails on a mocked clock and so may hide an ordering bug. The failed-listen test was meant to join the redial quest, which #6032 had already finished, so it reuses #6032's reserved-port fix as -its own child. +its own child. moq-uring's `host_tracks_moq_clock` was seen the same day +but #6056 had already fixed it by measuring the calibration gap. Public API: none. Wire: none. @@ -50,7 +51,6 @@ Public API: none. Wire: none. - [Draft-16 cancel race](/quest/m1/test-flakes-2/draft16-cancel-race.md) - the cancel-ahead-of-the-answer test settles the same way every run, fixing `run_publish_namespaces` if the order is real - [macOS QUIC connects](/quest/m1/test-flakes-2/macos-quic-connect.md) - moq-tokio's loopback QUIC tests stop timing out on the macOS Platform job - [Relay bench holder](/quest/m1/test-flakes-2/relay-bench-holder.md) - the foreign-listener bench test fails fast when its holder cannot bind, instead of hanging on a fifo -- [moq-uring host clock](/quest/m1/test-flakes-2/uring-host-clock.md) - the host clock test checks the calibration mapping instead of 1 ms of wall-clock slack - [moq-tokio failed listen](/quest/m1/test-flakes-2/tokio-failed-listen.md) - the failed-listen test keeps its port reserved from first bind to final check ## Related diff --git a/quest/m1/test-flakes-2/uring-host-clock.md b/quest/m1/test-flakes-2/uring-host-clock.md deleted file mode 100644 index d431c5c3120..00000000000 --- a/quest/m1/test-flakes-2/uring-host-clock.md +++ /dev/null @@ -1,22 +0,0 @@ -# [XS] moq-uring's host clock test stops racing wall time - -## Goal - -`clock::tests::host_tracks_moq_clock` (`rs/moq-uring/src/clock.rs`) passes -under load. It calibrates `Host` and compares it with `moq_clock::now()` -within 1 ms of wall-clock slack, which a preempted thread exceeds. - -## Plan - -Seen during the 2026-10-20 merge batch. - -- Per the epic's rules, never widen the slack. Test the mapping, not the - scheduler: calibrate from a known `(Instant, monotonic)` pair and assert - that `now()` and `monotonic(at)` translate between the two origins exactly, - so nothing depends on how long the thread waits between reads. -- If the calibration itself can be off by a preemption between its two - reads, that error reaches every deadline moq-uring computes; bound it in the - code (read until two samples agree, or take the narrower bracket) rather - than in the test. - -Public API: none. Wire: none. diff --git a/quest/m1/ts-qmux.md b/quest/m1/ts-qmux.md index 805eb9fe57e..bc260b8b6cd 100644 --- a/quest/m1/ts-qmux.md +++ b/quest/m1/ts-qmux.md @@ -18,14 +18,19 @@ web-transport-proto. at the version `js/net` pins (`^0.3.3`), and publish the next version from here under the same npm name so external users keep upgrading. - `@moq/web-socket-stream`: fold it into `@moq/qmux` unless something - outside qmux needs it on its own (maintainer: no strong view). + outside qmux needs it on its own (maintainer: no strong view). Folding + ends that package for any direct consumer, a breaking change: deprecate + it on npm with a pointer to `@moq/qmux` and add a `## Unreleased` entry to + `@moq/qmux`'s `MIGRATE.md`. No re-export shim. - Keep all four wire versions the Rust qmux negotiates. The Rust [qmux on moq-quic](/quest/m1/quic-qmux.md) interop tests then run against this in-tree peer instead of a published copy. - Wire it into `just check`, CI and the release tooling like the other `js/` packages. -Public API: same package name and exports; the source moves. Wire: none. +Public API: `@moq/qmux` keeps its name and exports; folding +`@moq/web-socket-stream` in removes that package (breaking, see above). +Wire: none. ## Related From a34ae1a026bee4fdd816251f15f8064713698046 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 20 Oct 2026 13:10:10 -0700 Subject: [PATCH 3/6] quest: place qmux in m1 beside the QUIC epic and assign its reliable reset Co-Authored-By: Claude Opus 5.5 --- quest/m1/quic-qmux.md | 5 ++++- quest/m1/quic/README.md | 13 ++++++++++--- quest/m1/quic/group-reset.md | 7 +++++-- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/quest/m1/quic-qmux.md b/quest/m1/quic-qmux.md index d46cbff4e13..7fb3249716a 100644 --- a/quest/m1/quic-qmux.md +++ b/quest/m1/quic-qmux.md @@ -51,7 +51,10 @@ qmux runs over a reliable ordered transport, serialization acknowledges the committed prefix immediately, but the receiver must still delay the reset until that prefix is available. Remove the qmux prototype's local `RESET_STREAM_AT` state once the shared core owns it (moved here from -reliable reset in the 2026-10-05 audit). +reliable reset in the 2026-10-05 audit). If +[A reset group keeps its header](/quest/m1/quic/group-reset.md) landed first, +the session's `transport::poll::SendStream` implements its offset reset +instead of reporting it unsupported. Carry the hierarchical send groups from the scheduler quest into qmux's record writer. Qmux over TCP, TLS, WebSocket, Unix sockets, and in-memory diff --git a/quest/m1/quic/README.md b/quest/m1/quic/README.md index e3bbaf9a0a6..2653e21df5d 100644 --- a/quest/m1/quic/README.md +++ b/quest/m1/quic/README.md @@ -9,9 +9,11 @@ MoQ never negotiates. One core serves the tokio backend, the thread-per-core BBR correctness, reliable stream resets, hierarchical stream scheduling with per-broadcast fairness, bounded incomplete-message memory, wider limits for relay peers, and endpoint sharding. -Per-stream acknowledgment progress, per-stream deadlines, qmux on the shared -stream state machine, and the experiments (the egress profile, ECN -measurement, deadline keep-alive) live in [m2](/quest/m2/README.md); media +qmux on the shared stream state machine is its own m1 quest, +[quic-qmux](/quest/m1/quic-qmux.md), outside this epic. +Per-stream acknowledgment progress, per-stream deadlines, and the experiments +(the egress profile, ECN measurement, deadline keep-alive) live in +[m2](/quest/m2/README.md); media probing is its own [m2 epic](/quest/m2/probe/README.md); receive timestamps and a delay-based uplink controller are the [m2 realtime epic](/quest/m2/quic-realtime/README.md); L4S and careful resume @@ -52,6 +54,9 @@ Decided in the 2026-09-30 audit: deadlines, qmux, BBR loss parity, ECN measurement, ACK progress, and the ACK hook moved to m2, since no m1 quest consumes them. Decided 2026-10-08: L4S and careful resume moved on to m3, and GCC shrank to a receive-timestamps spike, since none has a consumer. +Decided 2026-10-20 (maintainer): qmux moved back to m1 as a milestone +child, because moq stops depending on the web-transport repo and its credit +and close fixes now ship in-tree. Decided in the 2026-10-10 receive-memory plan: native message assembly holds connection credit until complete-unit handoff, while stream credit advances @@ -70,6 +75,8 @@ cache limits remain separate; bitrate pacing composes with this memory cap. ## Related +- [qmux on the QUIC stream state machine](/quest/m1/quic-qmux.md) - the m1 + qmux port onto `moq-quic`'s stream state - [Scope track priority](/quest/m1/track-priority-scope.md) - the per-broadcast fairness policy on cluster sessions - [Native probing](/quest/m2/probe/native.md) - moq-quic retransmits in-flight data during ProbeBW_UP to probe up to a session's target diff --git a/quest/m1/quic/group-reset.md b/quest/m1/quic/group-reset.md index 568c43ac43d..1c106556da9 100644 --- a/quest/m1/quic/group-reset.md +++ b/quest/m1/quic/group-reset.md @@ -33,8 +33,11 @@ unable to keep the header unless it does. 2026-10-17 on #5817: a byte offset rather than a `Checkpoint` handle; an offset past the written data panics instead of being unrepresentable. Implement it in the moq-tokio and - moq-uring adapters. Backends that cannot do it (browser, qmux, iroh) report - that rather than silently sending a plain reset. + moq-uring adapters. Backends that cannot do it (browser, iroh, and qmux + until it runs on `moq-quic`) report that rather than silently sending a + plain reset. Whichever of this quest and + [quic-qmux](/quest/m1/quic-qmux.md) lands second implements the reset on + qmux's session through the shared `RESET_STREAM_AT` state. - The lite publisher checkpoints right after writing a group header and resets the group from that checkpoint. - QUIC negotiation alone cannot drop the grace. A peer's stack can advertise From ba9f6c9ba66dbfab48cc014dc80afc340cccdded Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 20 Oct 2026 13:16:10 -0700 Subject: [PATCH 4/6] quest: keep the failed-listen test on its partial-bind path Co-Authored-By: Claude Opus 5.5 --- quest/m1/test-flakes-2/tokio-failed-listen.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/quest/m1/test-flakes-2/tokio-failed-listen.md b/quest/m1/test-flakes-2/tokio-failed-listen.md index a3fbbcb68ed..fad856e474e 100644 --- a/quest/m1/test-flakes-2/tokio-failed-listen.md +++ b/quest/m1/test-flakes-2/tokio-failed-listen.md @@ -12,12 +12,18 @@ fails for the wrong reason or the final rebind check does. Seen during the 2026-10-20 merge batch. The same probe-then-release pattern was fixed in the redial tests by [#6032](https://github.com/moq-dev/moq/pull/6032), which holds the failing -listener for the test's lifetime. +listener for the test's lifetime. That fix does not transfer as is: a held +probe makes the server's tcp bind fail first, so the test would never reach +the tcp-success, unix-failure path it exists to cover. - Keep the port owned by the test or the server from first bind to final check, so no other process can take it in between, while still proving the - server releases its partial tcp bind when the unix bind fails. + server releases its partial tcp bind when the unix bind fails. The + reservation has to be one the server takes over, not one the test keeps + holding. - Grep moq-tokio and moq-relay for other tests that probe a port and drop it - before use, and fix them the same way. + before use, and fix each with whichever shape fits: held, as in #6032, when + the port is meant to stay unusable, or handed over when the code under test + must bind it. Public API: none. Wire: none. From fa002f144994d29811cd39d53996bd5bc98d10ba Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 20 Oct 2026 13:40:19 -0700 Subject: [PATCH 5/6] quest: scope request credit to in-tree backends and put the mTLS accessor in-tree Co-Authored-By: Claude Opus 5.5 --- quest/m1/quic-qmux.md | 2 +- quest/m1/rs-request-credit.md | 4 +++- quest/m2/tls-listener-mtls.md | 12 ++++++++---- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/quest/m1/quic-qmux.md b/quest/m1/quic-qmux.md index 7fb3249716a..d20882aad23 100644 --- a/quest/m1/quic-qmux.md +++ b/quest/m1/quic-qmux.md @@ -97,4 +97,4 @@ The release line keeps its current qmux and gets no backport. ## Related -- [tls:// peer certificates](/quest/m2/tls-listener-mtls.md) - needs a peer-certificate accessor on qmux's TLS session, upstream or in-tree +- [tls:// peer certificates](/quest/m2/tls-listener-mtls.md) - adds a peer-certificate accessor to the in-tree qmux TLS session diff --git a/quest/m1/rs-request-credit.md b/quest/m1/rs-request-credit.md index d8631f99fcd..1567d6a158b 100644 --- a/quest/m1/rs-request-credit.md +++ b/quest/m1/rs-request-credit.md @@ -4,7 +4,9 @@ A moq-net request (subscribe, track info, fetch, announce interest; lite and moq-transport) whose stream open finds no stream credit fails at once with a -local error, instead of waiting for the peer to grant more. A relay resetting a +local error, instead of waiting for the peer to grant more. This holds on +every in-tree backend; iroh, an accepted exception that stays on upstream +noq, keeps waiting. A relay resetting a downstream stream because of it sends `Internal`. Publisher group streams are out of scope. diff --git a/quest/m2/tls-listener-mtls.md b/quest/m2/tls-listener-mtls.md index 6279ef548ee..796cf5506e4 100644 --- a/quest/m2/tls-listener-mtls.md +++ b/quest/m2/tls-listener-mtls.md @@ -16,9 +16,10 @@ config. The real gap is getting the peer certificate out afterwards: without it. Decided by the maintainer (2026-10-06): the peer certificate comes from a -new accessor on qmux's TLS session upstream (qmux is in our org), then a -dependency bump here. Rejected: owning the TLS accept in this repository, or -leaving it open. +new accessor on qmux's TLS session. Rejected: owning the TLS accept outside +qmux, or leaving it open. Decided 2026-10-20 (maintainer): no new work in +moq-dev/web-transport, so the accessor goes on the in-tree qmux crate that +[qmux on moq-quic](/quest/m1/quic-qmux.md) brings into this repository. Keep the token path working on the same listener, and keep pinned peers and CA roots meaning the same as on QUIC. @@ -37,7 +38,10 @@ keeps both working. Public API: relay config may gain a listener option. Wire: none. +## Required + +- [qmux on the QUIC stream state machine](/quest/m1/quic-qmux.md) - moves qmux in-tree, where the peer-certificate accessor lands + ## Related - [TCP acceptor](/quest/m2/one-port/tcp-demux.md) - carries `tls://` on the shared TCP port, where the certificate request stays on the moq ALPN -- [qmux on the QUIC stream state machine](/quest/m1/quic-qmux.md) - moves qmux in-tree; if it lands first, the peer-certificate accessor lands here instead of upstream From 5098ccc4f24e82011c4fcc2f59f4e198f8c398a2 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 20 Oct 2026 13:47:39 -0700 Subject: [PATCH 6/6] quest: assign qmux's non-waiting open to whichever quest lands second Co-Authored-By: Claude Opus 5.5 --- quest/m1/quic-qmux.md | 5 ++++- quest/m1/rs-request-credit.md | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/quest/m1/quic-qmux.md b/quest/m1/quic-qmux.md index 1c5dbdb1bdf..a8a9616c837 100644 --- a/quest/m1/quic-qmux.md +++ b/quest/m1/quic-qmux.md @@ -70,7 +70,10 @@ are no longer pursued): a session returns connection-level credit for every byte receives, whether the app reads it, drops the stream unread, or it arrives after STOP_SENDING, so a long-lived session never stalls on MAX_DATA; and `close()` delivers its APPLICATION_CLOSE frame before the transport drops, whenever the transport stays writable -within the close bound. Test both. Preserve rejection of +within the close bound. Test both. If +[Rust request credit](/quest/m1/rs-request-credit.md) landed first, the session +also implements its non-waiting open, so a request without stream credit fails +at once on qmux too. Preserve rejection of prohibited QUIC frames, params-first setup, record-size validation, close and reset semantics (the first recorded close wins, as close codes #4262 settled), keep-alive behavior, and bounded flow-control tests. diff --git a/quest/m1/rs-request-credit.md b/quest/m1/rs-request-credit.md index 1567d6a158b..9776c235a4f 100644 --- a/quest/m1/rs-request-credit.md +++ b/quest/m1/rs-request-credit.md @@ -31,7 +31,9 @@ but the backends `moq-tokio` wraps through `web-transport-trait` (qmux, iroh) have no credit query there. Decided 2026-10-20 (maintainer): no new work in moq-dev/web-transport, so qmux gets its query when it moves in-tree ([qmux on moq-quic](/quest/m1/quic-qmux.md)), and iroh, an accepted -exception, keeps waiting. Say in the PR which backends fail fast. +exception, keeps waiting. Whichever of this quest and quic-qmux lands second +gives the in-tree qmux session the non-waiting open, so this quest does not +wait on the port. Say in the PR which backends fail fast. Propose the error variant's name in the PR. Public API: one new `Error` variant, and possibly a `transport::poll` session method. Wire: none.