Skip to content

Commit a812808

Browse files
kixelatedclaude
andauthored
moq-lite-05: implement FETCH for past groups via TrackConsumer::fetch (#1601)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent c38abd5 commit a812808

10 files changed

Lines changed: 984 additions & 82 deletions

File tree

rs/kio/src/future.rs

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
use std::{
2+
ops::{Deref, DerefMut},
3+
pin::Pin,
4+
task::{Context, Poll},
5+
};
6+
7+
use crate::Waiter;
8+
9+
/// A pollable computation backed by kio channels.
10+
///
11+
/// Implementors write only [`Self::poll`], registering the [`Waiter`] with the
12+
/// channels they read. Wrap the value in [`Pending`] to get a real
13+
/// [`std::future::Future`].
14+
///
15+
/// This exists because a kio [`Waiter`] holds the strong `Arc<Waker>` while the
16+
/// channel's [`crate::WaiterList`] keeps only a `Weak`. A bare
17+
/// [`std::future::Future`] would have to stash the strong `Waiter` in a field and
18+
/// replace it every poll (or lose its wakeup); [`Pending`] does that once so each
19+
/// implementor doesn't have to.
20+
pub trait Future: Unpin {
21+
type Output;
22+
23+
/// Poll for the output, registering `waiter` with the relevant channels if not
24+
/// yet ready.
25+
///
26+
/// Takes `&self`: kio channels poll immutably, so a pollable can be driven
27+
/// through a shared borrow (e.g. while it lives inside an `&self`-borrowed enum).
28+
/// Carry any per-poll mutable state in a kio channel or a [`std::cell`] type.
29+
fn poll(&self, waiter: &Waiter) -> Poll<Self::Output>;
30+
}
31+
32+
/// Adapts a kio [`Future`] into a [`std::future::Future`], retaining the strong
33+
/// [`Waiter`] between polls so its weak registration stays live.
34+
///
35+
/// Derefs to the inner value, so any inherent methods you define on it are
36+
/// reachable through the pending handle (e.g. a non-blocking `poll`, or an
37+
/// `update`).
38+
pub struct Pending<F> {
39+
inner: F,
40+
// Retain the previous waiter so its Weak registration survives until the next
41+
// poll replaces it (see [`crate::WaiterList`]).
42+
waiter: Option<Waiter>,
43+
}
44+
45+
impl<F> Pending<F> {
46+
/// Wrap a [`Future`] so it can be `.await`ed.
47+
pub fn new(inner: F) -> Self {
48+
Self { inner, waiter: None }
49+
}
50+
51+
/// Consume the wrapper, returning the inner value.
52+
pub fn into_inner(self) -> F {
53+
self.inner
54+
}
55+
}
56+
57+
impl<F> Deref for Pending<F> {
58+
type Target = F;
59+
60+
fn deref(&self) -> &F {
61+
&self.inner
62+
}
63+
}
64+
65+
impl<F> DerefMut for Pending<F> {
66+
fn deref_mut(&mut self) -> &mut F {
67+
&mut self.inner
68+
}
69+
}
70+
71+
impl<F: Future> std::future::Future for Pending<F> {
72+
type Output = F::Output;
73+
74+
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<F::Output> {
75+
// Replacing drops the previous waiter, killing its Weak ref in the list so
76+
// the inner poll's register call can recycle the slot (see `WaiterList`).
77+
// `Pending<F>` is `Unpin` (F is, via the trait bound), so this deref is sound.
78+
let this = &mut *self;
79+
this.waiter = Some(Waiter::new(cx.waker().clone()));
80+
Future::poll(&this.inner, this.waiter.as_ref().unwrap())
81+
}
82+
}
83+
84+
#[cfg(test)]
85+
mod test {
86+
use super::*;
87+
use crate::Producer;
88+
89+
/// A pollable that waits for the channel value to reach a threshold, with an
90+
/// inherent method reachable through `Pending`'s `DerefMut`.
91+
struct AtLeast {
92+
consumer: crate::Consumer<u64>,
93+
threshold: u64,
94+
}
95+
96+
impl AtLeast {
97+
fn bump_threshold(&mut self) {
98+
self.threshold += 1;
99+
}
100+
}
101+
102+
impl Future for AtLeast {
103+
type Output = u64;
104+
105+
fn poll(&self, waiter: &Waiter) -> Poll<u64> {
106+
let threshold = self.threshold;
107+
match self.consumer.poll(waiter, |v| {
108+
let current = **v;
109+
if current >= threshold {
110+
Poll::Ready(current)
111+
} else {
112+
Poll::Pending
113+
}
114+
}) {
115+
Poll::Ready(Ok(v)) => Poll::Ready(v),
116+
_ => Poll::Pending,
117+
}
118+
}
119+
}
120+
121+
#[test]
122+
fn pending_derefs_and_drives() {
123+
use std::task::Waker;
124+
125+
let producer = Producer::new(0u64);
126+
let mut pending = Pending::new(AtLeast {
127+
consumer: producer.consume(),
128+
threshold: 5,
129+
});
130+
131+
// Inherent method on the inner reached via DerefMut.
132+
pending.bump_threshold(); // threshold now 6
133+
134+
// The kio-level poll (reached through Deref) is pending until the value catches up.
135+
assert!(Future::poll(&*pending, &Waiter::noop()).is_pending());
136+
137+
if let Ok(mut v) = producer.write() {
138+
*v = 6;
139+
}
140+
141+
// The std Future resolves once the threshold is met.
142+
let mut cx = Context::from_waker(Waker::noop());
143+
let mut pending = std::pin::pin!(pending);
144+
assert_eq!(std::future::Future::poll(pending.as_mut(), &mut cx), Poll::Ready(6));
145+
}
146+
}

rs/kio/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ mod lock;
1414
mod waiter;
1515

1616
mod consumer;
17+
mod future;
1718
mod producer;
1819
mod weak;
1920

2021
pub use consumer::Consumer;
22+
pub use future::{Future, Pending};
2123
pub use producer::{Mut, Producer, Ref};
2224
pub use waiter::{Waiter, WaiterList, wait};
2325
pub use weak::Weak;

rs/kio/src/producer.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,9 @@ impl<T> Producer<T> {
148148
}
149149
}
150150

