diff --git a/quest/m0/README.md b/quest/m0/README.md index e24848a1738..d7357a032bf 100644 --- a/quest/m0/README.md +++ b/quest/m0/README.md @@ -86,4 +86,5 @@ have fixes on `main`: macOS tests (#5335), drill-sensitivity (#5338), loom - [web-transport releases the qmux fixes](/quest/m0/qmux-credit-upstream.md) - waiting on moq-dev/web-transport#412 and #413 to merge and ship, which qmux credit bumps to - [qmux credit](/quest/m0/qmux-credit.md) - qmux returns connection credit for dropped and stopped streams and delivers its close frame, on both lines - [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 +- [Keep the winning transport](/quest/m0/drop-ws-upgrade.md) - a connection stays on the transport that won the race; the WebSocket-to-QUIC upgrade and its epochless `Restart` are removed - [Parked drain](/quest/m0/parked-drain.md) - draining parked groups after a cap rise, as a same-epoch relay handover causes, is near-linear in the backlog, not quadratic diff --git a/quest/m0/drop-ws-upgrade.md b/quest/m0/drop-ws-upgrade.md new file mode 100644 index 00000000000..73cab4f403f --- /dev/null +++ b/quest/m0/drop-ws-upgrade.md @@ -0,0 +1,60 @@ +# [M] A connection keeps the transport that won the race + +## Goal + +A `moq-tokio` connection whose `https://` race is won by WebSocket stays on +WebSocket for that session's lifetime; nothing migrates it to QUIC. The +pending QUIC dial is cancelled the moment WebSocket wins, as when QUIC wins. +An epochless viewer no longer sees a `Restart` shortly after connecting. + +Non-goals: changing the race itself, its head start, or the "WebSocket won" +memo's effect on later connects. + +## Plan + +Decided with the maintainer on 2026-10-13: remove the WebSocket-to-QUIC +upgrade from #4180. On an epochless route, which is the default for +lite-06, the browser, and the bindings, the upgrade is a `Restart`: players +re-resolve, re-fetch the catalog, and reset decoders right after connecting. +That is a brutal user experience, and nothing shows the upgrade helps outside +CI's debug builds. Rejected: upgrading only when epochs match (keeps the +machinery for a minority of sessions), keeping it as is, and measuring first. + +- Delete the upgrade in `rs/moq-tokio` (`connection.rs`'s `upgrade` state and + its handover, `client.rs`'s post-race `Setup` stage) and its tests + (`websocket_upgrades_to_quic`, `a_refused_upgrade_keeps_websocket`). +- Cancel the pending QUIC dial at once when WebSocket wins, deleting #5284's + closed fallback too: a WebSocket session that closes early goes through the + ordinary reconnect, which races again. +- Keep the "WebSocket won" memo (`WEBSOCKET_WON`). Both transports still race + without the head start, so QUIC can still win a later connect. Its only + eviction was a successful upgrade; check what clears it now and document it. +- Remove `moq_net::Session::setup()` unless the GOAWAY drain migration gates + on it; if it does, keep it and say why in its doc. Keep + `Connection::transport()`, which reports the live transport. +- Keep the origin fix from #5443 (`Release` in `model/origin.rs`): GOAWAY + drains and publisher replacement still hand an epochless route over the + same way. +- `js/net` keeps closing the loser as today. This plan's PR deleted the m1 + transport-upgrade epic and its JavaScript child, and folded the QUIC-wins + race flake in below. +- Add a regression: when WebSocket wins, the QUIC dial is dropped and no + second session ever starts, with mocked time. +- Absorbs the rest of the QUIC-wins race flake: `quic_winning_opens_one_session` + already holds the fallback handshake, but `fallback.accepted.try_recv()` + only proves the queue is empty at one instant. Observe the loser's + cancellation directly, by a drop or connection-close signal, in both + directions. +- Update docs and comments that describe the upgrade inline, and search the + bindings for the `WebTransport` transport report #4180 added; it stays. + +Public API: breaking in moq-net (`Session::setup()` removed, unless kept as +above). Behavior: no QUIC upgrade after a WebSocket win. Wire: none. Report +both in the PR. + +## Related + +- [#4180](https://github.com/moq-dev/moq/pull/4180) - the upgrade this removes +- [#5284](https://github.com/moq-dev/moq/pull/5284) - the closed fallback this removes +- [#5443](https://github.com/moq-dev/moq/pull/5443) - the origin fix that stays +- [Restart followers](/quest/m1/restart-followers.md) - consumers that follow the `Restart` GOAWAY drains and publisher replacement still send diff --git a/quest/m1/README.md b/quest/m1/README.md index df22c091740..3b6b718492e 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -133,7 +133,6 @@ above them. - [Typed refusal reason](/quest/m1/refusal-reason.md) - an auth server's 403 names the reason (including `expired`) and the root and tier, so the relay's session outcomes count and attribute refusals - [JS track handover](/quest/m1/js-group-handover.md) - a JS track subscription resumes across a route swap from the first frame it lacks, so `test/drain` passes at zero latency budget - [Drain web connections from accept](/quest/m1/drain-web-accept.md) - a relay drain never exits under an HTTP/1.1 WebSocket client still in TLS or header parsing -- [Transport upgrade](/quest/m1/transport-upgrade/README.md) - a session that came up over WebSocket moves to QUIC once the QUIC dial lands, handing over without dropping a group - [Scope track priority](/quest/m1/track-priority-scope.md) - priority orders one owner's streams, and a shared cluster session is fair across tenants - [io_uring CI](/quest/m1/uring-ci.md) - PRs touching io_uring code run `just rs uring` on a 6.12+ kernel runner - [Audio jitter target](/quest/m1/audio-jitter-target/README.md) - the playout target is default in both languages; a manual browser proof and a native trace replay remain diff --git a/quest/m1/js-group-handover.md b/quest/m1/js-group-handover.md index 7ff59c9dcbe..f833f4ada5e 100644 --- a/quest/m1/js-group-handover.md +++ b/quest/m1/js-group-handover.md @@ -43,9 +43,7 @@ Things to settle along the way: - Resume only between routes with the same epoch. Any other swap is a new broadcast that announce consumers see as a `Restart`, and players follow it with a fresh subscription. A route without an epoch never resumes, as - in Rust, so `drain.ts`'s publisher mints an epoch, and the - [transport upgrade](/quest/m1/transport-upgrade/js.md) test publishes - under one too. + in Rust, so `drain.ts`'s publisher mints an epoch. - `js/watch` and `js/hang` consumers that re-subscribe on `active` changes. Check whether they still need to. - Giving up a resumed group no route continues. Mirror Rust's rule from diff --git a/quest/m1/restart-followers.md b/quest/m1/restart-followers.md index a3d6803b766..f365d8b50ee 100644 --- a/quest/m1/restart-followers.md +++ b/quest/m1/restart-followers.md @@ -59,5 +59,5 @@ Wire: none. ## Related -- [Transport upgrade](/quest/m1/transport-upgrade/README.md) - its "epochless routes signal Restart and need a fresh follower resolution" decision +- [Keep the winning transport](/quest/m0/drop-ws-upgrade.md) - removes the upgrade, so `Restart` no longer follows every WebSocket connect - [Broadcast epochs](/quest/m0/broadcast-epoch/README.md) - the players that already follow `Restart` diff --git a/quest/m1/test-flakes-2/README.md b/quest/m1/test-flakes-2/README.md index 5f8ee18eeb5..74eea9ef3ac 100644 --- a/quest/m1/test-flakes-2/README.md +++ b/quest/m1/test-flakes-2/README.md @@ -33,7 +33,6 @@ Public API: none. Wire: none. ## Required - [Interop close code](/quest/m1/test-flakes-2/interop-close-code.md) - the browser close-code test reliably sees `unauthorized` for a refused session -- [QUIC-wins race](/quest/m1/test-flakes-2/quic-winning-race.md) - moq-tokio's QUIC-wins test sets up QUIC's win instead of racing the WebSocket head start - [Handshake ACK](/quest/m1/test-flakes-2/handshake-ack.md) - the impaired cluster drill's connect survives a lost handshake ACK - [Auth refresh expiry](/quest/m1/test-flakes-2/auth-refresh-expiry.md) - the auth refresh outage test measures the grant's life on one clock diff --git a/quest/m1/test-flakes-2/quic-winning-race.md b/quest/m1/test-flakes-2/quic-winning-race.md deleted file mode 100644 index 272bc36b9af..00000000000 --- a/quest/m1/test-flakes-2/quic-winning-race.md +++ /dev/null @@ -1,34 +0,0 @@ -# [S] QUIC-wins race test holds under load - -## Goal - -`connection::tests::quic_winning_opens_one_session` (`rs/moq-tokio`) passes -under a loaded `just check`, fixed at its cause. It still proves that when -QUIC wins, the WebSocket fallback is dropped and never opens a session. - -## Plan - -Seen 2026-10-11 by two agents independently, at load averages of 50 to 130. -WebSocket won the race, so the test's assertion that the transport is -`WebTransport` failed. It passes alone. - -Cause: the test races a real QUIC handshake against the 200 ms WebSocket head -start (`DEFAULT_DELAY` in `rs/moq-tokio/src/websocket.rs`). A loaded machine -can take longer than that to finish the handshake, and then WebSocket winning -is the product working as designed. The test asserts the outcome of a -wall-clock race. - -Decided 2026-10-11: make QUIC's win part of the test's setup rather than the -result of a race. For example, the fixture's WebSocket side doesn't answer -before the QUIC session is accepted. Pick the mechanism that keeps the -"dropped before it opened a session" assertion meaningful. Don't block on -[Controlled time](/quest/m1/time/README.md): real sockets fight a paused -clock, so the fixture needs this shape either way. Never widen the head start -or a wait, per this questline's rules. - -Observe the fallback's cancellation directly, for example by a drop or -connection-close signal. Today `accepted.try_recv()` only proves the queue -is empty at one instant, and a held WebSocket side could hide a fallback -that was never dropped. - -Public API: none. Wire: none. diff --git a/quest/m1/transport-upgrade/README.md b/quest/m1/transport-upgrade/README.md deleted file mode 100644 index d691001f8d5..00000000000 --- a/quest/m1/transport-upgrade/README.md +++ /dev/null @@ -1,85 +0,0 @@ -# WebSocket to QUIC upgrade - -## Goal - -A session that came up over the WebSocket fallback moves to QUIC once the -QUIC handshake completes. Matching explicit epochs permit continuation -without dropping a group; epochless routes instead signal Restart and need a -fresh follower resolution. `https://` races QUIC -against WebSocket, and WebSocket wins whenever QUIC is slower than the head -start plus a TCP+TLS+upgrade round trip (a lost Initial, a slow first -WebTransport dial). The end state: when WebSocket wins, the QUIC dial keeps -going; if it lands, the reconnect loop attaches the QUIC session, hands the -routes over, sends a GOAWAY on the WebSocket session, drains it, and forgets -the "WebSocket won" memo. When QUIC wins, WebSocket is closed immediately. - -The Rust connection-level upgrade exists since #4180 -(`websocket_upgrades_to_quic`, `a_refused_upgrade_keeps_websocket` in -`rs/moq-tokio/src/connection.rs`). What remains is the `js/net` half, where -the loser is still closed and the memo never forgotten, plus the two -children below. - -Non-goals: migrating between IPv6 and IPv4. That race is inside the QUIC dial, -before any MoQ session exists, the loser has no claim to being the better path -(RFC 8305 takes the first to complete and never migrates), the browser does -not expose it, and a family preference belongs to QUIC path migration rather -than a second MoQ session. No flap guard and no opt-out: a QUIC session that -completes the handshake and then dies goes through the ordinary reconnect -backoff, which races again. - -## Plan - -The upgrade is a self-initiated migration, so it reuses the peer-GOAWAY -machinery rather than adding a second handover path. -`moq_tokio::Connection` already dials a replacement while a `Draining` handle -keeps the old session serving until it closes or overstays the handover cap, -reports `Status::Migrating`, and `moq_net::Session::drain()` sends a GOAWAY on -every version (a client may send one with an empty URI; only a redirect URI is -forbidden to a moq-transport client). The origin's multi-route front prefers -the newest of two equal routes. With matching explicit epochs a front resumes each track -from the new route's copy at the first frame the subscriber lacks -(`model/resume.rs`), cancelling the old session's subscription once the new -one feeds it. The JavaScript -GOAWAY handover landed with the drain line, but it does not resume tracks yet; -the JS half requires [JS track handover](/quest/m1/js-group-handover.md) for -that. - -Shared decisions, which Rust implements and [JavaScript](/quest/m1/transport-upgrade/js.md) mirrors: - -- The race returns the winner plus the still-pending QUIC dial when WebSocket - wins. The attempt's connect deadline bounds that dial; no extra deadline. -- The swap waits for the peer's SETUP on the QUIC session - (`moq_net::Session::setup`) within that same deadline; until - then the WebSocket session gets no GOAWAY. A refused or stalled QUIC session - is dropped and WebSocket keeps serving. moq-lite-03 and -04 carry no server - SETUP, so they never upgrade. This crate's servers send SETUP after admission; - other servers may send it before admission and still refuse afterward. -- On a successful upgrade the "WebSocket won" memo (`WEBSOCKET_WON` in - `moq-tokio`, `websocketWon` in `js/net`) forgets the URL: QUIC works on this - network, so the head start comes back. Otherwise a network where WebSocket - narrowly beats QUIC would open two connections on every reconnect. -- The old session gets `Goaway::new()` with the configured handover cap before - it enters draining. The relay prices its routes at the drain cost and keeps - opening requests on it only until the new session's route wins; the front - cancels its subscriptions once the new session feeds them. -- One-shot `connect()` returns one session and never upgrades; every - `Connection` upgrades, reconnecting or not. -- Publishing over the old session is announced again over the new one; the - relay's route order prefers the newest route, and an anonymous client's - per-session origin makes that a replacement rather than a join, which is - immediate either way. - -The Rust `websocket_upgrades_to_quic` test currently reads only before the -old WebSocket closes. Extend proof to publish/read after that close with an -explicit epoch on lite-07. Add the default epochless case: old handles stay -sticky and end with their route, Restart causes a fresh follower to resolve, -and no cached groups splice across instances. Keep JS's matrix consistent. - -## Required - -- [JavaScript](/quest/m1/transport-upgrade/js.md) - js/net keeps the WebTransport dial after WebSocket wins and migrates through the client-goaway handover - -## Related - -- [Closed fallback](https://github.com/moq-dev/moq/pull/5284) - a WebSocket session that closes right after connecting falls back to the pending QUIC dial instead of redialing -- [Restart followers](/quest/m1/restart-followers.md) - the first-party consumers that follow the `Restart` an epochless upgrade delivers diff --git a/quest/m1/transport-upgrade/js.md b/quest/m1/transport-upgrade/js.md deleted file mode 100644 index 407ba8faec1..00000000000 --- a/quest/m1/transport-upgrade/js.md +++ /dev/null @@ -1,57 +0,0 @@ -# [M] JavaScript WebSocket to QUIC upgrade - -## Goal - -A `js/net` `Connection` that came up over the WebSocket fallback migrates to -WebTransport once the WebTransport dial completes: the app-visible handle and -its origins are unchanged, live tracks hand over without a dropped group, the -WebSocket session receives a GOAWAY and drains within the handover cap, and -the "WebSocket won" memo forgets the URL. When WebTransport wins the race -nothing changes. - -## Plan - -Lands in `js/net`, reusing the GOAWAY handover the drain line shipped once -[JS track handover](/quest/m1/js-group-handover.md) resumes tracks -across it: dial the replacement while the old session keeps serving, swap the origin wiring once it is established, leave the old -session to close on its own or at the handover cap. See the -[questline](/quest/m1/transport-upgrade/README.md) for the shared decisions. - -- `connectInner` (`js/net/src/connection/connect.ts`) currently resolves - `cancel` once one transport is ready, which closes the loser. When WebSocket - wins, leave the WebTransport attempt running and return it alongside the - established session; when WebTransport wins, cancel WebSocket as today. The - one-shot `connect()` export keeps returning one `Established` and closes the - pending attempt. -- The reconnecting `Connection` awaits the pending WebTransport `ready`, runs - the same `connectTransport` handshake on it, and feeds it into the GOAWAY - handover as if the WebSocket session had been told to migrate with an empty - URI: origins swap, the old session sends an empty-URI GOAWAY on every wire that - carries the message (lite 04+ and every IETF draft; a moq-transport client - may not name a redirect URI, and an empty one is legal) and closes at the - configured cap. A WebTransport attempt that fails after WebSocket won is logged at - debug and the session stays on WebSocket. -- Swap only once the server's SETUP arrives on the WebTransport session: mirror Rust's - `moq_net::Session::setup()` in `js/net` (resolves on the server's SETUP, - rejects with the close reason if already closed, even after SETUP) and await it inside the - connect deadline. A refusal, a stall, or moq-lite-03/-04 (no server SETUP) - keeps WebSocket with no GOAWAY sent. This crate's servers send SETUP after - admission; other servers may still refuse afterward. -- A self-sent GOAWAY must reprice the old session's routes to the drain cost - too, not only a received one, so new requests move to the QUIC session once - its route answers; sending one still has to set that same signal. -- On a successful upgrade delete the URL from `websocketWon`. -- Tests in the browser harness against the in-tree relay: with the - WebTransport dial delayed past the head start, a watched track keeps every - group across the upgrade, the WebSocket session closes within the cap, and - the next connect to the same URL gives WebTransport the head start again; - with no delay, WebTransport wins and no WebSocket session is ever opened. - The watched broadcast carries an epoch, since a track resumes across a - handover only between routes with the same epoch. -- Public API: the `setup` mirror, if it is exported; `transportOf` already - reports the live transport. Update `doc/lib/js` where the fallback race is - described. - -## Required - -- [JS track handover](/quest/m1/js-group-handover.md) - tracks carry across the handover this upgrade reuses without a dropped group diff --git a/quest/m1/ts-live-clock.md b/quest/m1/ts-live-clock.md index e4cc47aef59..b3923273945 100644 --- a/quest/m1/ts-live-clock.md +++ b/quest/m1/ts-live-clock.md @@ -54,6 +54,5 @@ public API and wire changes are not expected. - [Validation PR #5328](https://github.com/moq-dev/moq/pull/5328#issuecomment-6114456593) - retained local failure and process-status evidence - [Passing full CI](https://github.com/moq-dev/moq/actions/runs/38178905198) - same-head contrast, including real live timing - [Same-instance resume](/quest/m1/ts-same-instance-resume.md) - independent full-check failure -- [Transport upgrade](/quest/m1/transport-upgrade/README.md) - existing session handover and epoch contract - [Mux-rate hold #5287](https://github.com/moq-dev/moq/pull/5287) - initial rate publication, and a finished import lingering for its max-age - [TS AC-3 scheduling](/quest/m1/ts-atsc-ac3.md) - #5306 owns decoder deadlines and unknown-rate startup overflow diff --git a/quest/m3/quic-careful-resume.md b/quest/m3/quic-careful-resume.md index ef36abafaef..ad42ce4e135 100644 --- a/quest/m3/quic-careful-resume.md +++ b/quest/m3/quic-careful-resume.md @@ -29,8 +29,6 @@ start on the first loss or a mismatched RTT. `rs/moq-tokio/src/connection.rs`, dialing through the race in `failover.rs`) seeds a redial from the session it replaces, and the relay's cluster peers seed from the previous session to the same peer. - The transport-upgrade quest's QUIC dial seeds from nothing, since the - previous session ran over TCP. Measure time to the encoder's target rate after a reconnect on the impaired path profile, plus loss and latency during the jump. Ship it on by default @@ -42,5 +40,4 @@ only when the jump never makes the first second worse than slow start. ## Related -- [Transport upgrade](/quest/m1/transport-upgrade/README.md) - its first QUIC connection has no seed from TCP; later QUIC reconnects may reuse a remembered estimate - [noq#815](https://github.com/n0-computer/noq/issues/815) - the careful-resume proposal to n0 diff --git a/quest/m3/uring-tcp/README.md b/quest/m3/uring-tcp/README.md index 063060f3b57..124c71b3bcc 100644 --- a/quest/m3/uring-tcp/README.md +++ b/quest/m3/uring-tcp/README.md @@ -32,10 +32,11 @@ Measure before porting, the same way the echo bench (rs/moq-uring/benches/session_lite.rs) gated the UDP path. The ablation's number is what justifies the rest of the line. -Decided in the 2026-09-30 audit: moved to m2. The -[transport upgrade](/quest/m1/transport-upgrade/README.md) shrinks the -WebSocket hot path by moving clients to QUIC, and no fleet demand asks for -ring TCP. +Decided in the 2026-09-30 audit: moved to m2, since no fleet demand asks +for ring TCP. (Decided 2026-10-13: the WebSocket-to-QUIC upgrade that would +have shrunk the WebSocket hot path is being removed by +[Keep the winning transport](/quest/m0/drop-ws-upgrade.md), so a WebSocket +session will stay on TCP for its lifetime.) Decided 2026-10-08: moved to m3. No fleet demand asks for ring TCP; the ablation stays the gate for the rest of the line.