Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -107,7 +107,6 @@ explicit exception to the public model audit.
- [TS fidelity and recovery](/quest/m1/ts-reliability/README.md) - both modes have explicit profiles, automated validation, loss tests, and a dedicated guide
- [T-STD TS export](/quest/m1/tstd/README.md) - `moq export ts` is a proper remux that passes the supported T-STD buffer models, starting with a fixed `--delay`
- [Draft 14-16 updates](/quest/m1/ietf-legacy-updates.md) - Rust and JS apply and answer moq-transport 14-16 request updates without ending or leaking the request
- [Tail arrivals](/quest/m1/tail-arrivals.md) - a track judges its pending tail from recorded arrivals, not a cache scan, and errors a reader whose tail ends short
- [Resume reorder](/quest/m1/resume-reorder.md) - a group whose stream header arrives after its successor's still resumes on failover; only a dropped or long-gone group fails
- [Refuse rewinds](/quest/m1/refuse-rewinds.md) - a timed track refuses a group whose first frame is earlier than the previous group's, in Rust and js/net, on every hop
- [JS pending tail](/quest/m1/js-pending-tail.md) - a JS reader holds for a track's pending tail like Rust
Expand Down
14 changes: 7 additions & 7 deletions quest/m1/js-pending-tail.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,16 @@ 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](https://github.com/moq-dev/moq/pull/5214) on
2026-10-08 so the caps work stays ready.

Mirror the arrival record from [Tail arrivals](/quest/m1/tail-arrivals.md)
rather than #4225's cache scan (decided 2026-10-10), so JS ports the final
design once.
Mirror the Rust arrival record rather than #4225's cache scan (decided
2026-10-10): `TrackState::arrivals` in `rs/moq-net/src/model/track.rs`, a
`Ledger` (`rs/moq-net/src/model/ledger.rs`) of the live groups and datagrams
that arrived, which eviction never touches. A gap between two arrivals folds
once it is a subscription's grace old, the same rule as the session's
`tail::Tail`. A reader whose pending tail still owes a group when the track
aborts gets the abort error, not a clean end.

Public API: none. Wire: none.

## Required

- [Tail arrivals](/quest/m1/tail-arrivals.md) - Rust judges tail holes from recorded arrivals and errors a short tail

## Related

