Skip to main content

moq_net/model/
group.rs

1//! A group is a stream of frames, split into a [Producer] and [Consumer] handle.
2//!
3//! A [Producer] writes an ordered stream of frames.
4//! Frames can be written all at once ([Producer::write_frame]), or in chunks
5//! ([Producer::create_frame]).
6//!
7//! A [Consumer] reads an ordered stream of frames.
8//! The reader can be cloned, in which case each reader receives a copy of each frame. (fanout)
9//!
10//! The stream is closed with [Error] when all writers or readers are dropped.
11use crate::cache;
12use crate::frame::{self, Frame, FrameBuf};
13use crate::{Timescale, stats, track};
14use std::collections::VecDeque;
15use std::mem::MaybeUninit;
16use std::sync::Arc;
17use std::task::{Poll, ready};
18
19use crate::{Error, IntoBytes, Result, Timestamp};
20
21/// Maximum total size of frames cached in a group before old frames are evicted.
22///
23/// Doubles as the per-frame size cap: a single frame can be at most this large (a
24/// larger declared size is refused before allocating), so one maximum-size frame can
25/// fill a group's cache.
26const MAX_GROUP_CACHE: u64 = 32 * 1024 * 1024; // 32 MB
27
28/// A group contains a sequence number because they can arrive out of order.
29///
30/// You can use [track::Producer::append_group] if you just want to +1 the sequence number.
31#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)]
32pub struct Info {
33	/// Per-track sequence number used to detect ordering and gaps. Higher numbers
34	/// supersede lower ones; consumers may skip late arrivals.
35	pub sequence: u64,
36}
37
38impl Info {
39	/// Create an untimed producer for this group.
40	///
41	/// Test-only: real groups are created via [`track::Producer`], which
42	/// supplies the parent track's [`track::Info`]. This helper exists for in-crate
43	/// tests that don't exercise timestamps.
44	#[cfg(test)]
45	pub(crate) fn produce(self) -> Producer {
46		Producer::new(self, track::Info::default())
47	}
48}
49
50impl From<usize> for Info {
51	fn from(sequence: usize) -> Self {
52		Self {
53			sequence: sequence as u64,
54		}
55	}
56}
57
58impl From<u64> for Info {
59	fn from(sequence: u64) -> Self {
60		Self { sequence }
61	}
62}
63
64impl From<u32> for Info {
65	fn from(sequence: u32) -> Self {
66		Self {
67			sequence: sequence as u64,
68		}
69	}
70}
71
72impl From<u16> for Info {
73	fn from(sequence: u16) -> Self {
74		Self {
75			sequence: sequence as u64,
76		}
77	}
78}
79
80/// The in-flight (tail) frame being written. At most one exists at a time, since a
81/// group is a single ordered stream.
82pub(crate) struct Partial {
83	timestamp: Timestamp,
84	buf: FrameBuf,
85}
86
87/// Shared group state. `pub(crate)` so [`frame`] handles can observe the abort flag
88/// while streaming a partial frame.
89#[derive(Default)]
90pub(crate) struct GroupState {
91	// Completed frames, each a contiguous payload. Evicted frames are popped from the
92	// front; `offset` tracks how many.
93	pub(crate) frames: VecDeque<Frame>,
94
95	// The single in-flight frame, if one is open.
96	pub(crate) partial: Option<Partial>,
97
98	// The number of frames evicted from the front of the group.
99	pub(crate) offset: usize,
100
101	// The total size (in bytes) of all cached frames plus any in-flight frame.
102	pub(crate) cache: u64,
103
104	// This group's registration in the track's cache pool; mirrors `cache` so the
105	// pool can evict the least-recently-read groups under memory pressure.
106	charge: cache::Charge,
107
108	// Once finalized, the total number of frames the group will ever contain. Recorded
109	// at finish so the count outlives an abort that clears the cache.
110	pub(crate) fin: Option<usize>,
111
112	// The error that caused the group to be aborted, if any.
113	pub(crate) abort: Option<Error>,
114}
115
116impl GroupState {
117	/// Resolve the source for the frame at `index`: a completed frame (whole) or the
118	/// in-flight tail (streamed). Used by [`Consumer::poll_next_frame`].
119	fn poll_frame_source(&self, index: usize) -> Poll<Result<Option<(frame::Info, frame::Source)>>> {
120		if index < self.offset {
121			return Poll::Ready(Err(Error::Lagged));
122		}
123		let local = index - self.offset;
124		if let Some(f) = self.frames.get(local) {
125			self.charge.touch();
126			let info = frame::Info {
127				size: f.payload.len() as u64,
128				timestamp: f.timestamp,
129			};
130			return Poll::Ready(Ok(Some((info, frame::Source::Complete(f.payload.clone())))));
131		}
132		if local == self.frames.len()
133			&& let Some(p) = &self.partial
134		{
135			self.charge.touch();
136			let info = frame::Info {
137				size: p.buf.capacity() as u64,
138				timestamp: p.timestamp,
139			};
140			return Poll::Ready(Ok(Some((info, frame::Source::Partial(p.buf.clone())))));
141		}
142		ready!(self.poll_terminal(index))?;
143		Poll::Ready(Ok(None))
144	}
145
146	/// Resolve the group's terminal state for a reader positioned at `index`.
147	///
148	/// A finished group is still aborted once its frames are released to free memory
149	/// (aged out of the track's latency window, or evicted by the cache pool). A reader
150	/// that already consumed every frame is missing nothing, so it gets the clean end of
151	/// group; one that fell short sees the abort rather than a silently truncated stream.
152	fn poll_terminal(&self, index: usize) -> Poll<Result<()>> {
153		match (self.fin, &self.abort) {
154			(Some(total), Some(err)) if index < total => Poll::Ready(Err(err.clone())),
155			(Some(_), _) => Poll::Ready(Ok(())),
156			(None, Some(err)) => Poll::Ready(Err(err.clone())),
157			(None, None) => Poll::Pending,
158		}
159	}
160
161	fn poll_finished(&self) -> Poll<Result<u64>> {
162		// The count is recorded at finish, so a later abort that cleared the cache
163		// doesn't turn a complete group into an error.
164		if let Some(total) = self.fin {
165			Poll::Ready(Ok(total as u64))
166		} else if let Some(err) = &self.abort {
167			Poll::Ready(Err(err.clone()))
168		} else {
169			Poll::Pending
170		}
171	}
172
173	/// Evict completed frames from the front until within the byte budget.
174	fn evict(&mut self) {
175		while self.cache > MAX_GROUP_CACHE {
176			let Some(frame) = self.frames.pop_front() else {
177				break;
178			};
179			let size = frame.payload.len() as u64;
180			self.cache -= size;
181			self.charge.sub(size);
182			self.offset += 1;
183		}
184	}
185
186	/// Drop the cached frames (and any in-flight tail) and release their pool charge.
187	fn release(&mut self) {
188		self.frames.clear();
189		self.partial = None;
190		self.cache = 0;
191		self.charge.clear();
192	}
193}
194
195fn modify(state: &kio::Producer<GroupState>) -> Result<kio::Mut<'_, GroupState>> {
196	state.write().map_err(|r| r.abort.clone().unwrap_or(Error::Dropped))
197}
198
199/// The pool's eviction hook: abort the group with [`Error::Evicted`], freeing its
200/// frames immediately. A no-op once the group is already aborted or fully dropped.
201fn evict(weak: &kio::ProducerWeak<GroupState>) {
202	// Upgrade to write; a closed (finished or fully dropped) group has nothing to free.
203	let Some(producer) = weak.produce() else { return };
204	let Ok(mut state) = producer.write() else { return };
205	if state.abort.is_some() {
206		return;
207	}
208	state.abort = Some(Error::Evicted);
209	state.release();
210	state.close();
211}
212
213/// Writes frames to a group in order.
214///
215/// Each group is delivered independently over a QUIC stream.
216/// Use [Self::write_frame] for simple single-buffer frames,
217/// or [Self::create_frame] for multi-chunk streaming writes.
218pub struct Producer {
219	// Mutable stream state.
220	state: kio::Producer<GroupState>,
221
222	// The group header containing the sequence number. A small `Copy` value,
223	// inherited by each frame (see [`Self::create_frame`]).
224	info: Info,
225
226	// The parent track's info, inherited rather than passed piecemeal. Its
227	// `timescale` is used by [`Self::create_frame`] to normalize every frame's
228	// timestamp into the track scale before it enters the stream. Threaded down by
229	// value from [`track::Producer::create_group`] / `append_group`.
230	track: track::Info,
231
232	// Ingress payload meter, set by a tagged [`track::Producer`] via
233	// [`Self::with_meter`]. Empty (no-op) for an untagged group.
234	stats: stats::Meter,
235}
236
237impl std::ops::Deref for Producer {
238	type Target = Info;
239
240	fn deref(&self) -> &Self::Target {
241		&self.info
242	}
243}
244
245impl Producer {
246	/// Create a group producer bound to its parent track's [`track::Info`].
247	///
248	/// Crate-private: groups are only constructed via [`track::Producer`], which
249	/// threads its [`track::Info`] down so properties like the timescale are inherited
250	/// rather than passed in. Every frame added to this group is normalized to the
251	/// track's timescale by [`Self::create_frame`].
252	///
253	/// Registers the group in the shared cache pool reached through the track's
254	/// broadcast (`track.broadcast.origin.pool`), so its cached bytes count against
255	/// the budget and it can be evicted under memory pressure.
256	pub(crate) fn new(info: Info, track: track::Info) -> Self {
257		let state = kio::Producer::<GroupState>::default();
258		let weak = state.weak();
259		let charge = track.broadcast.origin.pool.register(Box::new(move || evict(&weak)));
260		state.write().ok().expect("a new group is open").charge = charge;
261		Self {
262			info,
263			state,
264			track,
265			stats: stats::Meter::default(),
266		}
267	}
268
269	/// Attach an ingress payload meter, counting this as one delivered group.
270	/// Called by a tagged [`track::Producer`] when it creates the group.
271	pub(crate) fn with_meter(mut self, meter: stats::Meter) -> Self {
272		meter.group();
273		self.stats = meter;
274		self
275	}
276
277	/// The group header.
278	pub(crate) fn info(&self) -> Info {
279		self.info
280	}
281
282	/// The parent track's timescale.
283	pub fn timescale(&self) -> Timescale {
284		self.track.timescale
285	}
286
287	/// A helper method to write a frame from a single byte buffer.
288	///
289	/// If you want to write multiple chunks, use [Self::create_frame] to get a frame producer.
290	/// But an upfront size is required.
291	///
292	/// `timestamp` is converted into the parent track's timescale. For data without
293	/// a presentation time, pass [`Timestamp::now`] explicitly.
294	pub fn write_frame<B: IntoBytes>(&mut self, timestamp: Timestamp, data: B) -> Result<()> {
295		let timestamp = timestamp
296			.convert(self.track.timescale)
297			.map_err(|_| Error::TimestampMismatch)?;
298		let payload = data.into_bytes();
299		if payload.len() as u64 > MAX_GROUP_CACHE {
300			return Err(Error::FrameTooLarge);
301		}
302
303		let mut state = modify(&self.state)?;
304		if state.fin.is_some() {
305			return Err(Error::Closed);
306		}
307		debug_assert!(state.partial.is_none(), "a frame is already open");
308		let size = payload.len() as u64;
309		state.cache += size;
310		state.charge.add(size);
311		state.frames.push_back(Frame { timestamp, payload });
312		state.evict();
313
314		// The pool evicts other groups' state, so trigger it only after releasing our
315		// lock. Reached via the parent chain; a no-op when the pool is unbounded.
316		drop(state);
317		self.track.broadcast.origin.pool.evict();
318
319		// Ingress payload: one whole frame written.
320		self.stats.frames(1);
321		self.stats.bytes(size);
322		Ok(())
323	}
324
325	/// Create a frame with an upfront size and presentation timestamp, streamed in
326	/// chunks. Borrows the group exclusively until the returned [`frame::Producer`]
327	/// is finished or dropped, so only one frame is open at a time.
328	///
329	/// The `timestamp` is converted into the parent track's timescale, so the scale you
330	/// build it with doesn't have to match the track. Returns [`Error::FrameTooLarge`]
331	/// if the declared size exceeds the group's byte budget (refused before allocating)
332	/// or [`Error::TimestampMismatch`] if the timestamp can't be converted (overflow).
333	pub fn create_frame(&mut self, frame: frame::Info) -> Result<frame::Producer<'_>> {
334		let timestamp = frame
335			.timestamp
336			.convert(self.track.timescale)
337			.map_err(|_| Error::TimestampMismatch)?;
338		if frame.size > MAX_GROUP_CACHE {
339			return Err(Error::FrameTooLarge);
340		}
341		let buf = FrameBuf::new(frame.size as usize);
342
343		let mut state = modify(&self.state)?;
344		if state.fin.is_some() {
345			return Err(Error::Closed);
346		}
347		debug_assert!(state.partial.is_none(), "a frame is already open");
348		state.cache += frame.size;
349		state.charge.add(frame.size);
350		state.partial = Some(Partial {
351			timestamp,
352			buf: buf.clone(),
353		});
354		state.evict();
355
356		// The pool evicts other groups' state, so trigger it only after releasing our
357		// lock. Reached via the parent chain; a no-op when the pool is unbounded.
358		drop(state);
359		self.track.broadcast.origin.pool.evict();
360
361		// Ingress payload: one frame opened; its bytes are counted per chunk as the
362		// frame::Producer writes them.
363		self.stats.frames(1);
364		let meter = self.stats.clone();
365
366		let info = frame::Info {
367			size: frame.size,
368			timestamp,
369		};
370		Ok(frame::Producer::new(self, buf, info).with_meter(meter))
371	}
372
373	/// Wake consumers parked on the group channel (called after a partial write).
374	pub(crate) fn frame_notify(&self) {
375		// Taking the write lock and dropping it triggers kio's notify.
376		let _ = self.state.write();
377	}
378
379	/// Commit the in-flight frame as a completed frame (called by [`frame::Producer::finish`]).
380	pub(crate) fn frame_commit(&mut self, frame: Frame) -> Result<()> {
381		let mut state = modify(&self.state)?;
382		// Bytes were already counted against the cache (and the pool charge) when the
383		// frame was created; committing just moves the tail into the completed set.
384		state.partial = None;
385		state.frames.push_back(frame);
386		Ok(())
387	}
388
389	/// Fail the group because an in-flight frame couldn't complete (called by
390	/// [`frame::Producer::abort`] / its drop).
391	pub(crate) fn frame_abort(&mut self, err: Error) {
392		let _ = self.clone().abort(err);
393	}
394
395	/// Return the number of frames written so far (completed plus any in-flight).
396	pub fn frame_count(&self) -> usize {
397		let state = self.state.read();
398		state.offset + state.frames.len() + state.partial.is_some() as usize
399	}
400
401	/// Mark the group as complete; no more frames will be written.
402	///
403	/// Borrows rather than consumes, so a later failure can still be reported through
404	/// [`abort`](Self::abort). The handle also keeps the cached frames readable.
405	pub fn finish(&mut self) -> Result<()> {
406		let mut state = modify(&self.state)?;
407		state.fin = Some(state.offset + state.frames.len());
408		Ok(())
409	}
410
411	/// Abort the group with the given error.
412	///
413	/// Consumes the handle. Drops the cached frames so a stale [`Consumer`] can't pin
414	/// their buffers in memory forever; consumers that haven't drained yet surface the
415	/// abort error instead of the leftover cache.
416	pub fn abort(self, err: Error) -> Result<()> {
417		let mut guard = modify(&self.state)?;
418		guard.abort = Some(err);
419		guard.release();
420		guard.close();
421		Ok(())
422	}
423
424	/// Whether the group has been aborted (including pool eviction). The track's
425	/// read paths treat an aborted cached group as absent.
426	pub(crate) fn is_aborted(&self) -> bool {
427		self.state.read().abort.is_some()
428	}
429
430	/// This group's cache pool registration, used by the track to pin the latest
431	/// group. `None` when the pool is detached (the unbounded default).
432	pub(crate) fn cache_entry(&self) -> Option<Arc<cache::Entry>> {
433		self.state.read().charge.entry()
434	}
435
436	/// Create a new consumer for the group.
437	pub fn consume(&self) -> Consumer {
438		Consumer {
439			info: self.info,
440			state: self.state.consume(),
441			track: self.track.clone(),
442			index: 0,
443			prefetch: Prefetch::default(),
444			// Untagged: a tagged track attaches the egress meter via `with_meter`
445			// when it hands the consumer to a subscriber/fetch.
446			stats: stats::Meter::default(),
447		}
448	}
449
450	/// Block until the group is closed or aborted.
451	pub async fn closed(&self) -> Error {
452		kio::wait(|waiter| self.poll_closed(waiter)).await
453	}
454
455	/// Poll until the group is closed or aborted; ready with the cause.
456	pub fn poll_closed(&self, waiter: &kio::Waiter) -> Poll<Error> {
457		self.state.poll_closed(waiter).map(|()| self.abort_reason())
458	}
459
460	/// Block until there are no active consumers.
461	pub async fn unused(&self) -> Result<()> {
462		self.state.unused().await.map_err(|_| self.abort_reason())
463	}
464
465	/// The recorded abort reason, or [`Error::Dropped`] if the group closed without one.
466	fn abort_reason(&self) -> Error {
467		self.state.read().abort.clone().unwrap_or(Error::Dropped)
468	}
469}
470
471impl Clone for Producer {
472	fn clone(&self) -> Self {
473		Self {
474			info: self.info,
475			state: self.state.clone(),
476			track: self.track.clone(),
477			stats: self.stats.clone(),
478		}
479	}
480}
481
482impl Drop for Producer {
483	fn drop(&mut self) {
484		// See track::Producer::drop: the last producer dropping without a clean finish
485		// releases the cached frames so a stale consumer can't pin their buffers forever.
486		// A finished group keeps its cache so consumers can drain.
487		if !self.state.is_last() {
488			return;
489		}
490		if let Ok(mut state) = modify(&self.state)
491			&& state.fin.is_none()
492		{
493			// Dropped without finish() or abort(), so consumers will see
494			// Error::Dropped mid-group. Deliberate ends go through finish()/abort().
495			tracing::warn!(
496				sequence = self.info.sequence,
497				"group::Producer dropped without finish() or abort()"
498			);
499			state.release();
500		}
501	}
502}
503
504/// A small inline batch of completed frames, drained from the shared group state
505/// under one lock and then handed out without re-locking.
506///
507/// Each [`Consumer::read_frame`] otherwise takes the group mutex and allocates a
508/// waker just to clone one `Bytes`; draining a batch amortizes both across `CAP`
509/// frames. Storage is inline and uninitialized (no heap), so a consumer that never
510/// reads whole frames, or drains through a higher-level buffer, pays nothing.
511struct Prefetch {
512	// Initialized, not-yet-taken frames are `frames[pos..len]`; the rest are uninitialized.
513	frames: [MaybeUninit<Frame>; Self::CAP],
514	pos: usize,
515	len: usize,
516}
517
518impl Prefetch {
519	const CAP: usize = 8;
520
521	/// Take the next buffered frame, or `None` if the batch is drained.
522	fn pop(&mut self) -> Option<Frame> {
523		if self.pos == self.len {
524			return None;
525		}
526		// SAFETY: `pos < len`, so this slot was written by `fill` and not yet taken.
527		let frame = unsafe { self.frames[self.pos].assume_init_read() };
528		self.pos += 1;
529		Some(frame)
530	}
531
532	/// Refill with up to `CAP` frames. Must be drained first (`pop` returned `None`).
533	fn fill(&mut self, frames: impl Iterator<Item = Frame>) {
534		debug_assert_eq!(self.pos, self.len, "fill on a non-empty batch would leak frames");
535		self.pos = 0;
536		self.len = 0;
537		for frame in frames.take(Self::CAP) {
538			self.frames[self.len].write(frame);
539			self.len += 1;
540		}
541	}
542
543	/// `(frame count, total payload bytes)` of the buffered, not-yet-taken frames.
544	/// Read once per fill to bump the egress payload counters for the whole batch.
545	fn buffered(&self) -> (u64, u64) {
546		let mut bytes = 0u64;
547		for slot in &self.frames[self.pos..self.len] {
548			// SAFETY: slots in `pos..len` are initialized (written by `fill`, not yet popped).
549			bytes += unsafe { slot.assume_init_ref() }.payload.len() as u64;
550		}
551		((self.len - self.pos) as u64, bytes)
552	}
553}
554
555impl Default for Prefetch {
556	fn default() -> Self {
557		Self {
558			frames: [const { MaybeUninit::uninit() }; Self::CAP],
559			pos: 0,
560			len: 0,
561		}
562	}
563}
564
565impl Drop for Prefetch {
566	fn drop(&mut self) {
567		for slot in &mut self.frames[self.pos..self.len] {
568			// SAFETY: slots in `pos..len` are initialized and were never taken.
569			unsafe { slot.assume_init_drop() };
570		}
571	}
572}
573
574/// Consume a group, frame-by-frame.
575pub struct Consumer {
576	// Shared state with the producer.
577	state: kio::Consumer<GroupState>,
578
579	// Immutable stream state.
580	info: Info,
581
582	// The parent track's info, inherited from the producer. Its `timescale` lets the
583	// wire publisher emit per-frame timestamps at the right scale for a fetched group.
584	track: track::Info,
585
586	// The number of frames we've read.
587	// NOTE: Cloned readers inherit this offset, but then run in parallel.
588	index: usize,
589
590	// A batch of completed frames drained ahead under one lock (whole-frame reads only).
591	prefetch: Prefetch,
592
593	// Egress payload meter, set by a tagged track via [`Self::with_meter`]. Empty
594	// (no-op) for an untagged group.
595	stats: stats::Meter,
596}
597
598impl Clone for Consumer {
599	fn clone(&self) -> Self {
600		// A clone shares the channel and inherits `index`, but starts with an empty
601		// prefetch: it re-reads its batch from the shared state, in parallel.
602		Self {
603			state: self.state.clone(),
604			info: self.info,
605			track: self.track.clone(),
606			index: self.index,
607			prefetch: Prefetch::default(),
608			// Inherit the meter without re-counting the group: the original already
609			// counted it when the track handed it out.
610			stats: self.stats.clone(),
611		}
612	}
613}
614
615impl std::ops::Deref for Consumer {
616	type Target = Info;
617
618	fn deref(&self) -> &Self::Target {
619		&self.info
620	}
621}
622
623impl Consumer {
624	/// Attach an egress payload meter, counting this as one delivered group.
625	/// Called by a tagged track when it hands the consumer to a subscriber or fetch.
626	pub(crate) fn with_meter(mut self, meter: stats::Meter) -> Self {
627		meter.group();
628		self.stats = meter;
629		self
630	}
631
632	/// The parent track's timescale.
633	pub fn timescale(&self) -> Timescale {
634		self.track.timescale
635	}
636
637	// A helper to automatically apply Dropped if the state is closed without an error.
638	fn poll<F, R>(&self, waiter: &kio::Waiter, f: F) -> Poll<Result<R>>
639	where
640		F: Fn(&kio::Ref<'_, GroupState>) -> Poll<Result<R>>,
641	{
642		Poll::Ready(match ready!(self.state.poll(waiter, f)) {
643			Ok(res) => res,
644			// We try to clone abort just in case the function forgot to check for terminal state.
645			Err(state) => Err(state.abort.clone().unwrap_or(Error::Dropped)),
646		})
647	}
648
649	/// Return a consumer for the next frame for chunked reading.
650	pub async fn next_frame(&mut self) -> Result<Option<frame::Consumer>> {
651		kio::wait(|waiter| self.poll_next_frame(waiter)).await
652	}
653
654	/// Poll for the next frame, without blocking.
655	///
656	/// Returns None if the group is finished and the index is out of range.
657	pub fn poll_next_frame(&mut self, waiter: &kio::Waiter) -> Poll<Result<Option<frame::Consumer>>> {
658		// Hand out any frames a prior read_frame prefetched before touching the tail.
659		// Their bytes were already counted at the batch fill, so the frame::Consumer
660		// carries no meter.
661		if let Some(frame) = self.prefetch.pop() {
662			self.index += 1;
663			let info = frame::Info {
664				size: frame.payload.len() as u64,
665				timestamp: frame.timestamp,
666			};
667			let source = frame::Source::Complete(frame.payload);
668			return Poll::Ready(Ok(Some(frame::Consumer::new(self.state.clone(), info, source))));
669		}
670
671		let index = self.index;
672		let Some((info, source)) = ready!(self.poll(waiter, |state| state.poll_frame_source(index))?) else {
673			return Poll::Ready(Ok(None));
674		};
675
676		self.index += 1;
677		// A direct read (not prefetched): count the frame here; the frame::Consumer
678		// counts its bytes per chunk as they're read out.
679		self.stats.frames(1);
680		Poll::Ready(Ok(Some(
681			frame::Consumer::new(self.state.clone(), info, source).with_meter(self.stats.clone()),
682		)))
683	}
684
685	/// Read the next frame (timestamp and payload) all at once, without blocking.
686	pub fn poll_read_frame(&mut self, waiter: &kio::Waiter) -> Poll<Result<Option<frame::Frame>>> {
687		// Fast path: serve from the prefetched batch without locking or allocating a waker.
688		if let Some(frame) = self.prefetch.pop() {
689			self.index += 1;
690			return Poll::Ready(Ok(Some(frame)));
691		}
692
693		// The batch is drained: refill it under a single lock, registering the waiter if
694		// nothing is ready. Borrow the two fields disjointly so the closure can fill.
695		let index = self.index;
696		let prefetch = &mut self.prefetch;
697		let res = self.state.poll(waiter, |state| {
698			if index < state.offset {
699				return Poll::Ready(Err(Error::Lagged));
700			}
701			// `local` can run past the buffered count when frames were cleared or evicted out
702			// from under us (abort, unfinished drop, an eviction gap); clamp so `range` never
703			// panics on an out-of-bounds start. `fill` always resets the batch, so an empty
704			// range leaves `len == 0` and the terminal checks below resolve abort/fin/pending.
705			let local = (index - state.offset).min(state.frames.len());
706			prefetch.fill(state.frames.range(local..).cloned());
707			if prefetch.len > 0 {
708				// Mark the group recently read so the cache pool keeps it over staler
709				// groups. Touching once per batch fill is enough; the drained pops that
710				// follow serve from the prefetch without re-locking.
711				state.charge.touch();
712				return Poll::Ready(Ok(()));
713			}
714			// Nothing completed at `index`: an in-flight tail waits, otherwise resolve
715			// the terminal state (whole-frame reads never stream the partial).
716			state.poll_terminal(index)
717		});
718
719		match ready!(res) {
720			Ok(Ok(())) => {}
721			Ok(Err(err)) => return Poll::Ready(Err(err)),
722			Err(state) => return Poll::Ready(Err(state.abort.clone().unwrap_or(Error::Dropped))),
723		}
724
725		// A fresh batch was just filled (empty only on a clean end). Count the whole
726		// batch once here, under no lock, so the drained pops that follow stay free.
727		let (frames, bytes) = self.prefetch.buffered();
728		self.stats.frames(frames);
729		self.stats.bytes(bytes);
730
731		Poll::Ready(Ok(self.prefetch.pop().inspect(|_| {
732			self.index += 1;
733		})))
734	}
735
736	/// Read the next frame (timestamp and payload) all at once.
737	pub async fn read_frame(&mut self) -> Result<Option<frame::Frame>> {
738		// Serve from the prefetched batch without building a future or allocating a waker.
739		if let Some(frame) = self.prefetch.pop() {
740			self.index += 1;
741			return Ok(Some(frame));
742		}
743		kio::wait(|waiter| self.poll_read_frame(waiter)).await
744	}
745
746	/// Poll for the final number of frames in the group.
747	pub fn poll_finished(&mut self, waiter: &kio::Waiter) -> Poll<Result<u64>> {
748		self.poll(waiter, |state| state.poll_finished())
749	}
750
751	/// Block until the group is finished, returning the number of frames in the group.
752	pub async fn finished(&mut self) -> Result<u64> {
753		kio::wait(|waiter| self.poll_finished(waiter)).await
754	}
755}
756
757/// Options for a one-shot [`track::Consumer::fetch_group`] of a past group.
758#[derive(Clone, Debug, Default)]
759#[non_exhaustive]
760pub struct Fetch {
761	/// Delivery priority for the fetched group's stream. Defaults to 0.
762	pub priority: u8,
763}
764
765impl Fetch {
766	/// Set the delivery priority, returning `self` for chaining.
767	pub fn with_priority(mut self, priority: u8) -> Self {
768		self.priority = priority;
769		self
770	}
771}
772
773#[cfg(test)]
774mod test {
775	use super::*;
776	use bytes::Bytes;
777	use futures::FutureExt;
778
779	#[test]
780	fn basic_frame_reading() {
781		let mut producer = Info { sequence: 0 }.produce();
782		producer
783			.write_frame(Timestamp::ZERO, Bytes::from_static(b"frame0"))
784			.unwrap();
785		producer
786			.write_frame(Timestamp::ZERO, Bytes::from_static(b"frame1"))
787			.unwrap();
788		producer.finish().unwrap();
789
790		let mut consumer = producer.consume();
791		let f0 = consumer.next_frame().now_or_never().unwrap().unwrap().unwrap();
792		assert_eq!(f0.size, 6);
793		let f1 = consumer.next_frame().now_or_never().unwrap().unwrap().unwrap();
794		assert_eq!(f1.size, 6);
795		let end = consumer.next_frame().now_or_never().unwrap().unwrap();
796		assert!(end.is_none());
797	}
798
799	#[test]
800	fn read_frame_all_at_once() {
801		let mut producer = Info { sequence: 0 }.produce();
802		producer
803			.write_frame(Timestamp::ZERO, Bytes::from_static(b"hello"))
804			.unwrap();
805		producer.finish().unwrap();
806
807		let mut consumer = producer.consume();
808		let frame = consumer.read_frame().now_or_never().unwrap().unwrap().unwrap();
809		assert_eq!(frame.payload, Bytes::from_static(b"hello"));
810	}
811
812	#[test]
813	fn read_frame_preserves_timestamp() {
814		let mut producer = Info { sequence: 0 }.produce();
815		let timestamp = Timestamp::from_micros(20_000).unwrap();
816		producer.write_frame(timestamp, Bytes::from_static(b"hello")).unwrap();
817		producer.finish().unwrap();
818
819		let mut consumer = producer.consume();
820		let frame = consumer.read_frame().now_or_never().unwrap().unwrap().unwrap();
821		assert_eq!(frame.timestamp.as_micros(), 20_000);
822		assert_eq!(frame.payload, Bytes::from_static(b"hello"));
823	}
824
825	#[test]
826	fn chunked_frame_reads_whole() {
827		let mut producer = Info { sequence: 0 }.produce();
828		{
829			let mut frame = producer
830				.create_frame(frame::Info {
831					size: 10,
832					timestamp: Timestamp::ZERO,
833				})
834				.unwrap();
835			frame.write(Bytes::from_static(b"hello")).unwrap();
836			frame.write(Bytes::from_static(b"world")).unwrap();
837			frame.finish().unwrap();
838		}
839		producer.finish().unwrap();
840
841		// Frame data is held in a single per-frame buffer; a whole-frame read returns
842		// the full contents in one slice.
843		let mut consumer = producer.consume();
844		let frame = consumer.read_frame().now_or_never().unwrap().unwrap().unwrap();
845		assert_eq!(frame.payload, Bytes::from_static(b"helloworld"));
846	}
847
848	#[test]
849	fn chunked_frame_streams_partial() {
850		let mut producer = Info { sequence: 0 }.produce();
851		let mut consumer = producer.consume();
852
853		let mut frame = producer
854			.create_frame(frame::Info {
855				size: 6,
856				timestamp: Timestamp::ZERO,
857			})
858			.unwrap();
859		frame.write(Bytes::from_static(b"foo")).unwrap();
860
861		// A consumer can stream the in-flight tail before it's finished.
862		let mut f = consumer.next_frame().now_or_never().unwrap().unwrap().unwrap();
863		let c1 = f.read_chunk().now_or_never().unwrap().unwrap();
864		assert_eq!(c1, Some(Bytes::from_static(b"foo")));
865		assert!(f.read_chunk().now_or_never().is_none());
866
867		frame.write(Bytes::from_static(b"bar")).unwrap();
868		frame.finish().unwrap();
869
870		let c2 = f.read_chunk().now_or_never().unwrap().unwrap();
871		assert_eq!(c2, Some(Bytes::from_static(b"bar")));
872		let c3 = f.read_chunk().now_or_never().unwrap().unwrap();
873		assert_eq!(c3, None);
874	}
875
876	#[test]
877	fn group_finish_returns_none() {
878		let mut producer = Info { sequence: 0 }.produce();
879		producer.finish().unwrap();
880
881		let mut consumer = producer.consume();
882		let end = consumer.next_frame().now_or_never().unwrap().unwrap();
883		assert!(end.is_none());
884	}
885
886	#[test]
887	fn abort_propagates() {
888		let producer = Info { sequence: 0 }.produce();
889		let mut consumer = producer.consume();
890		producer.abort(crate::Error::Cancel).unwrap();
891
892		let result = consumer.next_frame().now_or_never().unwrap();
893		assert!(matches!(result, Err(crate::Error::Cancel)));
894	}
895
896	#[test]
897	fn abort_clears_cached_frames() {
898		let mut producer = Info { sequence: 0 }.produce();
899		producer
900			.write_frame(Timestamp::ZERO, Bytes::from_static(b"data"))
901			.unwrap();
902
903		// A stale consumer that never reads must not pin the cached frames.
904		let _consumer = producer.consume();
905		assert_eq!(producer.state.read().frames.len(), 1);
906
907		producer.clone().abort(crate::Error::Cancel).unwrap();
908
909		let state = producer.state.read();
910		assert!(state.frames.is_empty(), "cached frames should be dropped on abort");
911		assert_eq!(state.cache, 0);
912	}
913
914	#[test]
915	fn drop_unfinished_clears_cached_frames() {
916		let producer = Info { sequence: 0 }.produce();
917		let mut writer = producer.clone();
918		writer
919			.write_frame(Timestamp::ZERO, Bytes::from_static(b"data"))
920			.unwrap();
921
922		// A stale consumer keeps the channel (and thus the cache) alive.
923		let mut consumer = producer.consume();
924		assert_eq!(producer.state.read().frames.len(), 1);
925
926		// Drop every producer without finishing: the cache is released.
927		drop(writer);
928		drop(producer);
929
930		let result = consumer.next_frame().now_or_never().unwrap();
931		assert!(matches!(result, Err(crate::Error::Dropped)));
932	}
933
934	#[test]
935	fn drop_finished_keeps_cached_frames() {
936		let mut producer = Info { sequence: 0 }.produce();
937		producer
938			.write_frame(Timestamp::ZERO, Bytes::from_static(b"data"))
939			.unwrap();
940		producer.finish().unwrap();
941
942		let mut consumer = producer.consume();
943		drop(producer);
944
945		// A cleanly finished group keeps its cache so the consumer can still drain.
946		let frame = consumer.read_frame().now_or_never().unwrap().unwrap().unwrap();
947		assert_eq!(frame.payload, Bytes::from_static(b"data"));
948	}
949
950	#[tokio::test]
951	async fn pending_then_ready() {
952		let mut producer = Info { sequence: 0 }.produce();
953		let mut consumer = producer.consume();
954
955		// Consumer blocks because no frames yet.
956		assert!(consumer.next_frame().now_or_never().is_none());
957
958		producer
959			.write_frame(Timestamp::ZERO, Bytes::from_static(b"data"))
960			.unwrap();
961		producer.finish().unwrap();
962
963		let frame = consumer.next_frame().now_or_never().unwrap().unwrap().unwrap();
964		assert_eq!(frame.size, 4);
965	}
966
967	#[test]
968	fn eviction_drops_old_frames() {
969		let mut producer = Info { sequence: 0 }.produce();
970
971		// Write frames that total more than MAX_GROUP_CACHE.
972		let big = Bytes::from(vec![0u8; MAX_GROUP_CACHE as usize]);
973		producer.write_frame(Timestamp::ZERO, big.clone()).unwrap();
974		producer.write_frame(Timestamp::ZERO, big).unwrap();
975
976		// The first frame should have been evicted (tombstoned via offset).
977		let state = producer.state.read();
978		assert_eq!(state.offset, 1);
979		assert_eq!(state.frames.len(), 1);
980		assert_eq!(state.frames[0].payload.len(), MAX_GROUP_CACHE as usize);
981	}
982
983	#[test]
984	fn next_frame_returns_cache_full_on_tombstone() {
985		let mut producer = Info { sequence: 0 }.produce();
986
987		let big = Bytes::from(vec![0u8; MAX_GROUP_CACHE as usize]);
988		producer.write_frame(Timestamp::ZERO, big.clone()).unwrap();
989		producer.write_frame(Timestamp::ZERO, big).unwrap();
990
991		let mut consumer = producer.consume();
992		// First frame was evicted, next_frame should return Lagged.
993		let result = consumer.next_frame().now_or_never().unwrap();
994		assert!(matches!(result, Err(crate::Error::Lagged)));
995	}
996
997	#[test]
998	fn no_eviction_under_budget() {
999		let mut producer = Info { sequence: 0 }.produce();
1000		// Many small frames stay cached: there is no frame-count cap, only a byte budget.
1001		for _ in 0..100_000 {
1002			producer.write_frame(Timestamp::ZERO, Bytes::from_static(b"x")).unwrap();
1003		}
1004		producer.finish().unwrap();
1005
1006		let state = producer.state.read();
1007		assert_eq!(state.offset, 0);
1008		assert_eq!(state.frames.len(), 100_000);
1009	}
1010
1011	#[test]
1012	fn clone_consumer_independent() {
1013		let mut producer = Info { sequence: 0 }.produce();
1014		producer.write_frame(Timestamp::ZERO, Bytes::from_static(b"a")).unwrap();
1015
1016		let mut c1 = producer.consume();
1017		// Read one frame from c1
1018		let _ = c1.next_frame().now_or_never().unwrap().unwrap().unwrap();
1019
1020		// Clone c1, inheriting its index (past first frame).
1021		let mut c2 = c1.clone();
1022
1023		producer.write_frame(Timestamp::ZERO, Bytes::from_static(b"b")).unwrap();
1024		producer.finish().unwrap();
1025
1026		// c2 should get the second frame (inherited index)
1027		let f = c2.next_frame().now_or_never().unwrap().unwrap().unwrap();
1028		assert_eq!(f.size, 1); // "b"
1029
1030		let end = c2.next_frame().now_or_never().unwrap().unwrap();
1031		assert!(end.is_none());
1032	}
1033
1034	/// Reading more than one prefetch batch drains every frame in order across the
1035	/// batch boundary (the refill starts exactly where the previous batch ended).
1036	#[test]
1037	fn read_frame_crosses_prefetch_batches() {
1038		let n = Prefetch::CAP * 3 + 5;
1039		let mut producer = Info { sequence: 0 }.produce();
1040		for i in 0..n {
1041			producer
1042				.write_frame(Timestamp::ZERO, Bytes::from(vec![i as u8; 4]))
1043				.unwrap();
1044		}
1045		producer.finish().unwrap();
1046
1047		let mut consumer = producer.consume();
1048		for i in 0..n {
1049			let frame = consumer.read_frame().now_or_never().unwrap().unwrap().unwrap();
1050			assert_eq!(frame.payload, Bytes::from(vec![i as u8; 4]));
1051		}
1052		assert!(consumer.read_frame().now_or_never().unwrap().unwrap().is_none());
1053	}
1054
1055	/// A finished group is still aborted once its frames are released to free memory (the
1056	/// track's latency window, or the cache pool). A reader that already drained every frame
1057	/// is missing nothing, so it must see the clean end of group rather than the abort.
1058	#[test]
1059	fn abort_after_finish_keeps_the_clean_end_for_a_drained_reader() {
1060		let mut producer = Info { sequence: 0 }.produce();
1061		producer
1062			.write_frame(Timestamp::ZERO, Bytes::from_static(b"hello"))
1063			.unwrap();
1064		producer.finish().unwrap();
1065
1066		let mut drained = producer.consume();
1067		let mut behind = producer.consume();
1068		let frame = drained.read_frame().now_or_never().unwrap().unwrap().unwrap();
1069		assert_eq!(frame.payload, Bytes::from_static(b"hello"));
1070
1071		producer.abort(Error::Old).unwrap();
1072
1073		// Drained everything before the abort: nothing is missing.
1074		assert!(drained.read_frame().now_or_never().unwrap().unwrap().is_none());
1075		assert!(drained.next_frame().now_or_never().unwrap().unwrap().is_none());
1076
1077		// Never read the frame, and its bytes are gone: a truncated stream, not a clean end.
1078		assert!(matches!(behind.read_frame().now_or_never().unwrap(), Err(Error::Old)));
1079	}
1080
1081	/// The frame count is fixed at finish, so an abort that clears the cache can't turn a
1082	/// complete group into an error.
1083	#[test]
1084	fn finished_survives_a_later_abort() {
1085		let mut producer = Info { sequence: 0 }.produce();
1086		producer.write_frame(Timestamp::ZERO, Bytes::from_static(b"a")).unwrap();
1087		producer.write_frame(Timestamp::ZERO, Bytes::from_static(b"b")).unwrap();
1088		producer.finish().unwrap();
1089
1090		let mut consumer = producer.consume();
1091		producer.abort(Error::Old).unwrap();
1092
1093		assert_eq!(consumer.finished().now_or_never().unwrap().unwrap(), 2);
1094	}
1095
1096	/// `next_frame` drains frames a prior `read_frame` prefetched, preserving order.
1097	#[test]
1098	fn interleave_read_and_next_frame() {
1099		let mut producer = Info { sequence: 0 }.produce();
1100		for i in 0..5u8 {
1101			producer.write_frame(Timestamp::ZERO, Bytes::from(vec![i; 1])).unwrap();
1102		}
1103		producer.finish().unwrap();
1104
1105		let mut consumer = producer.consume();
1106		// The first whole-frame read prefetches all five frames into the batch.
1107		let f0 = consumer.read_frame().now_or_never().unwrap().unwrap().unwrap();
1108		assert_eq!(f0.payload, Bytes::from(vec![0u8; 1]));
1109
1110		// next_frame must continue from the batch, not skip ahead or repeat.
1111		for i in 1..5u8 {
1112			let mut f = consumer.next_frame().now_or_never().unwrap().unwrap().unwrap();
1113			let data = f.read_all().now_or_never().unwrap().unwrap();
1114			assert_eq!(data, Bytes::from(vec![i; 1]));
1115		}
1116		assert!(consumer.next_frame().now_or_never().unwrap().unwrap().is_none());
1117	}
1118
1119	/// A `read_frame` whose index sits past the buffered frames (cleared by an abort, or an
1120	/// eviction gap) must surface the error, not panic on an out-of-range `range(local..)`.
1121	#[test]
1122	fn read_frame_past_cleared_frames_does_not_panic() {
1123		let mut producer = Info { sequence: 0 }.produce();
1124		producer.write_frame(Timestamp::ZERO, Bytes::from_static(b"a")).unwrap();
1125		producer.write_frame(Timestamp::ZERO, Bytes::from_static(b"b")).unwrap();
1126
1127		let mut consumer = producer.consume();
1128		consumer.read_frame().now_or_never().unwrap().unwrap().unwrap();
1129		consumer.read_frame().now_or_never().unwrap().unwrap().unwrap();
1130
1131		// Abort clears the cached frames but leaves the consumer's index (2) past them, so the
1132		// refill's `local` (2) exceeds `frames.len()` (0).
1133		producer.abort(Error::Cancel).unwrap();
1134
1135		let result = consumer.read_frame().now_or_never().unwrap();
1136		assert!(matches!(result, Err(Error::Cancel)), "expected Cancel, got {result:?}");
1137	}
1138
1139	/// Dropping a consumer mid-batch must drop the buffered-but-untaken frames
1140	/// (exercises the `MaybeUninit` Drop path; run under miri to catch leaks/UB).
1141	#[test]
1142	fn drop_with_partial_batch() {
1143		let mut producer = Info { sequence: 0 }.produce();
1144		for _ in 0..Prefetch::CAP {
1145			producer.write_frame(Timestamp::ZERO, Bytes::from_static(b"x")).unwrap();
1146		}
1147		producer.finish().unwrap();
1148
1149		let mut consumer = producer.consume();
1150		// Take one frame so the batch is filled but only partially drained.
1151		let _ = consumer.read_frame().now_or_never().unwrap().unwrap().unwrap();
1152		drop(consumer);
1153	}
1154
1155	/// A frame whose timestamp is at a different scale is converted to the group's
1156	/// scale by `create_frame`.
1157	#[test]
1158	fn create_frame_converts_mismatched_scale() {
1159		use crate::{Timescale, Timestamp};
1160
1161		let mut producer = Producer::new(
1162			Info { sequence: 0 },
1163			track::Info::default().with_timescale(Timescale::MICRO),
1164		);
1165		let frame = frame::Info {
1166			size: 3,
1167			timestamp: Timestamp::from_millis(1).unwrap(), // 1ms -> 1000µs
1168		};
1169		let writer = producer.create_frame(frame).unwrap();
1170		assert_eq!(writer.timestamp.scale(), Timescale::MICRO);
1171		assert_eq!(writer.timestamp.value(), 1000);
1172	}
1173
1174	/// An explicit current timestamp is converted to the group's scale.
1175	#[tokio::test]
1176	async fn create_frame_converts_current_timestamp() {
1177		use crate::Timescale;
1178
1179		let mut producer = Producer::new(
1180			Info { sequence: 0 },
1181			track::Info::default().with_timescale(Timescale::MICRO),
1182		);
1183		let writer = producer
1184			.create_frame(frame::Info {
1185				size: 3,
1186				timestamp: Timestamp::now(),
1187			})
1188			.unwrap();
1189		assert_eq!(writer.timestamp.scale(), Timescale::MICRO);
1190		assert!(!writer.timestamp.is_zero(), "local clock should be non-zero");
1191	}
1192
1193	/// The per-frame size cap (the group byte budget) is enforced before allocating.
1194	#[test]
1195	fn create_frame_rejects_oversized() {
1196		let mut producer = Info { sequence: 0 }.produce();
1197		let result = producer.create_frame(frame::Info {
1198			size: MAX_GROUP_CACHE + 1,
1199			timestamp: Timestamp::ZERO,
1200		});
1201		assert!(matches!(result, Err(Error::FrameTooLarge)));
1202	}
1203}