From 87ed38de99ba44bfa87cb9babd3b5541937b67ee Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 9 Oct 2026 17:33:50 -0700 Subject: [PATCH 1/7] quest: plan the moq-ffi accept-cancel and track request demand follow-ups Co-Authored-By: Claude Opus 5.5 --- quest/m1/README.md | 1 + quest/m1/ffi-accept-cancel.md | 29 ++++++++++++++++++++++ quest/m1/ffi-shape/README.md | 1 + quest/m1/ffi-shape/track-request-demand.md | 26 +++++++++++++++++++ 4 files changed, 57 insertions(+) create mode 100644 quest/m1/ffi-accept-cancel.md create mode 100644 quest/m1/ffi-shape/track-request-demand.md diff --git a/quest/m1/README.md b/quest/m1/README.md index edd298c45d..7b765a7ab3 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -70,6 +70,7 @@ ladders. - [END_OF_TRACK placement](/quest/m1/ietf-end-of-track-placement.md) - END_OF_TRACK rides the upstream's Location, and Rust's header stops claiming END_OF_GROUP - [moq-uring tests under load](/quest/m1/uring-tests-under-load.md) - uring tests pass while parallel checks share locked memory - [FFI runtime](/quest/m1/ffi-runtime.md) - moq-ffi drives moq on a multi-thread runtime instead of one thread +- [Cancelled accept](/quest/m1/ffi-accept-cancel.md) - a cancelled moq-ffi `MoqServer::accept` never takes a session, so the server path drops its spawned task - [Audio group duration](/quest/m1/audio-group-duration.md) - audio groups span at least 20 ms by default, so small frames don't mint a group each - [Pipelined requests](/quest/m1/pipeline-requests/README.md) - SUBSCRIBE and the first FETCH go out with the track-info request at every hop, so first data arrives a round trip sooner per hop - [A spinning loop fails a sim test](/quest/m1/spinning-loops.md) - a sim test names any loop that holds a task poll too long, and each one yields through a budget diff --git a/quest/m1/ffi-accept-cancel.md b/quest/m1/ffi-accept-cancel.md new file mode 100644 index 0000000000..3560454e07 --- /dev/null +++ b/quest/m1/ffi-accept-cancel.md @@ -0,0 +1,29 @@ +# [S] A cancelled moq-ffi accept never takes a session + +## Goal + +Cancelling a pending `MoqServer::accept` in any binding never takes an +incoming session: the next `accept` gets it. `MoqServer::cancel` still +releases the bound port, and every server call then resolves `Cancelled`. + +## Plan + +Found in #5140 (ffi-cancel-read), decided 2026-10-09. That PR made +`Task::run` await in place, so a cancelled future that is never polled again +makes no progress. `MoqServer::listen` and `accept` kept the spawned path +(`Task::spawn`) because `MoqServer::cancel` blocks its thread until the +in-flight call finishes, and an `accept` parked on that same thread would +never finish (`server_cancel_releases_the_bound_port` hangs). So an `accept` +that is cancelled but not yet freed (uniffi frees it later, at +`rust_future_free`) can still take a session that then goes nowhere. + +- Decided: close the listener through a handle kept outside the `Task` lock, + so `cancel` stops it without waiting on the in-flight call. Then `listen` + and `accept` use `run` like every other call, and `Task::spawn` goes away. + Rejected: accepting the race and documenting it. +- Regression in moq-ffi: poll an `accept` once and stop without freeing it, + connect a client, free the cancelled accept, and require the next `accept` + to return that session. Keep `server_cancel_releases_the_bound_port` + passing. + +Public API: none. Wire: none. diff --git a/quest/m1/ffi-shape/README.md b/quest/m1/ffi-shape/README.md index e226be5520..74e41771d2 100644 --- a/quest/m1/ffi-shape/README.md +++ b/quest/m1/ffi-shape/README.md @@ -76,4 +76,5 @@ runs `just test interop --all`. - [Named error fields](/quest/m1/ffi-shape/error-fields.md) - `MoqError` variants name their fields, so no binding exposes a positional `v1` - [Bindings](/quest/m0/broadcast-epoch/bindings.md) - the wrappers expose epochs and rename `session.epoch()`, on the reshaped wrappers - [Group request demand](/quest/m1/ffi-shape/group-request-demand.md) - `MoqGroupRequest::demand()` in moq-ffi and every wrapper +- [Track request demand](/quest/m1/ffi-shape/track-request-demand.md) - `MoqTrackRequest::demand()` in moq-ffi and every wrapper, after Bindings - [Layers guide](/quest/m1/ffi-shape/layers-guide.md) - a `doc/lib` page maps each Rust layer to every binding's module, once Codecs adds `audio` and `video` diff --git a/quest/m1/ffi-shape/track-request-demand.md b/quest/m1/ffi-shape/track-request-demand.md new file mode 100644 index 0000000000..2730e31a03 --- /dev/null +++ b/quest/m1/ffi-shape/track-request-demand.md @@ -0,0 +1,26 @@ +# [S] A track request reports its demand + +## Goal + +`MoqTrackRequest` gains `demand()` in moq-ffi and every wrapper (Python, Go, +Swift, Kotlin, Dart, C++), returning a `MoqTrackDemand` like +`MoqTrackProducer.demand()`, so a dynamic track server can see when nobody +still wants the track and stop producing it. Mirrors Rust's +`track::Request::demand`. + +## Plan + +Found in #5139 (group request demand), decided 2026-10-09: that quest +assumed `MoqTrackRequest.demand()` already existed, but moq-ffi only has +`demand()` on `MoqTrackProducer` and the media and JSON producers. Follow +#5139's `MoqGroupRequest.demand()` shape, including returning `Closed` once +the request is answered or aborted. + +Additive in every binding. It edits the same wrappers as +[Bindings](/quest/m0/broadcast-epoch/bindings.md) (#5146), so land it after +that PR to avoid conflicts, without blocking on it. Run +`just test interop --all`. Wire: none. + +## Related + +- [Bindings](/quest/m0/broadcast-epoch/bindings.md) - edits the same wrappers; land after it From b6f5267a7607229427d8b6438bd6e9acf9239075 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 9 Oct 2026 17:52:00 -0700 Subject: [PATCH 2/7] quest: plan the watch prefix follow and the player republish test Co-Authored-By: Claude Opus 5.5 --- quest/m0/broadcast-epoch/README.md | 4 ++++ quest/m1/README.md | 1 + quest/m1/watch-follow-prefix.md | 24 ++++++++++++++++++++++++ 3 files changed, 29 insertions(+) create mode 100644 quest/m1/watch-follow-prefix.md diff --git a/quest/m0/broadcast-epoch/README.md b/quest/m0/broadcast-epoch/README.md index 956eb80694..f1d32e8ddd 100644 --- a/quest/m0/broadcast-epoch/README.md +++ b/quest/m0/broadcast-epoch/README.md @@ -84,6 +84,10 @@ epoch while the old publisher's session stays open. A lite-07 viewer and a lite- that follow the announce `Restart` (or END then START) both reach the new epoch within one RTT-scale bound rather than the idle timeout, and killing the newest epoch falls back to a still-live older one. +The same test drives the real players (decided 2026-10-09, from #5154): +`moq play` and a browser `@moq/watch`, the latter through the `just test +media` lane, both show the new run within that bound, without a manual +republish against a live relay. When the release cut carries stats epochs, drop [#4810](https://github.com/moq-dev/moq/pull/4810)'s wall-clock group seed and diff --git a/quest/m1/README.md b/quest/m1/README.md index 7b765a7ab3..b60511b5c3 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -199,3 +199,4 @@ ladders. - [Catalog colour](/quest/m1/color-catalog.md) - the catalog describes a rendition's colour and HDR properties, which the WebGPU HDR renderer reads - [WebGPU HDR](/quest/m1/webgpu-hdr.md) - HDR renditions play as HDR where the browser and display can show it, and tone-map to SDR elsewhere - [Request ID order](/quest/m1/request-id-order.md) - drafts 14 to 16 refuse a reused or lower Request ID in both languages +- [Watch follows a covering prefix](/quest/m1/watch-follow-prefix.md) - `@moq/watch` moves to a covering prefix when the exact route ends, as `moq play` does diff --git a/quest/m1/watch-follow-prefix.md b/quest/m1/watch-follow-prefix.md new file mode 100644 index 0000000000..b961d1ddb1 --- /dev/null +++ b/quest/m1/watch-follow-prefix.md @@ -0,0 +1,24 @@ +# [S] @moq/watch follows the route that serves its path + +## Goal + +When the exact route for a path ends while a covering prefix (such as +`pool`) still serves it, `@moq/watch` moves to the prefix instead of going +offline, as Rust's `moq play` does through `moq_mux::Source::follow`. + +## Plan + +Found in #5154 (broadcast-epoch apps), decided 2026-10-09: +`Broadcast.#runBroadcast` in `js/watch` ignores `End`, so the player goes +offline even though a covering prefix still announces the path. + +- Decided: mirror `Source::follow`'s reduction inside `@moq/watch`'s + `Broadcast`. The serving route is the most specific one covering the path; + another route taking over is a `Restart`, or an `Update` when both carry + the same epoch; routes beneath the path are ignored. Rejected: a shared + js/net helper, until a second JS consumer needs it. +- Test: a path served both exactly and by a `pool` prefix; ending the exact + route moves playback to the prefix as a restart, and ending both goes + offline. + +Public API: none. Wire: none. From eccedd888f92f80fabba95019d88fa29aef26929 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 9 Oct 2026 17:54:04 -0700 Subject: [PATCH 3/7] quest: plan the ts passthrough flake and catalog drop; follow helper moves to net Co-Authored-By: Claude Opus 5.5 --- quest/m1/README.md | 1 + quest/m1/import-catalog-drop.md | 18 +++++++++++++++++ quest/m1/test-flakes-2/README.md | 1 + quest/m1/test-flakes-2/ts-passthrough-jump.md | 20 +++++++++++++++++++ quest/m1/watch-follow-prefix.md | 16 +++++++++------ 5 files changed, 50 insertions(+), 6 deletions(-) create mode 100644 quest/m1/import-catalog-drop.md create mode 100644 quest/m1/test-flakes-2/ts-passthrough-jump.md diff --git a/quest/m1/README.md b/quest/m1/README.md index b60511b5c3..74d488be4b 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -200,3 +200,4 @@ ladders. - [WebGPU HDR](/quest/m1/webgpu-hdr.md) - HDR renditions play as HDR where the browser and display can show it, and tone-map to SDR elsewhere - [Request ID order](/quest/m1/request-id-order.md) - drafts 14 to 16 refuse a reused or lower Request ID in both languages - [Watch follows a covering prefix](/quest/m1/watch-follow-prefix.md) - `@moq/watch` moves to a covering prefix when the exact route ends, as `moq play` does +- [Import catalog drop](/quest/m1/import-catalog-drop.md) - moq-cli import never drops a catalog producer without finishing it diff --git a/quest/m1/import-catalog-drop.md b/quest/m1/import-catalog-drop.md new file mode 100644 index 0000000000..32a6fd1892 --- /dev/null +++ b/quest/m1/import-catalog-drop.md @@ -0,0 +1,18 @@ +# [XS] moq-cli import finishes its catalog producer + +## Goal + +No moq-cli import path drops a catalog `track::Producer` without `finish()` +or `abort()`. The warning seen in about 1 of 7 loaded runs of +`import_delivers_the_catalog_finish_at_eof` is traced to its producer and +fixed, or shown harmless and the warning's cause removed. + +## Plan + +Found in #5155, decided 2026-10-09: under load the test sometimes logs +`track::Producer dropped without finish() or abort() track=catalog.json`, +while the subscriber still sees a clean catalog finish. So some second +catalog producer, or a clone, is dropped on a path that skips `finish()`. +Find which one and finish or abort it at its source. + +Public API: none. Wire: none. diff --git a/quest/m1/test-flakes-2/README.md b/quest/m1/test-flakes-2/README.md index b0fc70f158..5fe00724fb 100644 --- a/quest/m1/test-flakes-2/README.md +++ b/quest/m1/test-flakes-2/README.md @@ -36,4 +36,5 @@ Public API: none. Wire: none. - [Import catalog finish](/quest/m1/test-flakes-2/import-catalog-finish.md) - `moq-cli`'s subprocess EOF catalog-finish test holds up under load with event-based fixture coordination - [Relay restart rebind](/quest/m1/test-flakes-2/relay-restart-rebind.md) - the crash drill restarts on its original UDP address under concurrent load - [Impaired handshake](/quest/m1/test-flakes-2/impaired-handshake.md) - the impaired cluster drills' clients never time out while connecting +- [TS passthrough jump](/quest/m1/test-flakes-2/ts-passthrough-jump.md) - moq-cli's flagged-jump TS passthrough test passes under load without wall-clock pacing - [Media audio tone](/quest/m1/media-audio-tone.md) - the `just test media` audio-tone check passes under load, fixed at its cause diff --git a/quest/m1/test-flakes-2/ts-passthrough-jump.md b/quest/m1/test-flakes-2/ts-passthrough-jump.md new file mode 100644 index 0000000000..37e7b6615a --- /dev/null +++ b/quest/m1/test-flakes-2/ts-passthrough-jump.md @@ -0,0 +1,20 @@ +# [S] TS passthrough jump test holds under load + +## Goal + +`publish::tests::ts_passthrough_crosses_a_relay_through_a_flagged_jump` +(`rs/moq-cli`) passes under a loaded `just check`, fixed at its cause. + +## Plan + +Seen 2026-10-09 by three PRs on unmodified `main` (#5146, #5155, #5153): +"moq-transport-14: both copies crossed", failing 2 of 3 loaded runs and +passing alone. The test came in with #5003. It paces its input with a 15 ms +wall-clock sleep and waits up to 10 s. + +Decided 2026-10-09: find why both copies cross under load before changing +anything, then replace the wall-clock pacing with mocked time or observable +events where the fixture allows. Never widen the wait or add a retry, per +this questline's rules. + +Public API: none. Wire: none. diff --git a/quest/m1/watch-follow-prefix.md b/quest/m1/watch-follow-prefix.md index b961d1ddb1..eb54d827cd 100644 --- a/quest/m1/watch-follow-prefix.md +++ b/quest/m1/watch-follow-prefix.md @@ -4,7 +4,7 @@ When the exact route for a path ends while a covering prefix (such as `pool`) still serves it, `@moq/watch` moves to the prefix instead of going -offline, as Rust's `moq play` does through `moq_mux::Source::follow`. +offline, as Rust's `moq play` does through the shared follow helper. ## Plan @@ -12,11 +12,15 @@ Found in #5154 (broadcast-epoch apps), decided 2026-10-09: `Broadcast.#runBroadcast` in `js/watch` ignores `End`, so the player goes offline even though a covering prefix still announces the path. -- Decided: mirror `Source::follow`'s reduction inside `@moq/watch`'s - `Broadcast`. The serving route is the most specific one covering the path; - another route taking over is a `Restart`, or an `Update` when both carry - the same epoch; routes beneath the path are ignored. Rejected: a shared - js/net helper, until a second JS consumer needs it. +- Decided: use the shared follow helper. #5154 moves it from + `moq_mux::Source::follow` into moq-net (`origin::Consumer::follow`) and + mirrors it in `@moq/net`, per the cross-package table; this quest wires + `@moq/watch`'s `Broadcast` onto the JS one. The serving route is the most + specific one covering the path; another route taking over is a `Restart`, + or an `Update` when both carry the same epoch; routes beneath the path are + ignored. Rejected: a private copy in `@moq/watch`, and keeping the Rust + helper in moq-mux (maintainer, 2026-10-09: a net-layer concern that should + mirror across languages). - Test: a path served both exactly and by a `pool` prefix; ending the exact route moves playback to the prefix as a restart, and ending both goes offline. From c9820eaeb5285848d29727432d1c1e03c6435e71 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 9 Oct 2026 18:09:48 -0700 Subject: [PATCH 4/7] quest: accept-cancel starts after #5140; track request demand outlives accept Co-Authored-By: Claude Opus 5.5 --- quest/m1/ffi-accept-cancel.md | 7 ++++--- quest/m1/ffi-shape/track-request-demand.md | 7 +++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/quest/m1/ffi-accept-cancel.md b/quest/m1/ffi-accept-cancel.md index 3560454e07..51799376dc 100644 --- a/quest/m1/ffi-accept-cancel.md +++ b/quest/m1/ffi-accept-cancel.md @@ -8,9 +8,10 @@ releases the bound port, and every server call then resolves `Cancelled`. ## Plan -Found in #5140 (ffi-cancel-read), decided 2026-10-09. That PR made -`Task::run` await in place, so a cancelled future that is never polled again -makes no progress. `MoqServer::listen` and `accept` kept the spawned path +Found in #5140 (ffi-cancel-read), decided 2026-10-09. Start after #5140 +lands: on `main` today `Task::run` still spawns every call and there is no +`Task::spawn`. That PR makes `Task::run` await in place, so a cancelled +future that is never polled again makes no progress. `MoqServer::listen` and `accept` keep the spawned path (`Task::spawn`) because `MoqServer::cancel` blocks its thread until the in-flight call finishes, and an `accept` parked on that same thread would never finish (`server_cancel_releases_the_bound_port` hangs). So an `accept` diff --git a/quest/m1/ffi-shape/track-request-demand.md b/quest/m1/ffi-shape/track-request-demand.md index 2730e31a03..6335f9fc26 100644 --- a/quest/m1/ffi-shape/track-request-demand.md +++ b/quest/m1/ffi-shape/track-request-demand.md @@ -13,8 +13,11 @@ still wants the track and stop producing it. Mirrors Rust's Found in #5139 (group request demand), decided 2026-10-09: that quest assumed `MoqTrackRequest.demand()` already existed, but moq-ffi only has `demand()` on `MoqTrackProducer` and the media and JSON producers. Follow -#5139's `MoqGroupRequest.demand()` shape, including returning `Closed` once -the request is answered or aborted. +#5139's `MoqGroupRequest.demand()` binding shape, but keep Rust's lifetime: +`accept` hands the same track state to the producer, so the handle keeps +watching it and returns `Closed` only once the request is rejected or the +track closes. A dynamic server can then stop producing when the last +subscriber leaves. Additive in every binding. It edits the same wrappers as [Bindings](/quest/m0/broadcast-epoch/bindings.md) (#5146), so land it after From 65bc9ef15e037ea0c169f8ed6c3d317460c72174 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 9 Oct 2026 18:31:35 -0700 Subject: [PATCH 5/7] quest: track request demand covers moq-c and doc/lib Co-Authored-By: Claude Opus 5.5 --- quest/m1/ffi-shape/track-request-demand.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/quest/m1/ffi-shape/track-request-demand.md b/quest/m1/ffi-shape/track-request-demand.md index 6335f9fc26..7217a0805f 100644 --- a/quest/m1/ffi-shape/track-request-demand.md +++ b/quest/m1/ffi-shape/track-request-demand.md @@ -19,6 +19,10 @@ watching it and returns `Closed` only once the request is rejected or the track closes. A dynamic server can then stop producing when the last subscriber leaves. +Per the cross-package table, `rs/moq-c` gains the same on its +`moq_track_request_*` functions (following `moq_publish_media_demand`), and +the `doc/lib` pages list it. + Additive in every binding. It edits the same wrappers as [Bindings](/quest/m0/broadcast-epoch/bindings.md) (#5146), so land it after that PR to avoid conflicts, without blocking on it. Run From 63e3ac4b2c115d315176fe7eb80776608a5d89a6 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 9 Oct 2026 18:42:25 -0700 Subject: [PATCH 6/7] quest: flake plan names the missing copy; track demand starts after #5139 Co-Authored-By: Claude Opus 5.5 --- quest/m1/ffi-shape/track-request-demand.md | 12 ++++++------ quest/m1/test-flakes-2/ts-passthrough-jump.md | 15 ++++++++------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/quest/m1/ffi-shape/track-request-demand.md b/quest/m1/ffi-shape/track-request-demand.md index 7217a0805f..09654394ec 100644 --- a/quest/m1/ffi-shape/track-request-demand.md +++ b/quest/m1/ffi-shape/track-request-demand.md @@ -12,12 +12,12 @@ still wants the track and stop producing it. Mirrors Rust's Found in #5139 (group request demand), decided 2026-10-09: that quest assumed `MoqTrackRequest.demand()` already existed, but moq-ffi only has -`demand()` on `MoqTrackProducer` and the media and JSON producers. Follow -#5139's `MoqGroupRequest.demand()` binding shape, but keep Rust's lifetime: -`accept` hands the same track state to the producer, so the handle keeps -watching it and returns `Closed` only once the request is rejected or the -track closes. A dynamic server can then stop producing when the last -subscriber leaves. +`demand()` on `MoqTrackProducer` and the media and JSON producers. Start +after #5139 lands, and follow its `MoqGroupRequest.demand()` binding shape, +but keep Rust's lifetime: `accept` hands the same track state to the +producer, so the handle keeps watching it and returns `Closed` only once the +request is rejected or the track closes. A dynamic server can then stop +producing when the last subscriber leaves. Per the cross-package table, `rs/moq-c` gains the same on its `moq_track_request_*` functions (following `moq_publish_media_demand`), and diff --git a/quest/m1/test-flakes-2/ts-passthrough-jump.md b/quest/m1/test-flakes-2/ts-passthrough-jump.md index 37e7b6615a..50ba913edd 100644 --- a/quest/m1/test-flakes-2/ts-passthrough-jump.md +++ b/quest/m1/test-flakes-2/ts-passthrough-jump.md @@ -7,14 +7,15 @@ ## Plan -Seen 2026-10-09 by three PRs on unmodified `main` (#5146, #5155, #5153): -"moq-transport-14: both copies crossed", failing 2 of 3 loaded runs and -passing alone. The test came in with #5003. It paces its input with a 15 ms +Seen 2026-10-09 by three PRs on unmodified `main` (#5146, #5155, #5153): the +assertion "moq-transport-14: both copies crossed" fails, so the recording +held at most one copy of the fixture. It failed 2 of 3 loaded runs and +passes alone. The test came in with #5003. It paces its input with a 15 ms wall-clock sleep and waits up to 10 s. -Decided 2026-10-09: find why both copies cross under load before changing -anything, then replace the wall-clock pacing with mocked time or observable -events where the fixture allows. Never widen the wait or add a retry, per -this questline's rules. +Decided 2026-10-09: find why the second copy goes missing under load before +changing anything, then replace the wall-clock pacing with mocked time or +observable events where the fixture allows. Never widen the wait or add a +retry, per this questline's rules. Public API: none. Wire: none. From b19d7e9852804815868f8dd463f1ab6a7ab6c0d1 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 9 Oct 2026 18:52:16 -0700 Subject: [PATCH 7/7] quest: watch prefix follow starts after #5154 Co-Authored-By: Claude Opus 5.5 --- quest/m1/watch-follow-prefix.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/quest/m1/watch-follow-prefix.md b/quest/m1/watch-follow-prefix.md index eb54d827cd..df7357422d 100644 --- a/quest/m1/watch-follow-prefix.md +++ b/quest/m1/watch-follow-prefix.md @@ -10,7 +10,8 @@ offline, as Rust's `moq play` does through the shared follow helper. Found in #5154 (broadcast-epoch apps), decided 2026-10-09: `Broadcast.#runBroadcast` in `js/watch` ignores `End`, so the player goes -offline even though a covering prefix still announces the path. +offline even though a covering prefix still announces the path. Start after +#5154 lands, since it adds the helper this quest uses. - Decided: use the shared follow helper. #5154 moves it from `moq_mux::Source::follow` into moq-net (`origin::Consumer::follow`) and