diff --git a/quest/m1/README.md b/quest/m1/README.md index bcec8fbae8..a29b80e341 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -31,6 +31,7 @@ QUIC studies there on that rule. - [Delete removed cluster flags](/quest/m1/cluster-shims.md) - on `dev`, once a release has carried their refusals, `mesh` and `linger` leave `cluster::Config` - [Track tail interop](/quest/m1/track-tail-interop.md) - a Rust publisher ending a track with a group in flight is read to its end by the JS subscriber, and the reverse, in `just test interop` - [moq-mux truncated group](/quest/m1/mux-truncated-group.md) - a test pins that moq-mux skips a truncated spliced group as aborted +- [Demand everywhere](/quest/m1/demand-everywhere.md) - on `dev`, requests and producers at every level watch subscribers through `demand()` alone, ahead of the bindings exposing it - [Merge queue](/quest/m1/merge-queue.md) - the required checks run on `merge_group`, so a stale green check can no longer break main - [Binding audio delay](/quest/m1/binding-surface.md) - moq-ffi and every wrapper configure and observe audio playout delay - [FFI shape](/quest/m1/ffi-shape/README.md) - the bindings mirror Rust's layers: net at the root, then media, json, flate, audio, and video namespaces built from the handle below @@ -50,6 +51,7 @@ QUIC studies there on that rule. - [Reader end parity](/quest/m1/reader-end-parity.md) - JS readers see a track's end once the newest group reaches the declared end, as Rust readers do - [JS cache window](/quest/m1/js-prune-cache-window.md) - js/net ages idle groups on a cache window and keeps max_age as media-time staleness, as Rust does - [Cross-relay bursts](/quest/m1/cross-relay-bursts.md) - bursty small-group tracks cross two relays without lost groups, unanswered FETCHes, or stalls +- [IETF FETCH abandonment](/quest/m1/ietf-fetch-abandonment.md) - the Rust moq-transport subscriber cancels a group FETCH once its last reader leaves - [Watch and publish under CSP](/quest/m1/csp-assets.md) - blob workers stay the default; strict-CSP apps host the files and set a base URL - [More tests under load](/quest/m1/test-flakes-2/README.md) - the second round of load-only failures, one quest per flake, fixed at the cause - [A week of nightly interop after #4529](/quest/m1/interop-week.md) - the nightly traces the stall diagnosis reads diff --git a/quest/m1/demand-everywhere.md b/quest/m1/demand-everywhere.md new file mode 100644 index 0000000000..9db4a1b3aa --- /dev/null +++ b/quest/m1/demand-everywhere.md @@ -0,0 +1,46 @@ +# [M] Demand is the one way to watch subscribers + +## Goal + +On `dev`, every model handle that serves subscribers watches them through a +`Demand` handle returned by `demand()`: `track::Request`, `group::Request`, +`group::Producer`, and `broadcast::Producer`. Their own +`used`/`unused`/`is_used`/`poll_unused` copies are gone, in Rust and `js/net`. +moq-ffi then exposes the same handle on its request and producer types through +the [FFI shape](/quest/m1/ffi-shape/README.md) line, so a binding's fetch +handler can see a group request abandoned. + +## Plan + +Facts (2026-10-01): `track::Demand` and `broadcast::Demand` already exist. +[#4528](https://github.com/moq-dev/moq/pull/4528) (the track-demand quest) moved +`track::Producer` to `demand()` on `dev`, and deliberately kept group and +broadcast `used`/`unused`, since group demand drives fetch coalescing. +`broadcast::Producer` is already `demand()`-only, with `used`/`unused` on +`broadcast::Demand`; there is no `group::Demand` yet. +`track::Request` and `track::Dynamic` have `poll_unused`, and #4691 added +`group::Request::poll_unused`. moq-ffi wraps only `track::Demand` +(`MoqTrackDemand`); `MoqGroupRequest` and `MoqTrackRequest` have no demand. + +Decided (2026-10-01): + +- ✅ Everything under `Demand`, reversing track-demand's call to keep group and + broadcast `used`/`unused`. Rejected: Requests only. +- ✅ The binding exposure follows this quest in the FFI shape line instead of + wrapping `poll_unused` now. Rejected: a standalone moq-ffi quest for + `poll_unused`, and waiting in m2 for a consumer. +- ✅ m1, ahead of FFI shape, so the bindings expose `Demand` once. + +- ✅ One type per level (`track::Demand`, `group::Demand`, + `broadcast::Demand`) with the same method set, like the two that exist. + Rejected: one generic `Demand`, which erases level-specific state such as + track priority. + +Keep fetch coalescing's group demand semantics, and keep `abort_unused` where +its race still needs an owner. + +Public API: breaking on `dev` in moq-net and `@moq/net`. Wire: none. + +## Related + +- [IETF FETCH abandonment](/quest/m1/ietf-fetch-abandonment.md) - first new consumer of group request demand; can ship on `poll_unused` without waiting for this diff --git a/quest/m1/ietf-fetch-abandonment.md b/quest/m1/ietf-fetch-abandonment.md new file mode 100644 index 0000000000..6f3977b781 --- /dev/null +++ b/quest/m1/ietf-fetch-abandonment.md @@ -0,0 +1,32 @@ +# [S] An abandoned IETF group FETCH is cancelled + +## Goal + +When the last reader leaves a group that the Rust moq-transport subscriber is +fetching upstream, it cancels that FETCH (STOP_SENDING plus a reset of the +request stream, or FETCH_CANCEL on drafts that define it), before and after +FETCH_OK, the same as an idle SUBSCRIBE and the lite FETCH after #4691. The +partial group aborts with `Error::Cancel` and never ends clean. + +## Plan + +Facts (`origin/main`, 2026-10-01): `run_group_fetch` in +`rs/moq-net/src/ietf/subscriber.rs` sends the FETCH and waits only for the +request stream to close or the group slot to finish. It never watches demand. +It is cancelled only when its whole subscription ends. The subscribe path +already polls `poll_unused` and calls `cancel_subscribe`. Rust already encodes +and decodes `ietf::FetchCancel` (0x17, `rs/moq-net/src/ietf/fetch.rs`) and the +adapter routes it, but `run_group_fetch` never sends it. JS never issues an IETF FETCH (`fetchGroup` refuses on +moq-transport), so it is out of scope. + +Decided (2026-10-01): cancel after the answer too, matching SUBSCRIBE; the +fetched group is not filled into the cache for absent readers. Reuse +`group::Request::poll_unused` from #4691, or `demand()` if +[Demand everywhere](/quest/m1/demand-everywhere.md) lands first. Send the +existing `FetchCancel` on the drafts that define it (14 to 16) and reset the +request stream on later drafts, which removed FETCH_CANCEL. + +Tests: a mock-time test abandons a group FETCH before and after FETCH_OK and +asserts the upstream stream is cancelled. + +Public API: none. Wire: none.