151-
fn poll_unused(&self, waiter: &Waiter) -> Poll<Option<()>> {
151+
/// Poll-based variant of [`Self::unused`]: `Ready(Some(()))` when no consumers
152+
/// remain, `Ready(None)` if the channel closed first, else `Pending`.
153+
pub fn poll_unused(&self, waiter: &Waiter) -> Poll<Option<()>> {
152154
let mut state = self.state.lock();
153155
if state.closed {
154156
return Poll::Ready(None);

rs/moq-native/tests/broadcast.rs

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,123 @@ async fn broadcast_moq_lite_05_timestamps_webtransport() {
229229
lite05_timestamp_roundtrip("https").await;
230230
}
231231

232+
/// Lite05 FETCH round-trip: retrieve a past group by sequence without holding a
233+
/// subscription, exercising the FETCH / FETCH_OK control flow and per-frame
234+
/// timestamp decoding on the fetch stream.
235+
async fn lite05_fetch_roundtrip(scheme: &str) {
236+
use moq_native::moq_net::{Timescale, Timestamp};
237+
238+
let pub_origin = Origin::random().produce();
239+
let mut broadcast = pub_origin.create_broadcast("test").expect("failed to create broadcast");
240+
let mut track = broadcast
241+
.create_track("video", moq_net::TrackInfo::default().with_timescale(Timescale::MICRO))
242+
.expect("failed to create track");
243+
244+
// A group with a few timestamped frames (middle PTS goes backwards, so the
245+
// fetch stream carries a negative zigzag delta too).
246+
let timestamps_us = [10_000u64, 30_000, 20_000];
247+
let mut group = track.append_group().expect("failed to append group"); // seq 0
248+
for &us in &timestamps_us {
249+
let payload = format!("frame@{us}").into_bytes();
250+
let frame = moq_native::moq_net::Frame {
251+
size: payload.len() as u64,
252+
timestamp: Some(Timestamp::new(us, Timescale::MICRO).unwrap()),
253+
};
254+
let mut writer = group.create_frame(frame).expect("failed to create frame");
255+
writer
256+
.write(bytes::Bytes::from(payload))
257+
.expect("failed to write frame");
258+
writer.finish().expect("failed to finish frame");
259+
}
260+
group.finish().expect("failed to finish group");
261+
262+
let mut server_config = moq_native::ServerConfig::default();
263+
server_config.bind = Some("[::]:0".to_string());
264+
server_config.tls.generate = vec!["localhost".into()];
265+
server_config.version = vec!["moq-lite-05-wip".parse().unwrap()];
266+
let mut server = server_config.init().expect("failed to init server");
267+
let addr = server.local_addr().expect("failed to get local addr");
268+
269+
let sub_origin = Origin::random().produce();
270+
let mut announcements = sub_origin.consume().announced();
271+
272+
let mut client_config = moq_native::ClientConfig::default();
273+
client_config.tls.disable_verify = Some(true);
274+
client_config.version = vec!["moq-lite-05-wip".parse().unwrap()];
275+
let client = client_config.init().expect("failed to init client");
276+
let url: url::Url = format!("{scheme}://localhost:{}", addr.port()).parse().unwrap();
277+
278+
let server_handle = tokio::spawn(async move {
279+
let request = server.accept().await.expect("no incoming connection");
280+
let session = request.with_publisher(pub_origin.clone()).ok().await?;
281+
let _broadcast = broadcast;
282+
let _track = track;
283+
let _ = session.closed().await;
284+
Ok::<_, anyhow::Error>(())
285+
});
286+
287+
let client = client.with_consumer(sub_origin);
288+
let session = tokio::time::timeout(TIMEOUT, client.connect(url))
289+
.await
290+
.expect("client connect timed out")
291+
.expect("client connect failed");
292+
293+
let (path, bc) = tokio::time::timeout(TIMEOUT, announcements.next())
294+
.await
295+
.expect("announce timed out")
296+
.expect("origin closed");
297+
assert_eq!(path.as_str(), "test");
298+
let bc = bc.broadcast().expect("expected announce, got unannounce");
299+
300+
// Fetch group 0 directly, without subscribing. No live producer holds the group
301+
// on the client, so this issues a wire FETCH upstream.
302+
let mut group_sub = tokio::time::timeout(TIMEOUT, async {
303+
bc.track("video").unwrap().fetch(0, None).unwrap().await
304+
})
305+
.await
306+
.expect("fetch timed out")
307+
.expect("fetch failed");
308+
assert_eq!(group_sub.sequence, 0);
309+
310+
for &expected_us in &timestamps_us {
311+
let mut frame_sub = tokio::time::timeout(TIMEOUT, group_sub.next_frame())
312+
.await
313+
.expect("next_frame timed out")
314+
.expect("next_frame failed")
315+
.expect("group closed prematurely");
316+
317+
let ts = frame_sub
318+
.timestamp
319+
.expect("Lite05 fetch must carry per-frame timestamps");
320+
assert_eq!(ts.scale(), Timescale::MICRO);
321+
assert_eq!(ts.value(), expected_us);
322+
323+
let payload = frame_sub.read_all().await.expect("failed to read frame");
324+
assert_eq!(payload, bytes::Bytes::from(format!("frame@{expected_us}")));
325+
}
326+
327+
// The fetched group ends cleanly (stream FIN → no more frames).
328+
let end = tokio::time::timeout(TIMEOUT, group_sub.next_frame())
329+
.await
330+
.expect("next_frame timed out")
331+
.expect("next_frame failed");
332+
assert!(end.is_none(), "group should finish after its frames");
333+
334+
drop(session);
335+
server_handle
336+
.await
337+
.expect("server task panicked")
338+
.expect("server task failed");
339+
}
340+
341+
#[tracing_test::traced_test]
342+
#[tokio::test]
343+
async fn broadcast_moq_lite_05_fetch_webtransport() {
344+
// WebTransport only: Lite05Wip isn't advertised over ALPN, so raw QUIC (moqt://)
345+
// can't negotiate it (same reason the other Lite05 tests are https-only).
346+
lite05_fetch_roundtrip("https").await;
347+
}
348+
232349
/// On Lite05 a publisher that doesn't advertise a timescale still works:
233350
/// SUBSCRIBE_OK carries `timescale = 0` and neither side encodes a
234351
/// per-frame timestamp byte. Subscribers receive `frame.timestamp = None`.

0 commit comments

Comments
 (0)