- [JS session parity](https://github.com/moq-dev/moq/pull/5214) - per-session caps, the other half
3 changes: 1 addition & 2 deletions quest/m1/public-model/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,7 @@ Rules every child follows:
growing a public shape it deletes: fetch and per-group requests
([Model ranges](/quest/m1/subscribe-ranges/model.md)), largest and
liveness ([Live largest location](/quest/m1/largest.md)), announce
interest ([Rust lazy discovery](/quest/m1/lazy-discovery/rust.md)), and
the received tail ([Tail arrivals](/quest/m1/tail-arrivals.md)).
interest ([Rust lazy discovery](/quest/m1/lazy-discovery/rust.md)).

Inventory method, to rerun before each child starts (counts drift). Rust:
replace every `pub(crate)` under `rs/moq-net/src/model/` with
Expand Down
9 changes: 6 additions & 3 deletions quest/m1/public-model/ingest.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,13 +108,17 @@ Rust items (`rs/moq-net/src/model/`), as they stood at b4eab0db1:
- **Received end** (`track::Producer::finish_at_pending`, `set_tail_pending`;
both subscribers). Without it, a custom subscriber over unordered streams
ends readers at a hole. **Decision**:
- Public after [Tail arrivals](/quest/m1/tail-arrivals.md), as one call
that returns a handle the session settles when its tail is in.
- Public after Tail arrivals, as one call that returns a handle the
session settles when its tail is in.
- Keep it private.
- ✅ Public after Tail arrivals, with the shape left to that quest. Once
the model records arrivals per track bounded by the subscription's
grace, it may own the grace timer itself, so a plain `finish_at` on a
received track could be enough and no settle handle is needed.
- Tail arrivals landed the record (`TrackState::arrivals`) without a
timer: the model folds a gap only on a write, so the session still
settles the tail with `set_tail_pending(false)`. The shape is this
quest's to settle.
- **Session end** (`track::Producer::close`, `abort_session`; both
subscribers): public. Fold `abort_session` into the public `abort` (abort
open groups too) unless the implementer finds a reader-visible difference,
Expand Down Expand Up @@ -181,7 +185,6 @@ cover the behavior.
## Required

- [Narrow the model before it goes public](/quest/m1/public-model/narrow.md) - its visibility rewrite and marker lint guard every model API this child edits
- [Tail arrivals](/quest/m1/tail-arrivals.md) - settles the received-end semantics before they become public

## Related

Expand Down
10 changes: 3 additions & 7 deletions quest/m1/resume-reorder.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ answers that the feed went past `seq` as soon as any newer group arrived
beat N's. Resume (`rs/moq-net/src/model/resume.rs`, the refusal verdict) then
fails an in-flight group it could have continued.

- Decided: build on [Tail arrivals](/quest/m1/tail-arrivals.md), the per-track
record of live arrivals in the model, and make `poll_group`'s "passed" verdict
read it. A gap stays open for the subscription's grace, as `tail::Tail` does
- Decided: build on the per-track record of live arrivals in the model
(`TrackState::arrivals`, a `model::ledger::Ledger`), and make
`poll_group`'s "passed" verdict read it. A gap stays open for the subscription's grace, as `tail::Tail` does
in the lite subscriber today.
- Decided: #5209's held fetches move off their local `Tail` read onto this
verdict, so one rule remains.
Expand All @@ -28,10 +28,6 @@ fails an in-flight group it could have continued.

Public API: behavior only. Wire: none.

## Required

- [Tail arrivals](/quest/m1/tail-arrivals.md) - the per-track arrival record this verdict reads

## Related

- [SUBSCRIBE_DROP](/quest/m1/subscribe-drop.md) - explicit terminal dispositions end recovery without waiting for a newer group
1 change: 0 additions & 1 deletion quest/m1/subscribe-drop-early.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,3 @@ Tests, with mock time:
## Related

- [SUBSCRIBE_DROP](/quest/m1/subscribe-drop.md) - the parent: lite-07, resets before the header, readers
- [Tail arrivals](/quest/m1/tail-arrivals.md) - per-track live dispositions survive cache eviction
1 change: 0 additions & 1 deletion quest/m1/subscribe-drop.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,5 @@ tail interop could land first.

## Related

- [Tail arrivals](/quest/m1/tail-arrivals.md) - per-track live dispositions survive cache eviction
- [Resume reorder](/quest/m1/resume-reorder.md) - late stream headers are not proof that a group was dropped
- [FETCH max-delay](/quest/m1/fetch-max-delay.md) - age expiry and DROP independently settle pending recovery
26 changes: 0 additions & 26 deletions quest/m1/tail-arrivals.md

This file was deleted.

5 changes: 2 additions & 3 deletions quest/m2/ordered-owed-wait.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ sequence until the budget elapsed. The model cannot tell "not yet" from
✅ Decided 2026-10-14 (maintainer):

- Wait only on sequences the track still owes, read from the dispositions
that [SUBSCRIBE_DROP](/quest/m1/subscribe-drop.md) and
[Tail arrivals](/quest/m1/tail-arrivals.md) record. Not chosen: an opt-in
that [SUBSCRIBE_DROP](/quest/m1/subscribe-drop.md) and the track's
arrival record (`TrackState::arrivals`) keep. Not chosen: an opt-in
waiting reader (new public API no consumer needs, and opted-in readers hit
the same holes); fixing #5475 forward (discontinuity markers still stall,
and unlimited-budget readers still hang on a never-produced group).
Expand Down Expand Up @@ -71,7 +71,6 @@ Public API: behavior only. Wire: none.
## Required

- [SUBSCRIBE_DROP](/quest/m1/subscribe-drop.md) - every stream group in a subscription arrives or is named dropped, including publisher skips
- [Tail arrivals](/quest/m1/tail-arrivals.md) - per-track live dispositions survive cache eviction

