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 cd8e8a09d7..79dbd028b3 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 @@ -198,3 +199,5 @@ 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 +- [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/ffi-accept-cancel.md b/quest/m1/ffi-accept-cancel.md new file mode 100644 index 0000000000..51799376dc --- /dev/null +++ b/quest/m1/ffi-accept-cancel.md @@ -0,0 +1,30 @@ +# [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. 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` +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..09654394ec --- /dev/null +++ b/quest/m1/ffi-shape/track-request-demand.md @@ -0,0 +1,33 @@ +# [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. 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 +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 +`just test interop --all`. Wire: none. + +## Related + +- [Bindings](/quest/m0/broadcast-epoch/bindings.md) - edits the same wrappers; land after 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 93f0d160bc..06b7b1e0d9 100644 --- a/quest/m1/test-flakes-2/README.md +++ b/quest/m1/test-flakes-2/README.md @@ -35,4 +35,5 @@ Public API: none. Wire: none. - [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 - [Cluster burst overrun](/quest/m1/test-flakes-2/cluster-burst-overrun.md) - the impaired cluster burst drill always overruns its bottleneck, so it always exercises it +- [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..50ba913edd --- /dev/null +++ b/quest/m1/test-flakes-2/ts-passthrough-jump.md @@ -0,0 +1,21 @@ +# [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): 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 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. diff --git a/quest/m1/watch-follow-prefix.md b/quest/m1/watch-follow-prefix.md new file mode 100644 index 0000000000..df7357422d --- /dev/null +++ b/quest/m1/watch-follow-prefix.md @@ -0,0 +1,29 @@ +# [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 the shared follow helper. + +## 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. 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 + 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. + +Public API: none. Wire: none.