diff --git a/quest/m0/README.md b/quest/m0/README.md index be7e4f2bad6..8357acee985 100644 --- a/quest/m0/README.md +++ b/quest/m0/README.md @@ -25,5 +25,7 @@ edge, which embeds moq-relay; it moved there on 2026-09-28. - [Backport IETF close codes](/quest/m0/backport-ietf-close-codes.md) - `@moq/net` on `release` closes moq-transport sessions with the right error for three hostile cases - [Backport @moq/flate bounds](/quest/m0/backport-flate-budget.md) - `@moq/flate` on `release` stops inflating past its cap and stops pinning pako scratch - [Backport MKV, FLV, and VP9 import fixes](/quest/m0/backport-mux-import.md) - moq-mux on `release` refuses malformed imports instead of wrapping, panicking, or dropping tracks +- [Backport the frame-start refusal](/quest/m0/backport-frame-start.md) - `@moq/net` on `release` refuses a nonzero lite frame start instead of misnumbering frames +- [Smoke tests the released packages](/quest/m0/smoke-released.md) - smoke clients call only released APIs and every red cell is fixed, so Smoke passes on main - [CI cost and latency](/quest/m0/ci/README.md) - cloud CI runs only in a batched merge gate as one `just ci` pass per batch, expensive checks run nightly and gate `release`, caches move to R2, and runners are chosen last - [More tests under load](/quest/m0/test-flakes-2/README.md) - load-only and queue failures, one quest per flake, fixed at the cause, since each one fails a whole batch diff --git a/quest/m0/backport-frame-start.md b/quest/m0/backport-frame-start.md new file mode 100644 index 00000000000..fbfed094614 --- /dev/null +++ b/quest/m0/backport-frame-start.md @@ -0,0 +1,25 @@ +# [S] Backport the frame-start refusal to release + +## Goal + +`@moq/net` on `release` refuses a lite group stream that starts at a nonzero +frame instead of silently numbering its frames from zero, and publishes a +patch release. + +## Plan + +Decided 2026-10-27 (maintainer): one PR against `release` per backport, in +m0, JS only. From +[moq-dev/moq#6922](https://github.com/moq-dev/moq/pull/6922), which makes +main honour a frame start. Release's Rust half does not apply (it lacks the +fill and group content model), and the #6903 and #6919 fixes are not +backported: #6919's `--stitch` is main-only, and #6903 would be a rewrite +against release's older watch API for a playback glitch. + +As of 6ce67943f7, release's `js/net/src/lite/subscriber.ts` creates +`new netGroup.Producer(group.sequence)` and never reads the frame start, so +a mid-group start misnumbers frames, while release's Rust publisher can +send one. Refuse a nonzero start as main did before #6922 (the refusal +added in [moq-dev/moq#6395](https://github.com/moq-dev/moq/pull/6395)). + +Public API: none. Wire: none; refuses a stream it misread before. diff --git a/quest/m0/smoke-released.md b/quest/m0/smoke-released.md new file mode 100644 index 00000000000..1c27fad9cb4 --- /dev/null +++ b/quest/m0/smoke-released.md @@ -0,0 +1,32 @@ +# [S] Smoke tests the released packages + +## Goal + +The Smoke workflow passes on main. Its clients call only the API of the +latest published release, so a main-only API never breaks it, and every +red cell is fixed at its cause. + +## Plan + +Decided 2026-10-27 (maintainer), from +[moq-dev/moq#6879](https://github.com/moq-dev/moq/pull/6879): smoke clients +use only released APIs, and a release cut moves them onto new ones. Smoke +has failed on main on every run since at least 2026-10-24 (for example +[run 40591351260](https://github.com/moq-dev/moq/actions/runs/40591351260)). + +- Go (`test/smoke/clients/go/smoke.go`) calls `moq.AnnounceRoute`, and C + (`test/smoke/clients/c/subscribe.c`) calls `moq_connection_state` and + `moq_media_read`/`MOQ_DECODE_READ_FRAME`. None are on `release`. Revert + them to the released API. +- Record in the release-cut process (CONTRIBUTING or the release skill, + wherever the cut's checklist lives) that the cut updates smoke clients to + the new API. +- Cloudflare fails with "missing fields `end_of_group` and `status`" in + `moq_transport::serve::Datagram`. Find the cause and fix it. +- Upstream Docker fails because the nix build lacks the `rs/moq-net-tests` + workspace member. [moq-dev/moq#6931](https://github.com/moq-dev/moq/pull/6931) + may fix it; confirm after it lands. +- `freshness.sh` already enforces Go `@latest` and the newest `libmoq`; + keep it. + +Public API: none. Wire: none. diff --git a/quest/m1/README.md b/quest/m1/README.md index d6efb7f4888..67f5da0c6b8 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -168,6 +168,7 @@ surface and zero-copy texture import, land before the cut. - [fMP4 boundary sync](/quest/m1/fmp4-boundary-sync.md) - a declared fMP4 boundary opens a passthrough group only on a sync sample - [Segment cuts hold bounded state](/quest/m1/segmenter-cuts.md) - the HLS segmenter keeps one pending cut, so `styp` input without sync samples cannot grow it - [Next Object on a cold Rust copy](/quest/m1/next-object-cold-copy.md) - a join after an upstream group report starts at the first frame received, never skipping +- [Container consumers buffer by backpressure](/quest/m1/container-backpressure.md) - Rust and JS container consumers decode ahead only a byte budget, so a hostile publisher cannot grow them - [One moq-ffi build definition](/quest/m1/ffi-build-flags.md) - every binding takes moq-ffi's default features from one place - [A reported drop means the same everywhere](/quest/m1/resume-drop-final.md) - Rust and JS agree whether a group reported dropped can still be served - [moq-clock moves into kio::clock](/quest/m1/kio-clock.md) - moq-clock and its test macro become `kio::clock` and `#[kio::test]` before the cut publishes them diff --git a/quest/m1/api-audit/README.md b/quest/m1/api-audit/README.md index 0f321a812c5..5d3565b75ce 100644 --- a/quest/m1/api-audit/README.md +++ b/quest/m1/api-audit/README.md @@ -77,7 +77,7 @@ Rejected in planning: - [Binding publications](/quest/m1/api-audit/binding-publication.md) - moq-ffi, the C ABI, and every wrapper hand out a publication handle that announces, replacing the private adapters - [Demand is wanted, everywhere](/quest/m1/api-audit/wanted.md) - every `used`/`unused` pair becomes `wanted()` and `wanted_changed()`, and unused ones go - [One meaning for Credential](/quest/m1/api-audit/dial-credential.md) - the internal dial type gets its own name in JS and moq-tokio, and the `Presented` alias goes -- [One strict path rule](/quest/m1/api-audit/strict-paths.md) - `Relative` refuses empty segments, the error is `path::Error`, and auth mints only strict paths +- [One strict path rule](/quest/m1/api-audit/strict-paths.md) - `Relative` refuses empty segments, `Path` refuses non-canonical escapes, the error is `path::Error`, and auth mints only strict paths - [Unix sockets accept through the allowlist](/quest/m1/api-audit/unix-accept.md) - the public pre-allowlist `unix::Listener::accept` becomes crate-private ## Related diff --git a/quest/m1/api-audit/strict-paths.md b/quest/m1/api-audit/strict-paths.md index 7d05cc2f198..49f46b95665 100644 --- a/quest/m1/api-audit/strict-paths.md +++ b/quest/m1/api-audit/strict-paths.md @@ -1,9 +1,9 @@ -# [S] One strict path rule, minted and parsed +# [M] One strict path rule, minted and parsed ## Goal -`path::Relative` refuses empty segments as `Path::new` does, the path error is -`path::Error`, and moq-auth and @moq/auth mint only roots and paths that parse +`path::Relative` refuses empty segments as `Path::new` does, `Path` refuses a +non-canonical escape, the path error is `path::Error`, and moq-auth and @moq/auth mint only roots and paths that parse strictly, so a token can never grant a path no peer can spell. ## Plan @@ -22,8 +22,24 @@ Decided 2026-10-27 (maintainer): - moq-auth and @moq/auth refuse to mint a root or path that would not parse strictly, in Rust and JS alike. +Decided 2026-10-27 (maintainer), from +[moq-dev/moq#6927](https://github.com/moq-dev/moq/pull/6927): the moq-transport +namespace mapping must be symmetric, so `Path` refuses a non-canonical escape on +every wire rather than only at the IETF boundary. + +- Today `to_tuple` (`rs/moq-net/src/ietf/namespace.rs`, and `toTuple` in + `js/net/src/ietf/namespace.ts`) keeps a lone `\x` as `\x`, while `from_tuple` + escapes every backslash, so the path returns as `\\x`. Both spellings are + valid paths today, and one tuple field can decode back to only one of them. +- `Path` (and `Relative`) accept a backslash only before `\` or `/`, a new + `path::Error` variant otherwise; `from_tuple`'s escape-every-backslash form + stays canonical. +- Add a round-trip property test in Rust and JS: `from_tuple(to_tuple(p)) == p` + for every valid path. + Public API: breaking for moq-net (`path::Invalid` becomes `path::Error`, -`Relative::new` fails on empty segments), moq-auth, and @moq/auth. Mirror the +`Relative::new` fails on empty segments, and both refuse a non-canonical +escape), moq-auth, and @moq/auth. Mirror the names in JS and the bindings. Wire: none for moq-lite, but the hang catalog carries `Relative` in JSON (`broadcast`, `echo.path`), so a catalog spelling one with a leading, trailing, or doubled `/` stops parsing. Our publishers never diff --git a/quest/m1/container-backpressure.md b/quest/m1/container-backpressure.md new file mode 100644 index 00000000000..67a55946011 --- /dev/null +++ b/quest/m1/container-backpressure.md @@ -0,0 +1,43 @@ +# [M] Container consumers buffer by backpressure + +## Goal + +A hostile publisher cannot make a container consumer hold unbounded decoded +frames. `@moq/hang`'s `container/consumer.ts` and moq-mux's +`container/consumer.rs` decode ahead at most a byte budget of frames; past +it, reads wait, and the undecoded data stays in the net cache, which the +pool already bounds. Many frames packed into one timestamp, or many groups +in flight, no longer grow the consumer. + +## Plan + +Decided 2026-10-27 (maintainer), from +[moq-dev/moq#6903](https://github.com/moq-dev/moq/pull/6903): bound by +backpressure in Rust and JS together, not a frame-count cap that refuses a +group. Release keeps the gap until the next cut; no backport. + +Today both consumers bound only by timestamp span: + +- JS pushes every decoded sample (`group.frames.push` in + `js/hang/src/container/consumer.ts`), and `#checkMaxDelay` measures the + span across at least two groups, so frames that share a timestamp have + zero span and are never skipped. +- Rust buffers a `VecDeque<(Frame, Instant)>` per group + (`rs/moq-mux/src/container/consumer.rs`) bounded by span and elapsed + time only. +- The net layers cap a group (32 MiB, 8192 frames) but not how many groups + a consumer holds, and one CMAF wire frame can decode to many samples. + +Steps: + +- Charge decoded frames to a per-consumer byte budget and stop pulling from + net groups while it is full, so pressure reaches the net cache and QUIC + flow control instead of a refusal. Keep the existing span skip for + latency. +- Mirror names across Rust and JS. +- Test a publisher that packs many frames into one timestamp, and one that + opens many groups: memory stays near the budget and playback still skips + to the live edge. + +Public API: the consumer config may gain a budget field; mirror it in JS. +Wire: none. diff --git a/quest/m1/quic-api/README.md b/quest/m1/quic-api/README.md index 09e38a29eb6..ac112a18b76 100644 --- a/quest/m1/quic-api/README.md +++ b/quest/m1/quic-api/README.md @@ -50,6 +50,7 @@ side by side and fix any name or signature that still differs, and check - [Streams](/quest/m1/quic-api/streams.md) - `send::`/`recv::` namespaces, one error per direction, `finish(self) -> send::Finished`, consuming `reset`/`stop`, `Buf` writes, both drivers mirrored - [Crypto](/quest/m1/quic-api/crypto.md) - crypto traits sealed to rustls, concrete peer identity and handshake data - [Config](/quest/m1/quic-api/config.md) - plain config structs, `Limits` as the one spelling, one endpoint constructor and `connect` that own the socket, v1-only `Version`, a local-close event +- [Incoming is not a future](/quest/m1/quic-api/incoming-future.md) - moq-tokio's `Incoming` drops `IntoFuture`, so no `futures` type is public - [Waits](/quest/m1/quic-api/waits.md) - moq-quic owns every wait as a `poll_*` on `&kio::Waiter`, deleting `StreamEvent` and the drivers' and qmux's waiter tables - [Poll forms](/quest/m1/quic-api/poll.md) - both drivers' waits poll as `&self, &kio::Waiter` forwards to moq-quic, the async forms wrap them, and `send::Finished` is a future - [moq-net polls on kio waiters](/quest/m1/quic-api/net-poll.md) - moq-net's `transport::poll` traits take `&kio::Waiter`, `Session` ops take `&self`, and `Session` drops `Clone` diff --git a/quest/m1/quic-api/incoming-future.md b/quest/m1/quic-api/incoming-future.md new file mode 100644 index 00000000000..0c6ca51d07d --- /dev/null +++ b/quest/m1/quic-api/incoming-future.md @@ -0,0 +1,26 @@ +# [XS] Incoming is not a future + +## Goal + +moq-tokio's `Incoming` no longer implements `IntoFuture`, so its public API +names no `futures` type. A server accepts with `incoming.accept()?.await`, +which awaits the named `Connecting` future. + +## Plan + +Decided 2026-10-27 (maintainer), from +[moq-dev/moq#6841](https://github.com/moq-dev/moq/pull/6841): delete the impl +rather than name a new future or fold an immediate refusal into +`Connecting`. The impl came from quinn and glues `accept()`'s two outcomes +with `futures::future::Either` (`rs/moq-tokio/src/quic/driver/incoming.rs`). +The only direct `.await` on an `Incoming` is one test +(`rs/moq-tokio/tests/integration/receive_credit.rs`); every other caller +already calls `accept()`. + +- Delete `impl IntoFuture for Incoming` and the `Either`/`Ready` imports. +- Rewrite the test to `accept()?.await`. +- Check moq-uring's driver for the same impl and drop it too if present. +- Fix any docs or examples that await an `Incoming` directly. + +Public API: breaking for moq-tokio (`Incoming` is no longer `IntoFuture`); +migrate with `incoming.accept()?.await`. Wire: none. diff --git a/quest/m1/ts-reliability/README.md b/quest/m1/ts-reliability/README.md index e57b61e455f..c908d9e6d33 100644 --- a/quest/m1/ts-reliability/README.md +++ b/quest/m1/ts-reliability/README.md @@ -101,6 +101,7 @@ catalog linger (#5305). - [PAT changes](/quest/m1/ts-reliability/pat-lifecycle.md) - a revised PAT never leaves a broadcast meaning another program - [TS workflow passes](/quest/m1/ts-reliability/validation-green.md) - the scheduled TS lanes pass on main again, with no lane loosened - [Export SI order](/quest/m1/ts-reliability/export-si-order.md) - a TS export sends SI and media after its PMT and a PCR interval, so a hop keeps first emissions +- [A stitched switch returns to --delay](/quest/m1/ts-reliability/stitch-catch-up.md) - `--stitch` drops the old program's tail once the replacement acquires, so latency settles back to `--delay` ## Related diff --git a/quest/m1/ts-reliability/stitch-catch-up.md b/quest/m1/ts-reliability/stitch-catch-up.md new file mode 100644 index 00000000000..7451bc2a113 --- /dev/null +++ b/quest/m1/ts-reliability/stitch-catch-up.md @@ -0,0 +1,33 @@ +# [M] A stitched switch returns to --delay + +## Goal + +After `moq export ts --stitch` switches to a replacement publisher instance, +end-to-end latency returns to `--delay` once the replacement acquires, +instead of carrying the old program's tail as added latency. + +## Plan + +Decided 2026-10-27 (maintainer), from +[moq-dev/moq#6919](https://github.com/moq-dev/moq/pull/6919): drop the old +tail, not time-compress. The switch is already a TS program change, so the +skip costs nothing extra. + +- Today `commit` in `rs/moq-mux/src/jitter.rs` starts the replacement past + the old `horizon`, and the anchor only moves forward. The only catch-up is + clock steering capped at about 30 ppm (`MAX_DRIFT`, `MAX_SLEW`), so the + 3.9 s, 3.6 s, and 7.5 s measured in #6919's run would take about a day + and a half each to remove. +- Once the replacement acquires, cut the old program's queued tail and + start the new program at `--delay`. Keep output monotonic across the + program change. +- Test repeated switches: latency settles back to `--delay` after each, + rather than accumulating. +- Also reproduce #6919's priority observation: a mid-stream + `Subscriber::update` demotion of the old leg still showed priority 0 on + the receive side. Fix it if update propagation is wrong; otherwise + explain it in the PR. +- Update `doc/bin/cli.md` and `doc/setup/redundancy.md` inline if they + describe switch latency. + +Public API: none expected. Wire: none.