## Related

Expand Down
6 changes: 6 additions & 0 deletions rs/moq-net/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,12 @@ name = "track"
harness = false
required-features = ["async"]

# Declares a pending tail through the hidden `fuzz` module.
[[bench]]
name = "tail"
harness = false
required-features = ["fuzz"]

[[bench]]
name = "priority"
harness = false
Expand Down
79 changes: 79 additions & 0 deletions rs/moq-net/benches/tail.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
//! The end check of a received track whose declared end still waits on its tail.
//!
//! `track_pending_tail` holds a reader at a hole below a pending end on every track, each
//! track caching `groups` groups above the hole, and re-polls one of those readers. The
//! check is judged by the track's record of arrivals, so a cost that grows with the groups
//! cached or the tracks held shows up as a slope over either axis.
//!
//! Run with `cargo bench -p moq-net --features fuzz --bench tail`.

use std::{hint::black_box, task::Poll, time::Duration};

use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
use moq_net::{broadcast, group, kio, track};

/// Tracks each holding a reader at the tail.
const TRACKS: [usize; 3] = [1, 32, 1024];

/// Groups each track caches above the hole.
const GROUPS: [usize; 3] = [1, 32, 1024];

/// Readers held at a hole below a pending end, one per track.
struct Held {
_broadcast: broadcast::Producer,
_tracks: Vec<track::Producer>,
readers: Vec<track::Subscriber>,
}

impl Held {
fn new(tracks: usize, groups: usize) -> Self {
let broadcast = broadcast::Info::default().produce();
let waiter = kio::Waiter::noop();
let mut readers = Vec::with_capacity(tracks);
let tracks = (0..tracks)
.map(|i| {
let mut track = broadcast.create_track(format!("bench{i}"), None).unwrap();
// A budget wide enough that no cached group is skipped as stale.
let mut reader =
track.subscribe(track::Subscription::default().with_max_delay(Duration::from_secs(3600)));
track.start_at(0).unwrap();
let end = track::Position::group(groups as u64 + 1);
moq_net::fuzz::finish_at_pending(&mut track, end).unwrap();
// Group 0 never arrives: the hole that holds the reader.
for sequence in 1..=groups as u64 {
track.create_group(group::Info { sequence }).unwrap().finish().unwrap();
}
for _ in 0..groups {
assert!(matches!(reader.poll_recv_group(&waiter), Poll::Ready(Ok(Some(_)))));
}
assert!(reader.poll_recv_group(&waiter).is_pending(), "held at the hole");
readers.push(reader);
track
})
.collect();
Self {
_broadcast: broadcast,
_tracks: tracks,
readers,
}
}
}

fn bench_pending_tail(c: &mut Criterion) {
let mut group = c.benchmark_group("track_pending_tail");
group.throughput(Throughput::Elements(1));
for tracks in TRACKS {
for groups in GROUPS {
let id = format!("{tracks}x{groups}");
group.bench_function(BenchmarkId::new("repoll", &id), |b| {
let mut held = Held::new(tracks, groups);
let waiter = kio::Waiter::noop();
b.iter(|| assert!(black_box(held.readers[0].poll_recv_group(&waiter)).is_pending()));
});
}
}
group.finish();
}

criterion_group!(benches, bench_pending_tail);
criterion_main!(benches);
6 changes: 6 additions & 0 deletions rs/moq-net/src/fuzz.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ pub fn object_group(frames: usize, properties: bool) -> (broadcast::Producer, tr
(broadcast, track, group)
}

/// Declare a received track's end while the wire may still owe groups below it, as a
/// subscriber does on SUBSCRIBE_END, for the pending-tail benchmark.
pub fn finish_at_pending(track: &mut track::Producer, end: track::Position) -> crate::Result<()> {
track.finish_at_pending(end)
}

/// Every target, keyed by the name of its `fuzz_targets/<name>.rs` shim.
pub const TARGETS: &[(&str, Target)] = &[
("lite", lite_wire),
Expand Down
Loading
Loading