From 59c086288486b417e810b34eedce70a34ca057d1 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Thu, 1 Oct 2026 17:35:24 -0700 Subject: [PATCH 1/2] quest: plan Demand everywhere and IETF FETCH abandonment Co-Authored-By: Claude Opus 5.5 --- quest/m1/README.md | 2 ++ quest/m1/demand-everywhere.md | 44 ++++++++++++++++++++++++++++++ quest/m1/ietf-fetch-abandonment.md | 31 +++++++++++++++++++++ 3 files changed, 77 insertions(+) create mode 100644 quest/m1/demand-everywhere.md create mode 100644 quest/m1/ietf-fetch-abandonment.md diff --git a/quest/m1/README.md b/quest/m1/README.md index e096bebf5d..c92f3c6546 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -28,6 +28,7 @@ QUIC studies there on that rule. - [One route cost](/quest/m1/route-cost.md) - on `dev`, Warm and Cold collapse to one static route cost - [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` +- [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 @@ -48,6 +49,7 @@ QUIC studies there on that rule. - [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 - [FETCH abandonment](/quest/m1/fetch-abandonment.md) - Rust and JS lite subscribers close an upstream FETCH once demand leaves, so an abandoned FETCH is released across relays +- [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..5534386f8f --- /dev/null +++ b/quest/m1/demand-everywhere.md @@ -0,0 +1,44 @@ +# [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. +`track::Request` and `track::Dynamic` have `poll_unused`, and #4691 adds +`group::Request::poll_unused` on `main`. 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 diff --git a/quest/m1/ietf-fetch-abandonment.md b/quest/m1/ietf-fetch-abandonment.md new file mode 100644 index 0000000000..110bea6888 --- /dev/null +++ b/quest/m1/ietf-fetch-abandonment.md @@ -0,0 +1,31 @@ +# [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 has no +FETCH_CANCEL encoding. 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. Check which +drafts the tree negotiates still define FETCH_CANCEL, and which cancel by +stream reset. + +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. From 85574ee799f8bc4cbe99d9ce16088e51067db15a Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Thu, 1 Oct 2026 17:41:39 -0700 Subject: [PATCH 2/2] quest: address review on Demand everywhere and IETF FETCH abandonment Co-Authored-By: Claude Opus 5.5 --- quest/m1/demand-everywhere.md | 8 +++++--- quest/m1/ietf-fetch-abandonment.md | 11 ++++++----- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/quest/m1/demand-everywhere.md b/quest/m1/demand-everywhere.md index 5534386f8f..b1dd7a529f 100644 --- a/quest/m1/demand-everywhere.md +++ b/quest/m1/demand-everywhere.md @@ -16,8 +16,10 @@ 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. -`track::Request` and `track::Dynamic` have `poll_unused`, and #4691 adds -`group::Request::poll_unused` on `main`. moq-ffi wraps only `track::Demand` +`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 (still +open) adds `group::Request::poll_unused`. moq-ffi wraps only `track::Demand` (`MoqTrackDemand`); `MoqGroupRequest` and `MoqTrackRequest` have no demand. Decided (2026-10-01): @@ -41,4 +43,4 @@ 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 +- [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 index 110bea6888..6f3977b781 100644 --- a/quest/m1/ietf-fetch-abandonment.md +++ b/quest/m1/ietf-fetch-abandonment.md @@ -14,16 +14,17 @@ 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 has no -FETCH_CANCEL encoding. JS never issues an IETF FETCH (`fetchGroup` refuses on +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. Check which -drafts the tree negotiates still define FETCH_CANCEL, and which cancel by -stream reset. +[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.