Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
4b535b9
chore: claim track tail interop quest
kixelated Sep 26, 2026
f0e73a3
test(interop): reproduce finite track loss through a relay
kixelated Sep 26, 2026
ea4fae7
Merge remote-tracking branch 'origin/main' into quest/m1/track-tail-i…
kixelated Oct 3, 2026
298e882
test(interop): follow broadcast close() and track demand() renames
kixelated Oct 3, 2026
1fd7ef3
Merge remote-tracking branch 'origin/main' into quest/m1/track-tail-i…
kixelated Oct 6, 2026
a231756
fix(net): keep a lite subscription's demand until its groups drain
kixelated Oct 6, 2026
368a8fc
Merge remote-tracking branch 'origin/main' into quest/m1/track-tail-i…
kixelated Oct 6, 2026
20dc075
Merge remote-tracking branch 'origin/main' into codex/merge-finite-ta…
kixelated Oct 6, 2026
491aef5
test(net): simulate finite relay group drains
kixelated Oct 6, 2026
3c7d007
Merge remote-tracking branch 'origin/main' into codex/merge-finite-ta…
kixelated Oct 6, 2026
d00cb6a
chore: merge main into track tail interop
kixelated Oct 7, 2026
76a922c
fix(net): hold readers at a hole until the upstream tail settles
kixelated Oct 7, 2026
b6f7c18
fix(net): box the drained subscriber and document reader tail holds
kixelated Oct 7, 2026
38bf64e
fix(net): publish the wire end and its pending tail in one write
kixelated Oct 7, 2026
5abb1d1
Merge remote-tracking branch 'origin/main' into pr-4225
kixelated Oct 8, 2026
00d1bf8
Merge remote-tracking branch 'origin/main' into pr-4225
kixelated Oct 8, 2026
786c8a0
Merge remote-tracking branch 'origin/main' into merge4225-a2c6
kixelated Oct 8, 2026
2703328
test(net): check the first end stands after a refused second end
kixelated Oct 8, 2026
e82ec5f
Merge remote-tracking branch 'origin/main' into merge4225-a2c6
kixelated Oct 8, 2026
41f98d3
test(interop): publish the JS tail frames untimed
kixelated Oct 8, 2026
cabc22e
Merge origin/main into quest/m1/track-tail-interop
kixelated Oct 10, 2026
7538bf2
fix(net): an abort ends the wait for a pending tail
kixelated Oct 10, 2026
bd2f6ea
Merge remote-tracking branch 'origin/main' into iterate-4225
kixelated Oct 10, 2026
e7e3c91
test(net): drop an unneeded mut
kixelated Oct 10, 2026
91e9af8
Merge remote-tracking branch 'origin/main' into iterate-4225
kixelated Oct 10, 2026
c086d50
Merge remote-tracking branch 'origin/main' into iterate-4225
kixelated Oct 10, 2026
5726689
fix(net): fetched backfill does not fill a pending tail; tail lanes s…
kixelated Oct 10, 2026
3ab018e
test(interop): refuse --tail with released binaries or clients
kixelated Oct 10, 2026
8a9fe6d
Merge remote-tracking branch 'origin/main' into iterate-4225
kixelated Oct 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion quest/m1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ ladders.
- [Publishing never invents a timestamp](/quest/m1/publish-timestamp.md) - no Rust or binding publish API fills in a timestamp; an untimed payload goes out untimed
- [Hard fork](/quest/m1/quic/fork/README.md) - quinn hard-forked in-tree as `moq-quic`, ranked ahead of perf; the rest of the QUIC line follows it
- [Cluster routing](/quest/m1/cluster-routing/README.md) - any node routes toward a broadcast's origin over CDN and P2P links alike, with per-origin routes and path-less announces
- [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`
- [Flat questlines](/quest/m1/quest-flat-lines.md) - moq pins the current quest CLI, lands its questline branches on main, and retires them
- [main merges through a squash queue](/quest/m1/merge-queue-settings.md) - condition: the maintainer enables the squash merge queue on `main`
- [Binding audio delay](/quest/m1/binding-surface.md) - moq-ffi and every wrapper configure and observe audio playout delay
Expand Down
11 changes: 3 additions & 8 deletions quest/m1/js-pending-tail.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,17 @@
## Goal

A `@moq/net` reader of a received track waits for missing groups below a
declared end until the tail settles, as Rust readers do once #4225 lands,
declared end until the tail settles, as Rust readers do since #4225,
instead of ending at the end with groups missing (`doc/lib/js/net.md`).

## Plan

Mirror the Rust hold from [Track tail interop](/quest/m1/track-tail-interop.md)
(#4225) once it merges, and test it against the Rust behavior with mocked
time. Split from [JS session parity](/quest/m1/js-session-parity.md) on
Mirror the Rust pending-tail hold from #4225 (`rs/moq-net/src/model/track.rs`)
and test it against the Rust behavior with mocked time. Split from [JS session parity](/quest/m1/js-session-parity.md) on
2026-10-08 so the caps work stays ready.

Public API: none. Wire: none.

## Required

- [Track tail interop](/quest/m1/track-tail-interop.md) - the Rust hold this mirrors

## Related

- [JS session parity](/quest/m1/js-session-parity.md) - per-session caps, the other half
9 changes: 2 additions & 7 deletions quest/m1/subscribe-drop.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,9 @@ its header; on each version the subscriber settles without waiting out the
grace. A skipped sequence at the end is covered on lite-05 through lite-07
(`track_tail::skipped_groups_end_without_the_grace`).

Add the lite-07 drop case to the tail interop harness from
[track tail interop](/quest/m1/track-tail-interop.md): Rust and JS
Add the lite-07 drop case to the tail interop harness
(`just test interop --tail`, `test/interop/clients/*/tail.*`): Rust and JS
subscribers both settle on SUBSCRIBE_DROP through the relay, so a group the
publisher skipped or never opened ends the track without waiting out the
grace. Decided in the 2026-09-30 audit: the case moved here so the basic
tail interop could land first.


## Related

- [Track tail interop](/quest/m1/track-tail-interop.md) - the Rust-JS tail harness the lite-07 drop case extends
47 changes: 0 additions & 47 deletions quest/m1/track-tail-interop.md

This file was deleted.

4 changes: 4 additions & 0 deletions rs/moq-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,7 @@ vt100 = "0.16.2"
[[bench]]
name = "play_buffer"
harness = false

[[example]]
name = "interop-tail"
path = "../../test/interop/clients/rust/tail.rs"
5 changes: 4 additions & 1 deletion rs/moq-net/src/ietf/subscriber.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2371,6 +2371,8 @@ where
fill.end();
}
}
// The tail settled, so readers may end at an END_OF_TRACK's boundary.
track.set_tail_pending(false);
// A no-op once an END_OF_TRACK declared the end.
let _ = track.finish();
}
Expand Down Expand Up @@ -3082,7 +3084,8 @@ fn end_track(track: &mut track::Producer, end: u64) -> Result<(), Error> {
if track.final_sequence().is_some() {
return Ok(());
}
if let Err(err) = track.finish_at(end) {
// Lower groups may still be on the wire, behind the one that carried the end.
if let Err(err) = track.finish_at_pending(end) {
tracing::warn!(%err, end, "invalid END_OF_TRACK");
let _ = track.clone().abort(Error::ProtocolViolation);
return Err(Error::ProtocolViolation);
Expand Down
27 changes: 23 additions & 4 deletions rs/moq-net/src/lite/publisher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1827,7 +1827,13 @@ enum SubscribeServe<S: crate::transport::poll::Session> {
/// is moved on every transition.
Run(Box<TrackRun<S>>),
/// The track finished: draining the in-flight group streams before the FIN.
Drain { children: kio::Tasks<GroupServe<S>> },
Drain {
children: kio::Tasks<GroupServe<S>>,
/// The subscription's demand lasts until its groups drain. A relay cancels its
/// upstream subscription once nobody subscribes, and the publisher then resets
/// every group still on the wire. `None` only in passing between states.
_track: Option<Box<track::Subscriber>>,
},
}

impl<S: crate::transport::poll::Session> Request<S> for SubscribeServe<S> {
Expand Down Expand Up @@ -1893,6 +1899,7 @@ impl<S: crate::transport::poll::Session> Request<S> for SubscribeServe<S> {
self,
Self::Drain {
children: Default::default(),
_track: None,
},
)
else {
Expand Down Expand Up @@ -1958,10 +1965,22 @@ impl<S: crate::transport::poll::Session> Request<S> for SubscribeServe<S> {
if ready!(run.poll_step(writer, waiter))?.is_continue() {
return Poll::Ready(Ok(ControlFlow::Continue(())));
}
let children = std::mem::take(&mut run.children);
*self = Self::Drain { children };
let Self::Run(run) = std::mem::replace(
self,
Self::Drain {
children: Default::default(),
_track: None,
},
) else {
unreachable!()
};
let TrackRun { children, track, .. } = *run;
*self = Self::Drain {
children,
_track: Some(Box::new(track)),
};
}
Self::Drain { children } => {
Self::Drain { children, .. } => {
ready!(children.poll(waiter));
return Poll::Ready(Ok(ControlFlow::Break(())));
}
Expand Down
7 changes: 5 additions & 2 deletions rs/moq-net/src/lite/subscriber.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4515,6 +4515,7 @@ impl<S: crate::transport::poll::Session> kio::Task for TrackServeRun<S> {
}
},
ServeEnd::Finished => {
serve_loop.serving.set_tail_pending(false);
let _ = serve_loop.serving.finish();
}
ServeEnd::GiveBack(err) => {
Expand Down Expand Up @@ -4870,13 +4871,15 @@ impl<S: crate::transport::poll::Session> ServeLoop<S> {
// consumers learn the boundary early; the later stream FIN
// then finds the track already finished.
lite::SubscribeResponse::End(end) => {
// finish_at rejects a boundary at or below a group
// Lower groups may still be on the wire, behind a higher
// one, so the end holds readers at a hole until the tail
// settles. finish_at rejects a boundary at or below a group
// already received. lite-05 specified an inclusive end,
// and `@moq/net` 0.1.3 to 0.1.9 sent one, so there it
// only costs the early boundary: warn, and let the FIN
// finish the track. Later drafts made it exclusive, so
// the publisher contradicted its own end.
if let Err(err) = self.serving.finish_at(end.group) {
if let Err(err) = self.serving.finish_at_pending(end.group) {
match serve.subscriber.version {
Version::Lite05 => {
tracing::warn!(track = %serve.name, group = end.group, %err, "invalid subscribe end")
Expand Down
Loading
Loading