diff --git a/quest/m0/audit-quests.md b/quest/m0/audit-quests.md index 2fcfcfad3cc..556e8afcf50 100644 --- a/quest/m0/audit-quests.md +++ b/quest/m0/audit-quests.md @@ -47,6 +47,19 @@ Checkpoint, 2026-10-19, against `main` at `2e1ec2ebf`: Quests added since the baseline (the resume line, egress held state, and the request-behind-its-token race from #5972) were checked only against this run's edits, not audited in full. +- Rechecked against `main` at `9b90cdd56` (#5967) for quests left behind by + their own merged PRs. Retired moq-uring on moq-clock (#5926), moving its + unmeasured `[vdso]` profile to an m2 perf quest, IETF max age + (#5993), the request-behind-its-token race (#5992), and the moq-quic + regression net with its last child (#5989). Trimmed the TRACK_STATUS race + (#5991) to its deferred upstream report, and recorded what #6006 and #5987 + left of the C++ package proof and the moq-clock crate migration. Check + first, next run: every PR merged from a `quest/...` branch whose quest file + still exists. +- Cause of those leftovers: no quest skill tells the PR that finishes a quest + to delete its file, or a partial PR to rewrite it to the remainder, and + `/quest-merge` enables auto-merge without checking either. Drafts whose + bodies said "the quest remains open" were merged with the quest unchanged. This run remains open, with no successor, until the unresolved conflicts are settled. The unattended instruction was to skip uncertain choices: diff --git a/quest/m1/README.md b/quest/m1/README.md index f480fbbbcc6..c594c8e9804 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -73,8 +73,7 @@ lands before the cut. - [Exact timestamps in every binding](/quest/m1/binding-timestamps.md) - bindings carry `{value, scale}` timestamps instead of lossy microseconds - [JSON window timestamps](/quest/m1/json-window-timestamps.md) - Rust and JS window consumers return each event's frame timestamp like the snapshot and stream consumers - [JavaScript FETCH](/quest/m1/js-fetch.md) - browser publishers answer IETF FETCH through the JS ranges request surface -- [Subscribing readers never ask TRACK_STATUS](/quest/m1/track-status-race.md) - a reader's subscription lands with its request, and the NOT_SUPPORTED fallback goes away -- [IETF max age](/quest/m1/ietf-max-age.md) - a moq-transport subscriber sets lite's max age through a timestamp-draft SUBSCRIBE parameter +- [Report relays that refuse TRACK_STATUS](/quest/m1/track-status-race.md) - moqx and any other relay refusing TRACK_STATUS with an undefined code get an upstream report - [IETF fill timeout](/quest/m1/ietf-fill-timeout.md) - native FILL_TIMEOUT bounds upstream waiting on drafts 18-22, alongside local content-age enforcement - [Browser follows a renamed track](/quest/m1/browser-rename-check.md) - a real browser keeps playing when `@moq/publish` renames a track after a codec change, while size-only changes keep their identity - [Rust identity enforcement](/quest/m1/catalog-identity-rs.md) - `moq-mux` refuses a redefined rendition, and every Rust publisher mints a new rendition name for a decoder identity change and updates everything else in place diff --git a/quest/m1/auth/README.md b/quest/m1/auth/README.md index e16a8a819c4..86d411cd9aa 100644 --- a/quest/m1/auth/README.md +++ b/quest/m1/auth/README.md @@ -48,8 +48,8 @@ Decisions settled while planning, recorded so review does not relitigate them: arrive on `requests()`, a moq-transport request is held to its token's grant, an update's token replaces it, and no lite wire changes. Step 2, lite requests naming an AUTH stream, is dropped; what it would have closed, - a request racing the token it relies on, is - [A request right behind its token](/quest/m1/auth/request-race.md). Step 3 + a request racing the token it relies on, landed as a presenter-side wait in + [#5992](https://github.com/moq-dev/moq/pull/5992). Step 3 sends outgoing request tokens on moq-transport through `Route::auth` as `USE_VALUE` ([Live requests follow their auth](/quest/m1/auth/request-update.md)); lite has nothing to send, since the token already sits in the union. @@ -97,7 +97,7 @@ Decisions settled while planning, recorded so review does not relitigate them: broadcast outside the union aborts the session with `Unauthorized`, naming the path. The check runs against the grants in hand once every token the session presented is answered, setup, replayed, or app-added alike - ([request race](/quest/m1/auth/request-race.md)). The origin is + ([#5992](https://github.com/moq-dev/moq/pull/5992)). The origin is untouched, so a broadcast shared across sessions (P2P hops, a cluster) is refused only where it is refused. Grant shrinkage cancels previously authorized work on this session without aborting it or deleting the shared @@ -146,7 +146,6 @@ because `main` assigned 0x3A to NOT_FETCHABLE first. - [Live requests follow their auth](/quest/m1/auth/request-update.md) - a route swaps its auth and the session sends the update, keeping the request up - [Auth outlives its session](/quest/m1/auth/issuer.md) - one `auth::Handle` spans a connection's sessions, so a route's auth survives a reconnect -- [A request right behind its token](/quest/m1/auth/request-race.md) - a lite request relying on a just-presented token is admitted, with no auth field on any request - [Bindings](/quest/m1/auth/bindings.md) - grants and tokens reach every binding through moq-ffi - [Token in band](/quest/m1/auth/token-in-band.md) - extra credentials ride diff --git a/quest/m1/auth/request-race.md b/quest/m1/auth/request-race.md deleted file mode 100644 index b1a7fc308e2..00000000000 --- a/quest/m1/auth/request-race.md +++ /dev/null @@ -1,114 +0,0 @@ -# [S] A request right behind its token is admitted - -## Goal - -On moq-lite, a request that only a just-presented token admits is never -refused because it reached the acceptor before that token's AUTH settled, and -a renewal never drops a live request the new grant still covers. Lite -requests stay judged by the union of session-lifetime auths alone: no Auth ID -on AUTH, no `Auth` field on requests, no extra bytes on any request, and no -timer. - -## Plan - -- ✅ Decided 2026-10-19 (maintainer, closing - [#5927](https://github.com/moq-dev/moq/pull/5927)): drop the named version. - "I don't want per-request auth and I don't reaaaallly want to waste a byte - on every request." Lite AUTH carries no ID and lite requests no `Auth` - field. `main` never carried either (checked 2026-10-19 in - `drafts/draft-lcurley-moq-lite.md`, `rs/moq-net` and `js/net`), so nothing - needs removing. -- ✅ Decided 2026-10-19 (maintainer, #5972): the first token rides SETUP, a - lite-07-wip SETUP parameter and moq-transport's existing AUTHORIZATION - TOKEN option, with the URL query as the fallback. "We need to block on - SETUP anyway": the acceptor already reads the client's SETUP before serving - the session (`Server::accept_request_lite`), and a lite client sends - requests without waiting on the server's, so it costs no round trip and a - session's first requests wait on no AUTH. Further tokens join the union - through AUTH streams. [Token in band](/quest/m1/auth/token-in-band.md) - carries the parameter. This replaces the earlier "always in the CONNECT - request" decision and drops its auth-header plan. -- ✅ Decided 2026-10-19 (maintainer): moq-transport's per-request - AUTHORIZATION TOKEN stays acceptor-side as - [#5148](https://github.com/moq-dev/moq/pull/5148) modeled it and never - becomes a lite wire feature. `draft-lcurley-moq-auth` gains no request - parameter naming an AUTH, and token aliases stay unsupported (cache size 0). -- Where `main` stands (2026-10-19). A lite acceptor judges SUBSCRIBE and - FETCH against its scope when it decodes them, and a grant joins the scope - only once the acceptor accepts its token. The presenter's own gate refuses - a request its known union does not cover before sending it, and - `auth::Handle::add` (Rust) and `add` (js/net) resolve only after AUTH_OK, so - an app that awaits `add()` never races. Announcements already heal: a - broadcast withheld for falling outside the scope comes back when the scope - widens. What is left: - - the draft does not say any of this; - - nothing pins that an acceptor widens its scope no later than it sends - AUTH_OK, which the presenter relies on; - - tokens the library presents without the app awaiting them race the app's - requests on the same session: moq-tokio's reconnect replay in - `connection::auth` today, and later the extra session-start tokens of - [Auth outlives its session](/quest/m1/auth/issuer.md) and - [Token in band](/quest/m1/auth/token-in-band.md). The SETUP token never - races, so only a client with more than one token can hit it. -- ✅ Decided 2026-10-19 (maintainer, #5972): **the presenter waits**, with a - log line so we see how often it triggers. "I want to make sure we're not - adding dumb RTTs." A request its known union does not cover waits, instead - of failing locally, while any non-empty token this side presented is - unanswered, then goes out or fails against the settled union. An unknown - union covers nothing for this rule, but the empty token never holds a - request, since the acceptor judges the connection credential (SETUP or URL) - before it serves the session. It costs one round trip, only for the first request relying on a new - token, which `add()` already spends; a single-credential session and a - renewal spend nothing. No wire change and no acceptor state. The rule lives - in the shared gate, so the moq-transport AUTH extension gets it too. Abuse: - an acceptor that never answers holds only our own requests, which it could - stall anyway by never answering them. No timer. -- Rejected 2026-10-19 (#5972): the acceptor waiting on that peer's unanswered - AUTHs, because AUTH and the request ride different streams, so a lost AUTH - packet still lets the request arrive first with nothing pending; and an - AUTH_WAIT prefix on the request stream, which is exact but a new message to - save a round trip nobody has measured. Revisit AUTH_WAIT only if the log - below shows the wait firing in practice. -- ✅ Decided 2026-10-19 (maintainer, #5972): observability is the log line - only, not a `stats::Session` counter. Every request that waits on an - unanswered token logs once (path and how long it waited), in Rust and - `js/net`. -- The wait only sees tokens already presented, so a replayed token must be - registered on the new session before the app can use it. Today - `Shared::connected` in moq-tokio publishes the new session before each - per-token replay task reaches `add()`, so a woken app can send a request - while nothing is pending. Present every token the connection holds on the - new session's handle (queued, not awaited) before publishing the session; - [Auth outlives its session](/quest/m1/auth/issuer.md) does this by - construction once the handle spans sessions. js/net mirrors it if its - reconnect replays tokens the same way. -- Spec in lite-07-wip that the acceptor judges a request against the union - when it reads it, that its scope includes a grant no later than it sends - that AUTH_OK, and that a presenter does not send a request only an - unanswered token admits. Hold the publish-outside-the-union check to the - same rule, so it waits on every unanswered token rather than only the setup - ones. Mirror in `js/net`. -- Renewal under the union: present the new token, wait for its AUTH_OK - (`add()` already does), then drop the old one. The union never shrinks - under a live request the new grant also covers, and a narrower renewal ends - only what it no longer covers. -- Not carried from #5927: FIN-linger withdrawal, since nothing names the old - token and dropping it after the new AUTH_OK races nothing; per-token - wakeups and the `named` bench, since no request watches one token. A change - still recomputes the union, as today. Extend `rs/moq-net/benches/auth.rs` - only if the wait adds per-request work on an AUTH_OK. -- Tests that fail without the fix, in Rust and JS: a direct request made - the moment a reconnect publishes its session, relying on a replayed token, - is admitted; an acceptor's AUTH_OK - never precedes its scope; a live subscription survives a renewal. -- Docs: renewal and the wait in `doc/concept/moq-lite.md`'s Authorization - section. - -Public API: none. Wire: lite-07-wip draft text only, no new bytes. - -## Related - -- [#5927](https://github.com/moq-dev/moq/pull/5927) - the closed named-auth - version this replaces -- [Live requests follow their auth](/quest/m1/auth/request-update.md) - - moq-transport request renewal; lite renews through the union diff --git a/quest/m1/auth/request-update.md b/quest/m1/auth/request-update.md index 47249551aec..9487485a000 100644 --- a/quest/m1/auth/request-update.md +++ b/quest/m1/auth/request-update.md @@ -8,7 +8,7 @@ application swaps it, the session sends REQUEST_UPDATE (moq-transport) carrying the new auth, and the request stays up. Lite sends nothing: a route's auth already sits in the session union through its AUTH stream, so lite renews through the union as in -[A request right behind its token](/quest/m1/auth/request-race.md). The first consumer is a client that keeps a +[A request right behind its token](https://github.com/moq-dev/moq/pull/5992). The first consumer is a client that keeps a PUBLISH_NAMESPACE alive past its credential's expiry by sending REQUEST_UPDATE with a fresh token before it expires. @@ -84,4 +84,4 @@ REQUEST_UPDATE. ## Related -- [A request right behind its token](/quest/m1/auth/request-race.md) - lite renewal through the union +- [A request right behind its token](https://github.com/moq-dev/moq/pull/5992) - lite renewal through the union diff --git a/quest/m1/cpp-package-check.md b/quest/m1/cpp-package-check.md index f20ebc92dcf..9fbd97b2aa0 100644 --- a/quest/m1/cpp-package-check.md +++ b/quest/m1/cpp-package-check.md @@ -1,4 +1,4 @@ -# [S] C++ and OBS packages pass their nightly dry run +# [XS] C++ and OBS packages pass their nightly dry run ## Goal @@ -8,11 +8,26 @@ checks before the first tag, using locally built release artifacts. ## Plan Extracted from first C++ publication on 2026-10-13 to avoid waiting for a tag -before validating its build path. Add a local-archive override beside OBS's -release-download branch so the nightly workflow links the C++ build artifacts. -Keep publication tag-only. Exercise the installed package and required OBS -settings/session-report parity through the existing nightly and interop lanes. -The first publication and released-download proof stay in release execution. +before validating its build path. Keep publication tag-only. The first +publication and released-download proof stay in release execution. -Public API: none beyond the binding changes owned by their quests. Wire: none. +[#6006](https://github.com/moq-dev/moq/pull/6006) landed the build path: each +packaged archive runs the installed-consumer checks (`just cpp check +--archive`), and an OBS dry run links the matching checked archive +(`just obs package --moq-archive`, CMake `MOQ_ARCHIVE_FILE`) instead of a +release download. It passed on one Linux host locally. + +What remains is the proof from a scheduled Nightly on `main` after #6006: + +- Its `Release Cpp` job (`.github/workflows/release-cpp.yml` through + `nightly.yml`) builds and checks the C++ archive for all four targets, and + packages the OBS plugin against the checked archive for the two OBS + targets, macOS and Windows. Linux OBS is build-from-source and ships no + plugin. +- Its separate Linux `OBS` lane (`just obs ci`) passes the settings and + session-report tests. That lane builds the plugin in-tree, not from the + packaged archive. +Fix whatever those runs turn up; delete this quest once both are green. + +Public API: none beyond the binding changes owned by their quests. Wire: none. diff --git a/quest/m1/ietf-max-age.md b/quest/m1/ietf-max-age.md deleted file mode 100644 index 394dbc6539a..00000000000 --- a/quest/m1/ietf-max-age.md +++ /dev/null @@ -1,50 +0,0 @@ -# [S] IETF subscribers set their own max age - -## Goal - -A moq-transport subscriber bounds how late it is served with a SUBSCRIBE -and REQUEST_UPDATE parameter that carries moq-lite's subscriber max age -exactly: a group expires only once a newer group superseded it, judged on -media time and the hop's wall clock, so the newest group, such as a -long-lived catalog group, is never cut. Without it, the subscriber is served -as today. - -## Plan - -Decided 2026-10-19 by the maintainer (#5881): - -- A new parameter, not the IETF delivery timeouts. OBJECT_DELIVERY_TIMEOUT - resets a stream once an object has waited at the hop, newest group - included: a late joiner's catalog snapshot would be reset and never - delivered. SUBGROUP_DELIVERY_TIMEOUT runs from the FIN. Neither matches - `max_delay`, so keep dropping both and say so in - `doc/concept/standard.md`, replacing its "advisory" paragraph. -- Define it in `drafts/draft-lcurley-moq-timestamp.md`, whose age-based - dropping already allows an application-supplied threshold. Pick a code - point from that draft's space. Validate with `just drafts check`. -- Negotiate it with its own Setup Option, as the hidden draft does for - HIDDEN. Drafts 16 and later close the session on an unknown SUBSCRIBE - parameter, so send the parameter only to a peer that declared the option. - The draft's "require no SETUP negotiation" sentence covers its properties, - not this parameter. -- It maps onto `Subscription::max_delay` on SUBSCRIBE (`serving_subscription` - in `rs/moq-net/src/ietf/publisher.rs`) and REQUEST_UPDATE. Absent keeps - `MAX_SAFE_DELAY_MS`, never `Duration::ZERO`, which means skip at once. -- Both directions, as lite does: our IETF subscribers send their - aggregated `max_delay` upstream, and the relay keeps lite's most-tolerant - aggregation, so one aggressive viewer never tightens another's budget. -- Rust and `js/net`, both publisher and subscriber, at every IETF draft that - carries SUBSCRIBE parameters. Test that a subscriber behind a shaped link - with a short max age is skipped to the live edge, and that a catalog's - newest group survives any budget. - -Public API: none. Wire: a new Setup Option and SUBSCRIBE and REQUEST_UPDATE parameter. - -## Closes - -- [#5881](https://github.com/moq-dev/moq/issues/5881) - IETF subscribers cannot bound delivery lateness - -## Related - -- [IETF fill timeout](/quest/m1/ietf-fill-timeout.md) - the upstream waiting budget, a different bound -- [Per-stream deadlines](/quest/m2/quic-deadline.md) - would derive per-stream resets from `max_delay` diff --git a/quest/m1/quic/README.md b/quest/m1/quic/README.md index 77cd236d029..e3bbaf9a0a6 100644 --- a/quest/m1/quic/README.md +++ b/quest/m1/quic/README.md @@ -63,7 +63,6 @@ cache limits remain separate; bitrate pacing composes with this memory cap. ## Required -- [Regression net](/quest/m1/quic/coverage/README.md) - benchmarks, fuzzing, characterization tests, and a seeded simulation, so later QUIC changes see every behavior or cost change - [Bound message assembly](/quest/m1/quic/receive-memory.md) - complete headers and frames return credit, and receiver priority cancellation restores progress - [A reset group keeps its header](/quest/m1/quic/group-reset.md) - a lite publisher resets a group with `RESET_STREAM_AT`, so its subscriber drops the diff --git a/quest/m1/quic/coverage/README.md b/quest/m1/quic/coverage/README.md deleted file mode 100644 index 321392d0aee..00000000000 --- a/quest/m1/quic/coverage/README.md +++ /dev/null @@ -1,49 +0,0 @@ -# Regression net for moq-quic - -## Goal - -`moq-quic` has benchmarks, fuzzing, characterization tests, and a seeded -simulation dense enough that later QUIC work can refactor aggressively and -see every behavior or cost change as a failing test or a benchmark delta. - -The net pins today's behavior; it does not change it. A bug the net finds -lands as an `#[ignore]`d failing test naming a new follow-up quest, never as -an inline fix. - -## Plan - -Decided in the 2026-10-16 plan with the maintainer: - -- **Baseline is `origin/main` at b30289127**, before the rewrites - (#5711 scheduler, #5690 reliable reset, #5735 receive memory, #5688 - shard, #5674 peer limits) merge. #5690 and #5688 merged while this was - planned, so they take the merged-first comparison in - [the benchmarks](/rs/moq-quic/benches/common/mod.rs) and the - characterization tests are written against b30289127, then rebased. The net lands first so each rewrite can - compare against it: benchmarks via `just bench` against the commit that - lands them (Criterion only compares cases present at the base), tests by - rebasing, where every behavior change shows up as a failing test to - accept or fix. -- **Ranked first in the QUIC epic, but blocks nothing.** Every m1 QUIC - rewrite already has an open PR, and `Required` gates starting, not - merging. The net mainly protects the next wave of changes. -- **Benchmarks and characterization tests drive the public - `Connection`/`Endpoint` API** through the test `Pair`, so they compile on - both sides of a rewrite. Internal unit tests may break with a refactor and - get rewritten there. - -Survey on the baseline (462 tests, 1.4 s): 88.1% of production lines run, -but behavior is thin where the rewrites cut. RESET_STREAM is received 4 -times in the whole suite, STREAMS_BLOCKED_BIDI never, -`set_max_concurrent_streams` and `RecvStream::received_reset` are 0%, -`endpoint.rs` is 86.5%, frame error paths are unexercised, upstream -quinn's fuzz targets were never imported, there are no crate benchmarks, -and the harness defaults to Cubic while production runs BBR3. - -## Required - -- [Richer simulation workloads](/quest/m1/quic/coverage/sim-workloads.md) - resets, bidi streams, datagrams, and migration in the random workloads, each with its own invariant - -## Related - -- [Receive memory coverage](https://github.com/moq-dev/moq/pull/5752) - the end-to-end moq-tokio receive-window benchmark; the crate benchmarks here sit below it diff --git a/quest/m1/quic/coverage/sim-workloads.md b/quest/m1/quic/coverage/sim-workloads.md deleted file mode 100644 index 15926f0aeab..00000000000 --- a/quest/m1/quic/coverage/sim-workloads.md +++ /dev/null @@ -1,32 +0,0 @@ -# [M] Richer simulation workloads - -## Goal - -The seeded simulation's random workloads in `rs/moq-quic/src/tests/sim.rs` -also exercise the paths the QUIC rewrites cut, each with its own invariant, -so a refactor that breaks one fails a replayable seed: - -- **Resets**: RESET_STREAM and RESET_STREAM_AT mid-transfer. The reader - sees an unmodified prefix of what was written, then the reset code. The - prefix covers at least the reliable size for RESET_STREAM_AT and may be - any length for RESET_STREAM, since bytes already read stay read. Stream - and connection credit come back. -- **Stops**: STOP_SENDING mid-transfer. The sender observes the code - through `SendStream::stopped` or `WriteError::Stopped`, the stopped - reader sees nothing further, and credit comes back. -- **Bidirectional streams**: both sides write on the same streams, and every - byte arrives once and in order in each direction. -- **Datagrams** interleaved with streams: each arrives at most once and - unmodified, and datagrams never stall stream progress. On a lossless link - with buffers sized for the workload, every datagram arrives, so dropping - them all fails. -- **Migration**: NAT rebinding and active migration mid-workload. The - connection survives and every delivery invariant still holds. - -## Plan - -Decided 2026-10-16 with the maintainer. Reuse the workload generator and -invariant checks from the seeded simulation, under the same PR budget (64 -fixed cases) and nightly sweep (`just rs quic-sim`). If migration needs -address changes that `Pair`'s `Link`s cannot express, add the smallest -harness hook for it here; flag it on the PR if that pushes past [M]. diff --git a/quest/m1/time/README.md b/quest/m1/time/README.md index ba2d1b4f3d7..0d902aa30df 100644 --- a/quest/m1/time/README.md +++ b/quest/m1/time/README.md @@ -81,8 +81,8 @@ reasons, so later sessions don't reopen them: `disallowed-types` in a per-crate `clippy.toml`, and a biome `noRestrictedGlobals` override, allowing only the backend modules. The workspace-wide test and lint sweep follows in m2. -- **Consolidation.** perf/3122 (one clock read per io_uring turn) merged into - [moq-uring on moq-clock](/quest/m1/time/uring.md). The relay seam +- **Consolidation.** perf/3122 (one clock read per io_uring turn) landed with + moq-uring on moq-clock in [#5926](https://github.com/moq-dev/moq/pull/5926). The relay seam [bench-relay](/quest/m2/bench-relay.md) needed is `moq_relay::Accepted`: a `moq_relay::Connection` over any moq-net transport, such as the in-memory one. Its tests run on paused tokio until @@ -98,7 +98,6 @@ cleanup and the global lint ratchet belong to ## Required - [Every crate on moq-clock](/quest/m1/time/crates.md) - every remaining crate reads time only through moq-clock, with controlled tests for the production/public API migrations -- [moq-uring on moq-clock](/quest/m1/time/uring.md) - one clock read per worker turn and moq-clock's deadline queue replace the private timer heap ## Related diff --git a/quest/m1/time/crates.md b/quest/m1/time/crates.md index 30cfebf1228..c84e16ca454 100644 --- a/quest/m1/time/crates.md +++ b/quest/m1/time/crates.md @@ -23,8 +23,7 @@ on a fixed sleep. moq-relay's in-memory integration tests moq-auth's `Grant` and lease clock, and moq-relay's lease, credential, and admission timers, already run on moq-clock (`Grant::deadline` returns a -`moq_clock::Instant`); build on that rather than migrating them again. What -remains in moq-auth is its JWT claim checks (`claims.rs`, `key.rs`) and `serve`. +`moq_clock::Instant`); build on that rather than migrating them again. Fix mixed clocks first: anything that reads std time but waits on tokio time (a backoff's stability window, a gate computed with std `Instant` and slept @@ -34,9 +33,12 @@ Split the PR by crate group if review needs it; ban direct clock reads in each production scope as it finishes. Unrelated legacy test conversions and the workspace-wide lint ratchet follow in m2. -The current checkpoint migrates auth clock reads, RTMP session timestamps and -deadlines, archive retention, and listener stall accounting. The rest of the -workspace and public `SystemTime` fields still need migration. In particular, +[#5987](https://github.com/moq-dev/moq/pull/5987) migrated moq-auth (auth +accounting, JWT checks, policy expiry), RTMP session timestamps and +deadlines, archive retention, and listener stall accounting, and added the +per-crate ratchet to moq-auth, moq-rtmp, and moq-archive. The ratchet also +covers moq-net, moq-mux, moq-ffi, moq-room, moq-stats, and moq-uring; the +rest of the workspace and public `SystemTime` fields still need migration. In particular, SRT and WebRTC hand instants to third-party APIs on std's clock, and audio/video device threads block outside an async runtime; those boundaries still need clock support rather than a second host-clock read at each call site. diff --git a/quest/m1/time/uring.md b/quest/m1/time/uring.md deleted file mode 100644 index b9017fef0a2..00000000000 --- a/quest/m1/time/uring.md +++ /dev/null @@ -1,42 +0,0 @@ -# [M] moq-uring on moq-clock - -## Goal - -moq-uring's worker is a `moq-clock` backend. It reads the clock once per drive -turn and hands that instant to `fire`, the QUIC `handle_timeout`, and -`poll_transmit`, so `[vdso]` falls from its ~2.5% share of io_uring relay CPU -to the tokio path's ~0.7%, with QUIC timeout and keep-alive arming unchanged -in effect. moq-clock's deadline queue replaces `timer::Heap`. The worker tests -`deadline_fires_at_park` and `dropped_worker_rejects_operations` run on -controlled time or event assertions and pass under load. - -## Plan - -Absorbed [#3122](https://github.com/moq-dev/moq/issues/3122) on 2026-10-10: -its profile (`[vdso]` 2.95% video and 2.55% chat on io_uring against 0.72% on -tokio workers; `Timer::set` plus btree search ~1.6%) and its plan (sample once -per turn; closed prototype #3136 froze the clock per turn behind an RAII -guard) carry over. The per-turn sample is the worker's ambient `now()`; code -on the worker never reads the host clock itself. Use a timer wheel instead of -the queue only if the bench says so. - -Measure before and after on `moq-quic`: the `[vdso]` share in `perf` on both -flavors, and relay CPU via `just bench BASE` on Linux. The existing keep-alive -and idle-timeout tests stay unchanged. Ban direct clock reads in moq-uring -outside its backend (the ratchet). - -Move `uring_workers_bound_stalled_handshakes` (`rs/moq-relay/tests/integration/runtime_uring.rs`) -onto controlled time too; it landed on the wall clock in #5244 rather than -waiting for this quest. - -Public API: breaking. `moq_uring::Timer` is re-exported, and `Timer::set` -takes `Option` instead of std's; no conversion shim. -Wire: none. - -## Closes - -- [#3122](https://github.com/moq-dev/moq/issues/3122) - close this issue when the quest finishes - -## Related - -- [Run to quiescence](/quest/m2/perf/uring-quiescence.md) - reshapes the same worker turn diff --git a/quest/m1/track-status-race.md b/quest/m1/track-status-race.md index 42e32bfcb74..a96784c216d 100644 --- a/quest/m1/track-status-race.md +++ b/quest/m1/track-status-race.md @@ -1,41 +1,22 @@ -# [S] A subscribing reader never asks TRACK_STATUS +# [XS] Report relays that refuse TRACK_STATUS ## Goal -A reader that subscribes to a track served over moq-transport reaches the -publisher as a SUBSCRIBE, never as a TRACK_STATUS, however its subscription -races the request it creates. TRACK_STATUS stays for fetch-only and -query-only demand, and a refused TRACK_STATUS fails that demand loudly. +Each relay found refusing TRACK_STATUS, or answering it with an undefined +code, has an upstream report. moqx is known: it answered REQUEST_ERROR +`0xFFFFFFFF` on draft 18 ([#5954](https://github.com/moq-dev/moq/issues/5954)). +Delete this quest once every known relay has one. ## Plan -Decided 2026-10-19 by the maintainer (#5954): - -- Fix the race at the source. `broadcast::Consumer::resolve_track` - (`rs/moq-net/src/model/broadcast.rs`) makes the request visible before - `Preferences::register` (`model/track/demand.rs`) writes the - subscription, and the front's `Action::Query` (`model/origin.rs`) runs - `lookup.query()` before `copy.subscribe`. Register the subscription - before the request or demand edge is visible, and subscribe the front's - copy with its query. Don't make the IETF subscriber wait or poll for a - subscription. -- Delete the NOT_SUPPORTED fallback in `track_status` - (`rs/moq-net/src/ietf/subscriber.rs`). It was added for the JS publisher, - which answers TRACK_STATUS since #5468, and falling back to SUBSCRIBE for - fetch-only demand brings back the problems TRACK_STATUS avoids (a - finished track refuses it; it races the fetches). This settles the - deferred step in [JavaScript FETCH](/quest/m1/js-fetch.md). -- Don't widen the fallback to undefined codes, as #5954 proposes: that would - hide the peer's bug and ours. -- File an issue upstream for each relay found refusing TRACK_STATUS, or - answering it with an undefined code: moqx is known (#5954, `0xFFFFFFFF`). - The maintainer asked for these on 2026-10-19. -- Regression test: a subscriber's catalog read against a publisher that - refuses every TRACK_STATUS with an undefined code still opens, and the - publisher sees no TRACK_STATUS. It fails today on a multi-thread runtime. - -Public API: none. Wire: none. - -## Closes - -- [#5954](https://github.com/moq-dev/moq/issues/5954) - a TRACK_STATUS refused with any code but NOT_SUPPORTED fails the reader's subscription +The fix landed in [#5991](https://github.com/moq-dev/moq/pull/5991): a +subscribing reader registers its subscription before its request is visible, +so it reaches the publisher as a SUBSCRIBE, and the NOT_SUPPORTED fallback is +gone. Fetch-only and query-only demand still uses TRACK_STATUS and fails +loudly when refused, so a refusing relay now breaks only those. The +maintainer asked for the upstream reports on 2026-10-19; #5991 deferred them. + +Posting outside the `kixelated` and `moq-dev` orgs needs the maintainer's +approval, so an agent drafts the report (the observed code, the draft, and +the TRACK_STATUS request that drew it) and the maintainer files or approves +it. diff --git a/quest/m2/perf/README.md b/quest/m2/perf/README.md index 22e0f4e7d60..74a6efaa9c2 100644 --- a/quest/m2/perf/README.md +++ b/quest/m2/perf/README.md @@ -23,8 +23,7 @@ hot-path survey, so quests don't re-litigate them: driver (Run to quiescence) measure their before and after on `moq-quic`. - `moq-uring`'s only backend is `moq-quic`. Every profile names its backend. The historical quiche-flavor numbers cited in - [Run to quiescence](/quest/m2/perf/uring-quiescence.md) and - [moq-uring on moq-clock](/quest/m1/time/uring.md) are re-measured on `moq-quic`. + [Run to quiescence](/quest/m2/perf/uring-quiescence.md) are re-measured on `moq-quic`. - Cross-thread wakeups are already cheap: one futex word per worker, at most one `futex(FUTEX_WAKE)` per park cycle, wake bursts coalesce through the `kio::Tasks` bitset. No eventfd, no MSG_RING, by design (`SINGLE_ISSUER`). @@ -59,9 +58,12 @@ recipe (`just bench-profile`), so the relay has one recipe with capture modes; # equally unmeasured ring micro-opt; cache shard was deleted, since #5031's profile shows no pool contention. -Decided 2026-10-10: #3122's per-turn clock read merged into -[moq-uring on moq-clock](/quest/m1/time/uring.md), where the worker's turn -sample becomes its `moq-clock` clock; it keeps #3122's measurement bar. +#3122's per-turn clock read landed in +[#5926](https://github.com/moq-dev/moq/pull/5926): the io_uring worker is a +`moq-clock` backend that samples the clock once per turn, cutting +`clock_gettime` from 44.7 to 1.05 calls per turn. `perf` was unavailable +there, so [Profile the io_uring clock read](/quest/m2/perf/uring-clock-profile.md) +owns the `[vdso]` share and relay CPU measurement. Decided 2026-10-14: kio channel contention ranks first. Its 215% lock wait on the default tokio runtime is the largest measured cost, and the lock-wait @@ -75,7 +77,4 @@ optimization. - [Run to quiescence](/quest/m2/perf/uring-quiescence.md) - a received packet's reply is staged in the same turn, under a pass and train budget that keeps the fairness rule - [Ingest batch](/quest/m2/perf/ingest-batch.md) - relay ingest pays one lock, wake, and clock read per burst of whole frames instead of per frame - [Shard by default](/quest/m2/perf/shard-default.md) - a saturating shard sweep decides whether an unset `runtime.workers` runs one worker per core on Linux - -## Related - -- [moq-uring on moq-clock](/quest/m1/time/uring.md) - one clock read per worker turn, formerly #3122 +- [Profile the io_uring clock read](/quest/m2/perf/uring-clock-profile.md) - a `perf` profile shows whether #5926's per-turn clock read cut the io_uring relay's `[vdso]` share and CPU diff --git a/quest/m2/perf/uring-clock-profile.md b/quest/m2/perf/uring-clock-profile.md new file mode 100644 index 00000000000..5ae4654ab68 --- /dev/null +++ b/quest/m2/perf/uring-clock-profile.md @@ -0,0 +1,30 @@ +# [XS] Profile the io_uring clock read + +## Goal + +A `perf` profile on `moq-quic` measures whether the per-turn clock read from +[#5926](https://github.com/moq-dev/moq/pull/5926) cut the io_uring relay's +`[vdso]` share from #3122's ~2.5 to 3% to the tokio path's ~0.7%, and +whether relay CPU fell with it. + +## Plan + +#5926 counted `clock_gettime` instead (44.7 to 1.05 calls per turn), since +`perf` was unavailable on its host and `just bench` was too noisy there to +read. That count is a proxy; this quest records the measurement it stood in +for. + +Run on a quiet Linux host where `perf` works. Profile the io_uring workers +on the video and chat workloads at #5926's base (`1dd9b83d1`) and at `main`, +and read the `[vdso]` share. `just bench-profile cpu` profiles only tokio +workers today, so either extend it to `io-uring-workers` or run `perf` +against the `just bench-runtime` relay. Read relay CPU from the +`io-uring-workers` rows of `just bench-runtime`, run at both revisions on the +same host and worker count. `just bench BASE` won't do: it runs only the +default tokio runtime. + +If `[vdso]` stays well above the tokio share, record what still reads the +clock; a follow-up decides whether it is worth chasing. Either result +deletes this quest. + +Public API: none. Wire: none.