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 @@ -77,7 +77,6 @@ above them.
- [Draft-20 FETCH edge cases](/quest/m1/fetch-edge-cases.md) - an exact-final range reports End of Track and a FETCH_OK past the requested group closes the session, on main then release
- [A local close never throws](/quest/m1/subscribe-close-throw.md) - closing a `@moq/net` lite subscription never raises ProtocolViolation for the end of stream it caused, on main
- [Subscribe-close backport](/quest/m1/subscribe-close-backport.md) - the same fix and the `race` loser fix on `release`, so closing a subscription during setup leaves no unhandled rejection in a later 0.17 patch
- [Main live-feed check](/quest/m1/fetch-live-feed.md) - tests pin that a fetch-only or parked warm cache never supplies Largest Object on main, fixing it if it does
- [Claim answer epochs](/quest/m1/claim-answer-epochs.md) - a claim answers each output by publishing it at its exact path under its own epoch, so a restarted output never reaches a returning reader as the old one's cache
- [Untimed decisions](/quest/m1/untimed-decisions.md) - the maintainer decides whether moq-archive keeps refusing untimed tracks, and whether a malformed FETCH object ends its track
- [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
Expand Down
4 changes: 0 additions & 4 deletions quest/m1/fetch-edge-cases.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,3 @@ later 0.17 patch.

Public API: none. Wire: behavior only, within the existing draft-20 through
draft-22 FETCH form.

## Related

- [Main live-feed check](/quest/m1/fetch-live-feed.md) - the other follow-up from the same backport review
29 changes: 0 additions & 29 deletions quest/m1/fetch-live-feed.md

This file was deleted.

30 changes: 30 additions & 0 deletions rs/moq-net/src/ietf/publisher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7320,6 +7320,36 @@ mod serve_tests {
}
}

/// A track whose cache only ever answered a fetch has no live feed, so its newest
/// object is not Largest Object: a draft-20 FETCH past it echoes the requested end.
#[moq_net_sim::test]
async fn a_draft20_fetch_of_a_fetch_only_cache_is_not_capped() {
for version in [Version::Draft20, Version::Draft21, Version::Draft22] {
let h = serve(version);
let request = h._broadcast.reserve_track("cam").unwrap();
let dynamic = request.dynamic();
let fetching = request.consume().fetch_group(4, None);
let mut fetched = dynamic.requested_group().await.unwrap().accept(None).unwrap();
for object in 0..2 {
fetched.write_frame(None, format!("4-{object}").into_bytes()).unwrap();
}
fetched.finish().unwrap();
fetching.await.unwrap();

let fetch_type = fetch_range(
version,
"cam",
Location { group: 4, object: 2 },
Location { group: 4, object: 6 },
);
let buf = run_fetch(&h, fetch_type, GroupOrder::Ascending, true).await;
let (ok, objects) = fetch_answer(buf, version);
assert_eq!(ok.end_location, Location { group: 4, object: 5 }, "{version}");
assert!(!ok.end_of_track, "{version}");
assert_eq!(objects, Vec::new(), "{version}");
}
}

/// The last group of a finished track ends the response at its last object, with End
/// of Track set.
#[moq_net_sim::test]
Expand Down
24 changes: 24 additions & 0 deletions rs/moq-net/src/model/resume.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1149,6 +1149,30 @@ mod test {
);
}

/// Retaining an old copy's cache does not keep a parked logical track live.
#[test]
fn a_parked_warm_cache_is_not_live() {
let routes = Producer::new();
let logical = logical(&routes);
let resumed = logical.consume();
let source = copy();
let mut warm = source.create_group(group::Info { sequence: 2 }).unwrap();
warm.write_frame(crate::Timestamp::from_millis(0).unwrap(), b"2-0".as_ref())
.unwrap();
warm.finish().unwrap();
routes.serve(source.consume());
assert!(resumed.is_live(), "a serving live copy is live");

routes.park();
assert!(source.consume().peek_group(2).is_some(), "the old copy stays warm");
assert!(routes.consume().serving().is_none(), "no copy is spliced after it");
assert!(!resumed.is_live(), "a parked warm cache is not live");

let next = copy();
routes.serve(next.consume());
assert!(resumed.is_live(), "the next serving copy is live");
}

#[test]
fn a_reader_reads_the_serving_copy() {
let routes = Producer::new();
Expand Down
49 changes: 44 additions & 5 deletions rs/moq-net/src/model/track.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3130,12 +3130,14 @@ impl Consumer {
}

/// Whether the cache is fed live, so the newest object it holds is the track's largest.
/// A relay's copy with no upstream subscription is not, even once it learns the end.
/// Fetch-only, closed, and parked copies are not, even once they learn the end.
pub(crate) fn is_live(&self) -> bool {
if let Some(serving) = self.serving() {
return serving.is_live();
}
self.state.read().feed == Feed::Live
let state = self.state.read();
let Some(routes) = state.routes.clone() else {
return state.published && !state.closed && state.feed == Feed::Live;
};
drop(state);
routes.serving().is_some_and(|serving| serving.is_live())
}

/// Fetching a single past group, without holding a live subscription.
Expand Down Expand Up @@ -7408,6 +7410,43 @@ mod test {
assert_eq!(group.sequence, 0);
}

/// A fetched group installs metadata but never accepts the track's live feed.
#[test]
fn a_fetch_only_cache_is_not_live() {
let waiter = kio::Waiter::noop();
let request = Request::new(Arc::new(broadcast::Info::default()), "test");
let dynamic = request.dynamic();
let consumer = request.consume();
let mut pending = consumer.fetch_group(2, None);
let Poll::Ready(Ok(req)) = dynamic.poll_requested_group(&waiter) else {
panic!("fetch request should be ready");
};
let mut fetched = req.accept(None).unwrap();
fetched.write_frame(None, b"2-0".as_ref()).unwrap();
fetched.finish().unwrap();
let Poll::Ready(Ok(mut group)) = kio::Task::poll(&mut *pending, &waiter) else {
panic!("fetched group should be cached");
};
assert_eq!(group.sequence, 2);
let Poll::Ready(Ok(Some(frame))) = group.poll_read_frame(&waiter) else {
panic!("fetched frame should be cached");
};
assert_eq!(frame.payload.as_ref(), b"2-0");
assert!(!consumer.is_live(), "a fetch-only cache is not live");

let mut producer = request.accept(None);
assert!(consumer.is_live(), "accepting the track starts its live feed");
producer.set_idle();
assert!(!consumer.is_live(), "an idle accepted copy is not live");
producer.set_live(None);
assert!(consumer.is_live(), "the answered copy is live again");

// The fetch handler holds the same producer lifetime, so both leave.
drop(dynamic);
drop(producer);
assert!(!consumer.is_live(), "a track whose producer left is not live");
}

/// A one-shot fetch populates the shared cache but never the live arrival cursor,
/// so it cannot make content available to a subscription look stale.
#[moq_net_sim::test]
Expand Down
Loading