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::sync::Arc;
16use std::task::{Poll, ready};
17
18use crate::{Error, IntoBytes, Result, Timestamp};
19
20/// Maximum total size of frames cached in a group before old frames are evicted.
21///
22/// Doubles as the per-frame size cap: a single frame can be at most this large (a
23/// larger declared size is refused before allocating), so one maximum-size frame can
24/// fill a group's cache.
25pub const MAX_CACHE_BYTES: u64 = 32 * 1024 * 1024; // 32 MB
26
27/// Slots `VecDeque` rounds a group's first frame up to.
28///
29/// A `RawVec` detail rather than a knob, so it is asserted rather than trusted: std
30/// handing out more would silently undercharge every cached group.
31const FRAME_SLOTS: usize = 4;
32
33/// Heap one cached group costs beyond its frame payloads, excluding the track-side
34/// bookkeeping in [`track::CACHE_OVERHEAD`].
35///
36/// A group is one kio channel (allocated whether or not anything ever parks on it), the
37/// `Arc<Alive>` its producer clones share, and the frame slots the first write rounds up
38/// to. Half of [`cache::ENTRY_OVERHEAD`]; see it for why this is derived rather than
39/// measured.
40pub(crate) const CACHE_OVERHEAD: u64 = (kio::Producer::<GroupState>::HEAP
41	// `Alive` behind an `Arc`'s two reference counts, which it is pointer-aligned to sit
42	// straight after.
43	+ 2 * size_of::<usize>()
44	+ size_of::<Alive>()
45	+ FRAME_SLOTS * size_of::<Frame>()) as u64;
46
47/// A group contains a sequence number because they can arrive out of order.
48///
49/// You can use [track::Producer::append_group] if you just want to +1 the sequence number.
50#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)]
51pub struct Info {
52	/// Per-track sequence number used to detect ordering and gaps. Higher numbers
53	/// supersede lower ones; consumers may skip late arrivals.
54	pub sequence: u64,
55}
56
57impl Info {
58	/// Create an untimed producer for this group.
59	///
60	/// Test-only: real groups are created via [`track::Producer`], which
61	/// supplies the parent track's [`track::Info`]. This helper exists for in-crate
62	/// tests that don't exercise timestamps.
63	#[cfg(test)]
64	pub(crate) fn produce(self) -> Producer {
65		Producer::new(self, track::Info::default(), Default::default())
66	}
67}
68
69impl From<usize> for Info {
70	fn from(sequence: usize) -> Self {
71		Self {
72			sequence: sequence as u64,
73		}
74	}
75}
76
77impl From<u64> for Info {
78	fn from(sequence: u64) -> Self {
79		Self { sequence }
80	}
81}
82
83impl From<u32> for Info {
84	fn from(sequence: u32) -> Self {
85		Self {
86			sequence: sequence as u64,
87		}
88	}
89}
90
91impl From<u16> for Info {
92	fn from(sequence: u16) -> Self {
93		Self {
94			sequence: sequence as u64,
95		}
96	}
97}
98
99/// The in-flight (tail) frame being written. At most one exists at a time, since a
100/// group is a single ordered stream.
101pub(crate) struct Partial {
102	timestamp: Timestamp,
103	buf: FrameBuf,
104}
105
106/// Shared group state. `pub(crate)` so [`frame`] handles can observe the abort flag
107/// while streaming a partial frame.
108#[derive(Default)]
109pub(crate) struct GroupState {
110	// Completed frames, each a contiguous payload. Evicted frames are popped from the
111	// front; `offset` tracks how many.
112	pub(crate) frames: VecDeque<Frame>,
113
114	// The single in-flight frame, if one is open.
115	pub(crate) partial: Option<Partial>,
116
117	// The number of frames evicted from the front of the group.
118	pub(crate) offset: usize,
119
120	// The total size (in bytes) of all cached frames plus any in-flight frame.
121	pub(crate) cache: u64,
122
123	// Mirrors `cache` into the track's shared cache pool, so the group's bytes count
124	// against the byte budget tracks evict toward.
125	charge: cache::Charge,
126
127	// Once finalized, the total number of frames the group will ever contain. Recorded
128	// at finish so the count outlives an abort that clears the cache.
129	pub(crate) fin: Option<usize>,
130
131	// The error that caused the group to be aborted, if any.
132	pub(crate) abort: Option<Error>,
133}
134
135impl GroupState {
136	/// Resolve the source for the frame at `index`: a completed frame (whole) or the
137	/// in-flight tail (streamed). Used by [`Consumer::poll_next_frame`].
138	fn poll_frame_source(&self, index: usize) -> Poll<Result<Option<(frame::Info, frame::Source)>>> {
139		if index < self.offset {
140			return Poll::Ready(Err(Error::Lagged));
141		}
142		let local = index - self.offset;
143		if let Some(f) = self.frames.get(local) {
144			// A frame read is a cache access: stamp it so expiry and the eviction
145			// walk spare a group a consumer is actively draining.
146			self.charge.refresh();
147			let info = frame::Info {
148				size: f.payload.len() as u64,
149				timestamp: f.timestamp,
150			};
151			return Poll::Ready(Ok(Some((info, frame::Source::Complete(f.payload.clone())))));
152		}
153		if local == self.frames.len()
154			&& let Some(p) = &self.partial
155		{
156			self.charge.refresh();
157			let info = frame::Info {
158				size: p.buf.capacity() as u64,
159				timestamp: p.timestamp,
160			};
161			return Poll::Ready(Ok(Some((info, frame::Source::Partial(p.buf.clone())))));
162		}
163		ready!(self.poll_terminal(index))?;
164		Poll::Ready(Ok(None))
165	}
166
167	/// Resolve the group's terminal state for a reader positioned at `index`.
168	///
169	/// A finished group is still aborted once its frames are released to free memory
170	/// (aged out of the track's latency window, or evicted by the cache pool). A reader
171	/// that already consumed every frame is missing nothing, so it gets the clean end of
172	/// group; one that fell short sees the abort rather than a silently truncated stream.
173	fn poll_terminal(&self, index: usize) -> Poll<Result<()>> {
174		match (self.fin, &self.abort) {
175			(Some(total), Some(err)) if index < total => Poll::Ready(Err(err.clone())),
176			(Some(_), _) => Poll::Ready(Ok(())),
177			(None, Some(err)) => Poll::Ready(Err(err.clone())),
178			(None, None) => Poll::Pending,
179		}
180	}
181
182	/// Resolve whether a reader at `index` can still make progress, answering the same
183	/// question as a read without consuming anything.
184	fn poll_end(&self, index: usize) -> Poll<Result<()>> {
185		if index < self.offset {
186			return Poll::Ready(Err(Error::Lagged));
187		}
188		self.poll_terminal(index)
189	}
190
191	/// Evict completed frames from the front until within the byte budget.
192	fn evict(&mut self) {
193		while self.cache > MAX_CACHE_BYTES {
194			let Some(frame) = self.frames.pop_front() else {
195				break;
196			};
197			let size = frame.payload.len() as u64;
198			self.cache -= size;
199			self.charge.sub(size);
200			self.offset += 1;
201		}
202	}
203
204	/// Drop the cached frames (and any in-flight tail) and release their pool charge.
205	fn release(&mut self) {
206		self.frames.clear();
207		self.partial = None;
208		self.cache = 0;
209		self.charge.clear();
210	}
211}
212
213fn modify(state: &kio::Producer<GroupState>) -> Result<kio::Mut<'_, GroupState>> {
214	state.write().map_err(|r| r.abort.clone().unwrap_or(Error::Dropped))
215}
216
217/// Writes frames to a group in order.
218///
219/// Each group is delivered independently over a QUIC stream.
220/// Use [Self::write_frame] for simple single-buffer frames,
221/// or [Self::create_frame] for multi-chunk streaming writes.
222pub struct Producer {
223	// Mutable stream state.
224	state: kio::Producer<GroupState>,
225
226	// The group header containing the sequence number. A small `Copy` value,
227	// inherited by each frame (see [`Self::create_frame`]).
228	info: Info,
229
230	// The parent track's properties, inherited rather than passed piecemeal. Its
231	// `timescale` is used by [`Self::create_frame`] to normalize every frame's
232	// timestamp into the track scale before it enters the stream. Threaded down by
233	// value from [`track::Producer::create_group`] / `append_group`.
234	track: track::Info,
235
236	// The parent track's account against the shared cache pool. Held here as well as
237	// in the group's `cache::Charge` so a frame write can settle the track's eviction
238	// debt with the group lock released.
239	cache: Arc<cache::Track>,
240
241	// Ingress payload meter, set by a tagged [`track::Producer`] via
242	// [`Self::with_meter`]. Empty (no-op) for an untagged group.
243	stats: stats::Meter,
244
245	// Shared by every clone: its `Drop` is the abrupt-teardown, running exactly once
246	// when the last of them goes.
247	alive: Arc<Alive>,
248}
249
250/// Ends the group when the last [`Producer`] clone drops, including the clone the
251/// parent track holds in its cache.
252///
253/// A refcount rather than a "am I the last one?" check inside `Drop`: that answer is
254/// a snapshot, and acting on it is exactly what can invalidate it. Holding a producer
255/// of its own also keeps the state writable until the teardown has run, whatever order
256/// the last owner's fields drop in.
257struct Alive {
258	info: Info,
259	state: kio::Producer<GroupState>,
260}
261
262impl Drop for Alive {
263	fn drop(&mut self) {
264		// See track::Alive: the last producer dropping without a clean finish releases
265		// the cached frames so a stale consumer can't pin their buffers forever. A
266		// finished group keeps its cache so consumers can drain.
267		//
268		// Check Ok and Err: Ok is unreachable after a deliberate close.
269		match self.state.write() {
270			Ok(mut state) => {
271				if state.fin.is_some() || state.abort.is_some() {
272					return;
273				}
274				tracing::warn!(
275					sequence = self.info.sequence,
276					"group::Producer dropped without finish() or abort()"
277				);
278				state.release();
279			}
280			Err(state) => {
281				if state.fin.is_some() || state.abort.is_some() {
282					return;
283				}
284				tracing::warn!(
285					sequence = self.info.sequence,
286					"group::Producer dropped without finish() or abort()"
287				);
288			}
289		}
290	}
291}
292
293impl std::ops::Deref for Producer {
294	type Target = Info;
295
296	fn deref(&self) -> &Self::Target {
297		&self.info
298	}
299}
300
301impl Producer {
302	/// Create a group producer bound to its parent track's [`track::Info`] and cache
303	/// account.
304	///
305	/// Crate-private: groups are only constructed via [`track::Producer`], which
306	/// threads both down so properties like the timescale are inherited rather than
307	/// passed in. Every frame added to this group is normalized to the track's
308	/// timescale by [`Self::create_frame`].
309	///
310	/// Charges the group into `cache`, so its cached bytes count against the budget the
311	/// track evicts toward under memory pressure.
312	pub(crate) fn new(info: Info, track: track::Info, cache: Arc<cache::Track>) -> Self {
313		let state = kio::Producer::<GroupState>::default();
314		state.write().ok().expect("a new group is open").charge = cache.charge();
315		let alive = Arc::new(Alive {
316			info,
317			state: state.clone(),
318		});
319		Self {
320			info,
321			state,
322			track,
323			cache,
324			stats: stats::Meter::default(),
325			alive,
326		}
327	}
328
329	/// Attach an ingress payload meter, counting this as one delivered group.
330	/// Called by a tagged [`track::Producer`] when it creates the group.
331	pub(crate) fn with_meter(mut self, meter: stats::Meter) -> Self {
332		meter.group();
333		self.stats = meter;
334		self
335	}
336
337	/// The group header.
338	pub(crate) fn info(&self) -> Info {
339		self.info
340	}
341
342	/// The parent track's timescale.
343	pub fn timescale(&self) -> Timescale {
344		self.track.timescale
345	}
346
347	/// A helper method to write a frame from a single byte buffer.
348	///
349	/// If you want to write multiple chunks, use [Self::create_frame] to get a frame producer.
350	/// But an upfront size is required.
351	///
352	/// `timestamp` is converted into the parent track's timescale. For data without
353	/// a presentation time, pass [`Timestamp::now`] explicitly.
354	pub fn write_frame<B: IntoBytes>(&mut self, timestamp: Timestamp, data: B) -> Result<()> {
355		let timestamp = timestamp
356			.convert(self.track.timescale)
357			.map_err(|_| Error::TimestampMismatch)?;
358		let payload = data.into_bytes();
359		if payload.len() as u64 > MAX_CACHE_BYTES {
360			return Err(Error::FrameTooLarge);
361		}
362
363		let mut state = self.writable()?;
364		let size = payload.len() as u64;
365		state.cache += size;
366		state.charge.add(size);
367		state.frames.push_back(Frame { timestamp, payload });
368		state.evict();
369		drop(state);
370
371		// With the group lock released (lock order is track then group), settle
372		// eviction debt if enough has been written since the track last paid.
373		self.cache.settle();
374
375		// Ingress payload: one whole frame written.
376		self.stats.frames(1);
377		self.stats.bytes(size);
378		Ok(())
379	}
380
381	/// Take the group state for a write, refusing one that can no longer accept frames.
382	///
383	/// A group with an open frame rejects rather than appends: `create_frame` borrows
384	/// its producer exclusively, but `Producer` is `Clone`, so a second handle can
385	/// reach this while the first is still streaming. Appending around the open frame
386	/// would hand readers the batch before the frame that was opened first.
387	fn writable(&self) -> Result<kio::Mut<'_, GroupState>> {
388		let state = modify(&self.state)?;
389		if state.fin.is_some() {
390			return Err(Error::Closed);
391		}
392		if state.partial.is_some() {
393			return Err(Error::FrameOpen);
394		}
395		Ok(state)
396	}
397
398	/// Write a whole batch of frames at once, draining `frames`.
399	///
400	/// One lock covers the batch, so an ingest with several frames in hand pays the
401	/// group mutex and the track's eviction settle once rather than per frame. Build
402	/// the batch with [`frame::Buffer::push`].
403	///
404	/// The batch is validated before anything is written, so a rejected frame leaves
405	/// both the group and the buffer exactly as they were, ready to retry or redirect.
406	/// Returns [`Error::FrameOpen`] if another handle is streaming a frame into this
407	/// group, since appending around it would reorder the group.
408	pub fn write_frames<const N: usize>(&mut self, frames: &mut frame::Buffer<N>) -> Result<()> {
409		// Check the whole batch up front, without touching it: a rejected batch stays
410		// exactly as the caller built it, so it can be retried or sent elsewhere.
411		// Timestamp conversion is lossy across scales that don't divide evenly, so
412		// converting in place here would silently shift presentation times on retry.
413		for frame in frames.filled() {
414			frame
415				.timestamp
416				.convert(self.track.timescale)
417				.map_err(|_| Error::TimestampMismatch)?;
418			if frame.payload.len() as u64 > MAX_CACHE_BYTES {
419				return Err(Error::FrameTooLarge);
420			}
421		}
422
423		let count = frames.len() as u64;
424		let mut bytes = 0;
425
426		let mut state = self.writable()?;
427		// Past every fallible check: converting again can't fail, and the batch is
428		// ours from here.
429		for mut frame in frames.drain() {
430			frame.timestamp = frame
431				.timestamp
432				.convert(self.track.timescale)
433				.expect("timestamp scale checked above");
434			let size = frame.payload.len() as u64;
435			bytes += size;
436			state.cache += size;
437			state.charge.add(size);
438			state.frames.push_back(frame);
439		}
440		state.evict();
441		drop(state);
442
443		// With the group lock released (lock order is track then group), settle
444		// eviction debt if enough has been written since the track last paid.
445		self.cache.settle();
446
447		// Ingress payload: the whole batch, counted once.
448		self.stats.frames(count);
449		self.stats.bytes(bytes);
450		Ok(())
451	}
452
453	/// Create a frame with an upfront size and presentation timestamp, streamed in
454	/// chunks. Borrows the group exclusively until the returned [`frame::Producer`]
455	/// is finished or dropped, so only one frame is open at a time.
456	///
457	/// The `timestamp` is converted into the parent track's timescale, so the scale you
458	/// build it with doesn't have to match the track. Returns [`Error::FrameTooLarge`]
459	/// if the declared size exceeds the group's byte budget (refused before allocating)
460	/// or [`Error::TimestampMismatch`] if the timestamp can't be converted (overflow).
461	pub fn create_frame(&mut self, frame: frame::Info) -> Result<frame::Producer<'_>> {
462		let timestamp = frame
463			.timestamp
464			.convert(self.track.timescale)
465			.map_err(|_| Error::TimestampMismatch)?;
466		if frame.size > MAX_CACHE_BYTES {
467			return Err(Error::FrameTooLarge);
468		}
469		let buf = FrameBuf::new(frame.size as usize);
470
471		let mut state = self.writable()?;
472		state.cache += frame.size;
473		state.charge.add(frame.size);
474		state.partial = Some(Partial {
475			timestamp,
476			buf: buf.clone(),
477		});
478		state.evict();
479		drop(state);
480
481		// With the group lock released (lock order is track then group), settle
482		// eviction debt if enough has been written since the track last paid.
483		self.cache.settle();
484
485		// Ingress payload: one frame opened; its bytes are counted per chunk as the
486		// frame::Producer writes them.
487		self.stats.frames(1);
488		let meter = self.stats.clone();
489
490		let info = frame::Info {
491			size: frame.size,
492			timestamp,
493		};
494		Ok(frame::Producer::new(self, buf, info).with_meter(meter))
495	}
496
497	/// Wake consumers parked on the group channel (called after a partial write).
498	pub(crate) fn frame_notify(&self) {
499		// The chunk that was just written is a write access: restart the retention
500		// clock so a straggler group streaming a large frame isn't expired
501		// mid-write (its bytes were already charged when the frame was created).
502		// `record_write` takes `&mut`, which marks the guard modified: kio only
503		// notifies on a mutably-accessed guard's release, and that notify is what
504		// delivers the chunk to parked readers.
505		if let Ok(mut state) = self.state.write() {
506			state.charge.record_write();
507		}
508	}
509
510	/// Commit the in-flight frame as a completed frame (called by [`frame::Producer::finish`]).
511	pub(crate) fn frame_commit(&mut self, frame: Frame) -> Result<()> {
512		let mut state = modify(&self.state)?;
513		// Bytes were already counted against the cache (and the pool charge) when the
514		// frame was created; committing just moves the tail into the completed set.
515		state.partial = None;
516		state.frames.push_back(frame);
517		Ok(())
518	}
519
520	/// Fail the group because an in-flight frame couldn't complete (called by
521	/// [`frame::Producer::abort`] / its drop).
522	pub(crate) fn frame_abort(&mut self, err: Error) {
523		let _ = self.clone().abort(err);
524	}
525
526	/// Return the number of frames written so far (completed plus any in-flight).
527	pub fn frame_count(&self) -> usize {
528		let state = self.state.read();
529		state.offset + state.frames.len() + state.partial.is_some() as usize
530	}
531
532	/// Mark the group as complete; no more frames will be written.
533	///
534	/// Borrows rather than consumes, so a later failure can still be reported through
535	/// [`abort`](Self::abort). The handle also keeps the cached frames readable.
536	pub fn finish(&mut self) -> Result<()> {
537		let mut state = modify(&self.state)?;
538		// The recorded count is what tells readers the group ended, so an open frame
539		// would be left out of it and read as a clean end rather than a frame still
540		// coming. Another clone can reach this while the frame's producer holds the
541		// handle, so refuse rather than strand it. Use `abort` to end a group early.
542		if state.partial.is_some() {
543			return Err(Error::FrameOpen);
544		}
545		state.fin = Some(state.offset + state.frames.len());
546		Ok(())
547	}
548
549	/// Abort the group with the given error.
550	///
551	/// Consumes the handle. Drops the cached frames so a stale [`Consumer`] can't pin
552	/// their buffers in memory forever; consumers that haven't drained yet surface the
553	/// abort error instead of the leftover cache.
554	pub fn abort(self, err: Error) -> Result<()> {
555		let mut guard = modify(&self.state)?;
556		guard.abort = Some(err);
557		guard.release();
558		guard.close();
559		Ok(())
560	}
561
562	/// Whether the group has been aborted (including pool eviction). The track's
563	/// read paths treat an aborted cached group as absent.
564	pub(crate) fn is_aborted(&self) -> bool {
565		self.state.read().abort.is_some()
566	}
567
568	/// The group's full cached footprint (payload plus fixed overhead), used by the
569	/// track to size this group as an eviction victim.
570	pub(crate) fn cache_size(&self) -> u64 {
571		self.state.read().charge.size()
572	}
573
574	/// Tick of the group's last cache access, driving eviction protection and age
575	/// expiry (see [`cache::Pool::average`]).
576	pub(crate) fn cache_accessed(&self) -> u64 {
577		self.state.read().charge.accessed()
578	}
579
580	/// Enter the group into the evictable population: demoted from the live edge,
581	/// or inserted behind it. Idempotent; a no-op once the group is closed.
582	pub(crate) fn cache_demote(&self) {
583		if let Ok(mut state) = self.state.write() {
584			state.charge.demote();
585		}
586	}
587
588	/// Record a cache access (delivery to a subscriber, a FETCH hit, or a fetched
589	/// backfill's birth), protecting the group from eviction and restarting its
590	/// expiry clock. Stamps through a read guard, whose release never notifies, so
591	/// delivery can't wake every consumer parked on the group. Harmless on a
592	/// closed group: its charge is already cleared.
593	pub(crate) fn cache_refresh(&self) {
594		self.state.read().charge.refresh();
595	}
596
597	/// Create a new consumer for the group.
598	pub fn consume(&self) -> Consumer {
599		Consumer {
600			info: self.info,
601			state: self.state.consume(),
602			track: self.track.clone(),
603			index: 0,
604			// Untagged: a tagged track attaches the egress meter via `with_meter`
605			// when it hands the consumer to a subscriber/fetch.
606			stats: stats::Meter::default(),
607		}
608	}
609
610	/// Block until the group is closed or aborted.
611	pub async fn closed(&self) -> Error {
612		kio::wait(|waiter| self.poll_closed(waiter)).await
613	}
614
615	/// Poll until the group is closed or aborted; ready with the cause.
616	pub fn poll_closed(&self, waiter: &kio::Waiter) -> Poll<Error> {
617		self.state.poll_closed(waiter).map(|()| self.abort_reason())
618	}
619
620	/// Block until there are no active consumers.
621	pub async fn unused(&self) -> Result<()> {
622		self.state.unused().await.map_err(|_| self.abort_reason())
623	}
624
625	/// The recorded abort reason, or [`Error::Dropped`] if the group closed without one.
626	fn abort_reason(&self) -> Error {
627		self.state.read().abort.clone().unwrap_or(Error::Dropped)
628	}
629}
630
631impl Clone for Producer {
632	fn clone(&self) -> Self {
633		Self {
634			info: self.info,
635			state: self.state.clone(),
636			track: self.track.clone(),
637			cache: self.cache.clone(),
638			stats: self.stats.clone(),
639			alive: self.alive.clone(),
640		}
641	}
642}
643
644/// Consume a group, frame-by-frame.
645pub struct Consumer {
646	// Shared state with the producer.
647	state: kio::Consumer<GroupState>,
648
649	// Immutable stream state.
650	info: Info,
651
652	// The parent track's info, inherited from the producer. Its `timescale` lets the
653	// wire publisher emit per-frame timestamps at the right scale for a fetched group.
654	track: track::Info,
655
656	// The number of frames we've read.
657	// NOTE: Cloned readers inherit this offset, but then run in parallel.
658	index: usize,
659
660	// Egress payload meter, set by a tagged track via [`Self::with_meter`]. Empty
661	// (no-op) for an untagged group.
662	stats: stats::Meter,
663}
664
665impl Clone for Consumer {
666	fn clone(&self) -> Self {
667		// A clone shares the channel and inherits `index`, but then runs in parallel.
668		Self {
669			state: self.state.clone(),
670			info: self.info,
671			track: self.track.clone(),
672			index: self.index,
673			// Inherit the meter without re-counting the group: the original already
674			// counted it when the track handed it out.
675			stats: self.stats.clone(),
676		}
677	}
678}
679
680impl std::ops::Deref for Consumer {
681	type Target = Info;
682
683	fn deref(&self) -> &Self::Target {
684		&self.info
685	}
686}
687
688impl Consumer {
689	/// Attach an egress payload meter, counting this as one delivered group.
690	/// Called by a tagged track when it hands the consumer to a subscriber or fetch.
691	pub(crate) fn with_meter(mut self, meter: stats::Meter) -> Self {
692		meter.group();
693		self.stats = meter;
694		self
695	}
696
697	/// Whether the group has been aborted (including pool eviction); the abort
698	/// dropped the cached frames, so a held consumer has nothing left to read.
699	pub(crate) fn is_aborted(&self) -> bool {
700		self.state.read().abort.is_some()
701	}
702
703	/// Mark the group as still being read, so a slow drain doesn't expire it.
704	///
705	/// [`Self::read_frames`] stamps the group's cache access once per batch, which
706	/// bounds frames rather than elapsed time. A reader that takes longer than the
707	/// track's `latency_max` to work through one batch (a publisher writing to a
708	/// flow-controlled peer, say) calls this between frames, or the rest of the group
709	/// is expired out from under it mid-serve. [`Self::read_frame`] stamps on every
710	/// call and needs no help.
711	///
712	/// Cheap and idempotent within a coarse clock tick, so calling it per frame is
713	/// fine.
714	pub fn keep_alive(&self) {
715		self.state.read().charge.refresh();
716	}
717
718	/// Record a cache access from the consumer side: a parked group re-offered to
719	/// its subscriber. Same stamp as [`Producer::cache_refresh`].
720	pub(crate) fn cache_refresh(&self) {
721		self.keep_alive();
722	}
723
724	/// Park `waiter` until the group closes (finish, abort, or eviction). Spliced
725	/// subscribers register on parked groups so an eviction wakes them; a group
726	/// that already closed cleanly can never abort, so no waiter is needed.
727	pub(crate) fn poll_closed(&self, waiter: &kio::Waiter) -> Poll<()> {
728		self.state.poll_closed(waiter)
729	}
730
731	/// The parent track's timescale.
732	pub fn timescale(&self) -> Timescale {
733		self.track.timescale
734	}
735
736	/// The number of frames written so far (completed plus any in-flight), independent of
737	/// how many this consumer has read. The final total once the group is finished.
738	pub fn frame_count(&self) -> usize {
739		let state = self.state.read();
740		state
741			.fin
742			.unwrap_or(state.offset + state.frames.len() + state.partial.is_some() as usize)
743	}
744
745	/// Advance the read cursor to `sequence`, skipping every frame below it.
746	///
747	/// Skipped frames are never returned, and an eviction confined to them is not a gap:
748	/// reads resume at the cursor instead of failing with [`Error::Lagged`]. An eviction at
749	/// or above the cursor still fails, because the caller asked for that frame. The cursor
750	/// only moves forward; a `sequence` at or below it is a no-op.
751	pub fn skip_to(&mut self, sequence: u64) {
752		let sequence = usize::try_from(sequence).unwrap_or(usize::MAX);
753		self.index = self.index.max(sequence);
754	}
755
756	// A helper to automatically apply Dropped if the state is closed without an error.
757	fn poll<F, R>(&self, waiter: &kio::Waiter, f: F) -> Poll<Result<R>>
758	where
759		F: FnMut(&kio::Ref<'_, GroupState>) -> Poll<Result<R>>,
760	{
761		Poll::Ready(match ready!(self.state.poll(waiter, f)) {
762			Ok(res) => res,
763			// We try to clone abort just in case the function forgot to check for terminal state.
764			Err(state) => Err(state.abort.clone().unwrap_or(Error::Dropped)),
765		})
766	}
767
768	/// Return a consumer for the next frame for chunked reading.
769	pub async fn next_frame(&mut self) -> Result<Option<frame::Consumer>> {
770		kio::wait(|waiter| self.poll_next_frame(waiter)).await
771	}
772
773	/// Poll for the next frame, without blocking.
774	///
775	/// Returns None if the group is finished and the index is out of range.
776	pub fn poll_next_frame(&mut self, waiter: &kio::Waiter) -> Poll<Result<Option<frame::Consumer>>> {
777		let index = self.index;
778		let Some((info, source)) = ready!(self.poll(waiter, |state| state.poll_frame_source(index))?) else {
779			return Poll::Ready(Ok(None));
780		};
781
782		self.index += 1;
783		// Count the frame here; the frame::Consumer counts its bytes per chunk as
784		// they're read out.
785		self.stats.frames(1);
786		Poll::Ready(Ok(Some(
787			frame::Consumer::new(self.state.clone(), info, source).with_meter(self.stats.clone()),
788		)))
789	}
790
791	/// Read the next frame (timestamp and payload) all at once, without blocking.
792	///
793	/// Use [`Self::read_frames`] to pull a whole batch under one lock; a group of small
794	/// frames drains several times faster that way.
795	pub fn poll_read_frame(&mut self, waiter: &kio::Waiter) -> Poll<Result<Option<frame::Frame>>> {
796		let index = self.index;
797		let frame = ready!(self.poll(waiter, |state| {
798			if index < state.offset {
799				return Poll::Ready(Err(Error::Lagged));
800			}
801			if let Some(frame) = state.frames.get(index - state.offset) {
802				// A frame read is a cache access: stamp it so expiry and the eviction
803				// walk spare a group a consumer is actively draining.
804				state.charge.refresh();
805				return Poll::Ready(Ok(Some(frame.clone())));
806			}
807			// Nothing completed at `index`: an in-flight tail waits, otherwise resolve
808			// the terminal state (whole-frame reads never stream the partial).
809			state.poll_terminal(index).map_ok(|()| None)
810		})?);
811
812		if let Some(frame) = &frame {
813			self.index += 1;
814			self.stats.frames(1);
815			self.stats.bytes(frame.payload.len() as u64);
816		}
817
818		Poll::Ready(Ok(frame))
819	}
820
821	/// Read the next frame (timestamp and payload) all at once.
822	pub async fn read_frame(&mut self) -> Result<Option<frame::Frame>> {
823		kio::wait(|waiter| self.poll_read_frame(waiter)).await
824	}
825
826	/// Fill `out` with every frame that is ready, up to its capacity, without blocking.
827	///
828	/// Returns how many frames were written; they're in [`frame::Buffer::filled`]. The
829	/// buffer's previous batch is dropped first, so one buffer serves a whole group.
830	///
831	/// This is a *short* read: it returns as soon as anything is ready rather than
832	/// waiting for `out` to fill, so a partial batch does not mean the group ended.
833	/// Only a count of `0` does (and only for a non-zero capacity).
834	///
835	/// One stamp covers the whole batch, so a slow drain calls
836	/// [`Self::keep_alive`] between frames.
837	pub fn poll_read_frames<const N: usize>(
838		&mut self,
839		waiter: &kio::Waiter,
840		out: &mut frame::Buffer<N>,
841	) -> Poll<Result<usize>> {
842		// Drop the previous batch before taking the lock: deallocating payloads is the
843		// caller's cost to pay, not something to hold the group's mutex through.
844		out.clear();
845
846		let index = self.index;
847		let res = self.poll(waiter, |state| {
848			if index < state.offset {
849				return Poll::Ready(Err(Error::Lagged));
850			}
851			// `local` can run past the buffered count when frames were cleared or evicted
852			// out from under us (abort, unfinished drop, an eviction gap); clamp so
853			// `range` never panics on an out-of-bounds start.
854			let local = (index - state.offset).min(state.frames.len());
855			if out.fill(state.frames.range(local..).cloned()) > 0 {
856				// One stamp covers the whole batch.
857				state.charge.refresh();
858				return Poll::Ready(Ok(()));
859			}
860			// An empty fill means nothing completed at `index`: park on an in-flight
861			// tail, otherwise resolve the terminal state. A finished group resolves to
862			// `Ok`, leaving the zero count to report the end.
863			state.poll_terminal(index)
864		});
865
866		// A `Pending` here leaves `out` cleared, which is what an empty batch should look
867		// like to a caller that inspects it anyway.
868		ready!(res)?;
869
870		let filled = out.filled().len();
871		self.index += filled;
872		// Count the whole batch once, under no lock.
873		self.stats.frames(filled as u64);
874		self.stats
875			.bytes(out.filled().iter().map(|f| f.payload.len() as u64).sum());
876
877		Poll::Ready(Ok(filled))
878	}
879
880	/// Fill `out` with every frame that is ready, blocking until at least one is or the
881	/// group ends. Returns the batch, empty only at the end of the group.
882	///
883	/// See [`Self::poll_read_frames`] for the short-read semantics.
884	pub async fn read_frames<'a, const N: usize>(
885		&mut self,
886		out: &'a mut frame::Buffer<N>,
887	) -> Result<&'a mut [frame::Frame]> {
888		// The closure reborrows `out` for less than `'a`, so the buffer is free again
889		// once the wait resolves.
890		kio::wait(|waiter| self.poll_read_frames(waiter, out)).await?;
891		Ok(out.filled_mut())
892	}
893
894	/// Poll until the group terminates, returning this cursor's next frame index.
895	pub fn poll_finished(&mut self, waiter: &kio::Waiter) -> Poll<Result<u64>> {
896		let index = self.index;
897		ready!(self.poll(waiter, |state| state.poll_end(index)))?;
898		Poll::Ready(Ok(index as u64))
899	}
900
901	/// Block until the group terminates, returning this cursor's next frame index.
902	///
903	/// This answers for the cursor, not the group: a reader that drained every frame gets the
904	/// clean end even if the group was aborted afterwards to release its cache, while one that
905	/// stopped short gets that abort. A prior [`Self::skip_to`] contributes to the index even
906	/// though those frames were not read. Use [`Self::frame_count`] for the producer's total.
907	pub async fn finished(&mut self) -> Result<u64> {
908		kio::wait(|waiter| self.poll_finished(waiter)).await
909	}
910}
911
912/// Options for a one-shot [`track::Consumer::fetch_group`] of a past group.
913#[derive(Clone, Debug, Default)]
914#[non_exhaustive]
915pub struct Fetch {
916	/// Delivery priority for the fetched group's stream. Defaults to 0.
917	pub priority: u8,
918}
919
920impl Fetch {
921	/// Set the delivery priority, returning `self` for chaining.
922	pub fn with_priority(mut self, priority: u8) -> Self {
923		self.priority = priority;
924		self
925	}
926}
927
928#[cfg(test)]
929mod test {
930	use super::*;
931	use crate::model::test_tracing::count_drop_warnings;
932	use bytes::Bytes;
933	use futures::FutureExt;
934
935	/// [`FRAME_SLOTS`] is std's rounding, not ours, so measure it: a larger real value
936	/// would undercharge every cached group without touching a line of this crate.
937	#[test]
938	fn one_frame_fits_the_charged_slots() {
939		let mut frames: VecDeque<Frame> = VecDeque::new();
940		frames.push_back(Frame {
941			timestamp: Timestamp::ZERO,
942			payload: Bytes::new(),
943		});
944		let capacity = frames.capacity();
945		assert!(
946			capacity <= FRAME_SLOTS,
947			"a one-frame deque now allocates {capacity} slots"
948		);
949	}
950
951	#[test]
952	fn basic_frame_reading() {
953		let mut producer = Info { sequence: 0 }.produce();
954		producer
955			.write_frame(Timestamp::ZERO, Bytes::from_static(b"frame0"))
956			.unwrap();
957		producer
958			.write_frame(Timestamp::ZERO, Bytes::from_static(b"frame1"))
959			.unwrap();
960		producer.finish().unwrap();
961
962		let mut consumer = producer.consume();
963		let f0 = consumer.next_frame().now_or_never().unwrap().unwrap().unwrap();
964		assert_eq!(f0.size, 6);
965		let f1 = consumer.next_frame().now_or_never().unwrap().unwrap().unwrap();
966		assert_eq!(f1.size, 6);
967		let end = consumer.next_frame().now_or_never().unwrap().unwrap();
968		assert!(end.is_none());
969	}
970
971	/// Write `n` frames with payloads "0".."n-1" into a fresh group.
972	fn filled_group(n: usize) -> Producer {
973		let mut producer = Info { sequence: 0 }.produce();
974		for i in 0..n {
975			producer
976				.write_frame(Timestamp::ZERO, Bytes::from(i.to_string()))
977				.unwrap();
978		}
979		producer
980	}
981
982	/// The payload strings of a batch.
983	fn payloads(frames: &[Frame]) -> Vec<String> {
984		frames
985			.iter()
986			.map(|frame| String::from_utf8(frame.payload.to_vec()).unwrap())
987			.collect()
988	}
989
990	/// Drain a consumer through a batch buffer of `N`, collecting payload strings.
991	fn drain<const N: usize>(consumer: &mut Consumer) -> Vec<String> {
992		let mut buf = frame::Buffer::<N>::new();
993		let mut seen = Vec::new();
994		loop {
995			let batch = consumer.read_frames(&mut buf).now_or_never().unwrap().unwrap();
996			if batch.is_empty() {
997				break;
998			}
999			seen.extend(payloads(batch));
1000		}
1001		seen
1002	}
1003
1004	/// `create_frame` borrows its producer exclusively, but `Producer` is `Clone`, so a
1005	/// second handle can reach the whole-frame writes while a frame is still open.
1006	/// Appending there would hand readers the new frames before the one opened first,
1007	/// so every whole-frame path refuses instead.
1008	#[test]
1009	fn writes_are_refused_while_a_frame_is_open() {
1010		let mut producer = Info { sequence: 0 }.produce();
1011		let mut other = producer.clone();
1012
1013		// One handle opens a frame and holds it, incomplete.
1014		let mut open = producer
1015			.create_frame(frame::Info {
1016				size: 4,
1017				timestamp: Timestamp::ZERO,
1018			})
1019			.unwrap();
1020
1021		let mut buf = frame::Buffer::<4>::new();
1022		buf.push(frame::Frame {
1023			timestamp: Timestamp::ZERO,
1024			payload: Bytes::from_static(b"batch"),
1025		})
1026		.unwrap();
1027
1028		assert!(matches!(other.write_frames(&mut buf), Err(Error::FrameOpen)));
1029		assert_eq!(buf.len(), 1, "the batch is still the caller's");
1030		assert!(matches!(
1031			other.write_frame(Timestamp::ZERO, Bytes::from_static(b"single")),
1032			Err(Error::FrameOpen)
1033		));
1034		assert!(matches!(
1035			other
1036				.create_frame(frame::Info {
1037					size: 1,
1038					timestamp: Timestamp::ZERO,
1039				})
1040				.err(),
1041			Some(Error::FrameOpen)
1042		));
1043
1044		// Once the open frame lands, the group takes writes again in order.
1045		open.write(&b"open"[..]).unwrap();
1046		open.finish().unwrap();
1047		other.write_frames(&mut buf).unwrap();
1048		other.finish().unwrap();
1049
1050		let mut consumer = other.consume();
1051		assert_eq!(drain::<4>(&mut consumer), ["open", "batch"]);
1052	}
1053
1054	/// Finishing records the frame count, and a batch read consults that count to
1055	/// decide the group ended. `create_frame` borrows its producer exclusively, but
1056	/// `Producer` is `Clone`, so a second handle can finish the group while the first
1057	/// is still writing a frame. The open frame would be left out of the count and
1058	/// read as a clean end of group, so a publisher would close the stream without
1059	/// ever sending it.
1060	#[test]
1061	fn finish_is_refused_while_a_frame_is_open() {
1062		let mut producer = Info { sequence: 0 }.produce();
1063		let mut other = producer.clone();
1064		let mut consumer = producer.consume();
1065
1066		let mut frame = producer
1067			.create_frame(frame::Info {
1068				size: 4,
1069				timestamp: Timestamp::ZERO,
1070			})
1071			.unwrap();
1072
1073		assert!(matches!(other.finish(), Err(Error::FrameOpen)));
1074
1075		// Not "the group ended": the batch read parks until the frame lands.
1076		let mut buf = frame::Buffer::<4>::new();
1077		assert!(
1078			consumer.read_frames(&mut buf).now_or_never().is_none(),
1079			"an open frame must not read as the end of the group"
1080		);
1081
1082		frame.write(&b"open"[..]).unwrap();
1083		frame.finish().unwrap();
1084		other.finish().unwrap();
1085
1086		let batch = consumer.read_frames(&mut buf).now_or_never().unwrap().unwrap();
1087		assert_eq!(batch.len(), 1);
1088		assert_eq!(batch[0].payload, Bytes::from_static(b"open"));
1089	}
1090
1091	#[test]
1092	fn write_frames_appends_the_whole_batch() {
1093		let mut producer = Info { sequence: 0 }.produce();
1094		let mut buf = frame::Buffer::<8>::new();
1095		for i in 0..5u8 {
1096			buf.push(frame::Frame {
1097				timestamp: Timestamp::ZERO,
1098				payload: Bytes::from(i.to_string()),
1099			})
1100			.unwrap();
1101		}
1102		producer.write_frames(&mut buf).unwrap();
1103		assert!(buf.is_empty(), "the batch was drained");
1104		producer.finish().unwrap();
1105
1106		let mut consumer = producer.consume();
1107		assert_eq!(drain::<8>(&mut consumer), ["0", "1", "2", "3", "4"]);
1108	}
1109
1110	/// A rejected frame must leave both the group and the batch untouched, or the
1111	/// caller has no way to tell what was written.
1112	#[test]
1113	fn write_frames_rejects_the_batch_atomically() {
1114		let mut producer = Info { sequence: 0 }.produce();
1115		let mut buf = frame::Buffer::<4>::new();
1116		buf.push(frame::Frame {
1117			timestamp: Timestamp::ZERO,
1118			payload: Bytes::from_static(b"ok"),
1119		})
1120		.unwrap();
1121		// Larger than the group's whole byte budget.
1122		buf.push(frame::Frame {
1123			timestamp: Timestamp::ZERO,
1124			payload: Bytes::from(vec![0u8; MAX_CACHE_BYTES as usize + 1]),
1125		})
1126		.unwrap();
1127
1128		assert!(matches!(producer.write_frames(&mut buf), Err(Error::FrameTooLarge)));
1129		assert_eq!(buf.len(), 2, "the batch is still the caller's");
1130
1131		producer.finish().unwrap();
1132		let mut consumer = producer.consume();
1133		assert!(drain::<4>(&mut consumer).is_empty(), "nothing was written");
1134	}
1135
1136	/// A batch rejected mid-validation must leave the caller's frames byte-identical,
1137	/// including their timestamps: converting in place would compound scale loss if
1138	/// the batch is retried against another track.
1139	#[test]
1140	fn write_frames_leaves_a_rejected_batch_unconverted() {
1141		use crate::Timescale;
1142
1143		let mut producer = Producer::new(
1144			Info { sequence: 0 },
1145			track::Info::default().with_timescale(Timescale::MICRO),
1146			Default::default(),
1147		);
1148
1149		let mut buf = frame::Buffer::<4>::new();
1150		buf.push(frame::Frame {
1151			timestamp: Timestamp::from_millis(1).unwrap(),
1152			payload: Bytes::from_static(b"ok"),
1153		})
1154		.unwrap();
1155		// Refused after the first frame would already have been converted in place.
1156		buf.push(frame::Frame {
1157			timestamp: Timestamp::from_millis(2).unwrap(),
1158			payload: Bytes::from(vec![0u8; MAX_CACHE_BYTES as usize + 1]),
1159		})
1160		.unwrap();
1161
1162		assert!(matches!(producer.write_frames(&mut buf), Err(Error::FrameTooLarge)));
1163		let kept = buf.filled();
1164		assert_eq!(kept.len(), 2, "the batch is still the caller's");
1165		assert_eq!(kept[0].timestamp.scale(), Timescale::MILLI, "timestamp was rewritten");
1166		assert_eq!(kept[0].timestamp.value(), 1);
1167	}
1168
1169	/// A batch that is accepted still converts into the track's scale.
1170	#[test]
1171	fn write_frames_converts_into_the_track_scale() {
1172		use crate::Timescale;
1173
1174		let mut producer = Producer::new(
1175			Info { sequence: 0 },
1176			track::Info::default().with_timescale(Timescale::MICRO),
1177			Default::default(),
1178		);
1179
1180		let mut buf = frame::Buffer::<4>::new();
1181		buf.push(frame::Frame {
1182			timestamp: Timestamp::from_millis(1).unwrap(),
1183			payload: Bytes::from_static(b"x"),
1184		})
1185		.unwrap();
1186		producer.write_frames(&mut buf).unwrap();
1187		producer.finish().unwrap();
1188
1189		let frame = producer
1190			.consume()
1191			.read_frame()
1192			.now_or_never()
1193			.unwrap()
1194			.unwrap()
1195			.unwrap();
1196		assert_eq!(frame.timestamp.scale(), Timescale::MICRO);
1197		assert_eq!(frame.timestamp.value(), 1000);
1198	}
1199
1200	#[test]
1201	fn buffer_push_refuses_past_capacity() {
1202		let mut buf = frame::Buffer::<2>::new();
1203		let frame = || frame::Frame {
1204			timestamp: Timestamp::ZERO,
1205			payload: Bytes::from_static(b"x"),
1206		};
1207		buf.push(frame()).unwrap();
1208		buf.push(frame()).unwrap();
1209		assert!(buf.is_full());
1210		assert!(buf.push(frame()).is_err(), "a full buffer hands the frame back");
1211	}
1212
1213	/// A partially consumed drain still empties the buffer, dropping the rest.
1214	#[test]
1215	fn buffer_drain_empties_even_when_abandoned() {
1216		let mut buf = frame::Buffer::<4>::new();
1217		for i in 0..4u8 {
1218			buf.push(frame::Frame {
1219				timestamp: Timestamp::ZERO,
1220				payload: Bytes::from(vec![i; 1]),
1221			})
1222			.unwrap();
1223		}
1224		let taken: Vec<_> = buf.drain().take(2).collect();
1225		assert_eq!(taken.len(), 2);
1226		assert!(buf.is_empty(), "an abandoned drain still empties the buffer");
1227	}
1228
1229	#[test]
1230	fn read_frames_fills_whole_batch() {
1231		let mut producer = filled_group(5);
1232		producer.finish().unwrap();
1233
1234		let mut consumer = producer.consume();
1235		let mut buf = frame::Buffer::<8>::new();
1236
1237		let batch = consumer.read_frames(&mut buf).now_or_never().unwrap().unwrap();
1238		assert_eq!(payloads(batch), ["0", "1", "2", "3", "4"]);
1239
1240		// A finished group reports the end with an empty batch.
1241		let batch = consumer.read_frames(&mut buf).now_or_never().unwrap().unwrap();
1242		assert!(batch.is_empty());
1243	}
1244
1245	#[test]
1246	fn read_frames_bounded_by_capacity() {
1247		let mut producer = filled_group(5);
1248		producer.finish().unwrap();
1249
1250		let mut consumer = producer.consume();
1251		assert_eq!(drain::<2>(&mut consumer), ["0", "1", "2", "3", "4"]);
1252	}
1253
1254	#[test]
1255	fn read_frames_resumes_after_a_single_read() {
1256		let mut producer = filled_group(12);
1257		producer.finish().unwrap();
1258
1259		let mut consumer = producer.consume();
1260		let first = consumer.read_frame().now_or_never().unwrap().unwrap().unwrap();
1261		assert_eq!(first.payload, Bytes::from_static(b"0"));
1262
1263		assert_eq!(
1264			drain::<8>(&mut consumer),
1265			["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11"]
1266		);
1267	}
1268
1269	#[test]
1270	fn read_frames_returns_short_instead_of_waiting() {
1271		let mut producer = filled_group(2);
1272
1273		let mut consumer = producer.consume();
1274		let mut buf = frame::Buffer::<8>::new();
1275
1276		// The group is still open, so the batch is short rather than blocking for more.
1277		let batch = consumer.read_frames(&mut buf).now_or_never().unwrap().unwrap();
1278		assert_eq!(payloads(batch), ["0", "1"]);
1279
1280		// Nothing left and no terminal state: this one parks.
1281		assert!(consumer.read_frames(&mut buf).now_or_never().is_none());
1282
1283		producer.write_frame(Timestamp::ZERO, Bytes::from_static(b"2")).unwrap();
1284		let batch = consumer.read_frames(&mut buf).now_or_never().unwrap().unwrap();
1285		assert_eq!(payloads(batch), ["2"]);
1286	}
1287
1288	#[test]
1289	fn read_frames_reports_an_abort() {
1290		let producer = filled_group(2);
1291		let mut consumer = producer.consume();
1292		producer.abort(Error::Cancel).unwrap();
1293
1294		// The abort released the cached frames, so nothing survives it.
1295		let mut buf = frame::Buffer::<8>::new();
1296		let res = consumer.read_frames(&mut buf).now_or_never().unwrap();
1297		assert!(matches!(res, Err(Error::Cancel)));
1298	}
1299
1300	/// A refill drops the previous batch, so a reused buffer never accumulates frames.
1301	#[test]
1302	fn read_frames_refill_replaces_the_previous_batch() {
1303		let mut producer = filled_group(3);
1304		producer.finish().unwrap();
1305
1306		let mut consumer = producer.consume();
1307		let mut buf = frame::Buffer::<2>::new();
1308
1309		let batch = consumer.read_frames(&mut buf).now_or_never().unwrap().unwrap();
1310		assert_eq!(payloads(batch), ["0", "1"]);
1311
1312		let batch = consumer.read_frames(&mut buf).now_or_never().unwrap().unwrap();
1313		assert_eq!(payloads(batch), ["2"]);
1314		assert_eq!(buf.filled().len(), 1, "the buffer holds only the latest batch");
1315	}
1316
1317	#[test]
1318	fn read_frames_zero_capacity_reads_nothing() {
1319		let mut producer = filled_group(2);
1320		producer.finish().unwrap();
1321
1322		let mut consumer = producer.consume();
1323		let mut buf = frame::Buffer::<0>::new();
1324		let batch = consumer.read_frames(&mut buf).now_or_never().unwrap().unwrap();
1325		assert!(batch.is_empty());
1326
1327		// The reader did not advance.
1328		let frame = consumer.read_frame().now_or_never().unwrap().unwrap().unwrap();
1329		assert_eq!(frame.payload, Bytes::from_static(b"0"));
1330	}
1331
1332	#[test]
1333	fn read_frame_all_at_once() {
1334		let mut producer = Info { sequence: 0 }.produce();
1335		producer
1336			.write_frame(Timestamp::ZERO, Bytes::from_static(b"hello"))
1337			.unwrap();
1338		producer.finish().unwrap();
1339
1340		let mut consumer = producer.consume();
1341		let frame = consumer.read_frame().now_or_never().unwrap().unwrap().unwrap();
1342		assert_eq!(frame.payload, Bytes::from_static(b"hello"));
1343	}
1344
1345	#[test]
1346	fn read_frame_preserves_timestamp() {
1347		let mut producer = Info { sequence: 0 }.produce();
1348		let timestamp = Timestamp::from_micros(20_000).unwrap();
1349		producer.write_frame(timestamp, Bytes::from_static(b"hello")).unwrap();
1350		producer.finish().unwrap();
1351
1352		let mut consumer = producer.consume();
1353		let frame = consumer.read_frame().now_or_never().unwrap().unwrap().unwrap();
1354		assert_eq!(frame.timestamp.as_micros(), 20_000);
1355		assert_eq!(frame.payload, Bytes::from_static(b"hello"));
1356	}
1357
1358	#[test]
1359	fn chunked_frame_reads_whole() {
1360		let mut producer = Info { sequence: 0 }.produce();
1361		{
1362			let mut frame = producer
1363				.create_frame(frame::Info {
1364					size: 10,
1365					timestamp: Timestamp::ZERO,
1366				})
1367				.unwrap();
1368			frame.write(Bytes::from_static(b"hello")).unwrap();
1369			frame.write(Bytes::from_static(b"world")).unwrap();
1370			frame.finish().unwrap();
1371		}
1372		producer.finish().unwrap();
1373
1374		// Frame data is held in a single per-frame buffer; a whole-frame read returns
1375		// the full contents in one slice.
1376		let mut consumer = producer.consume();
1377		let frame = consumer.read_frame().now_or_never().unwrap().unwrap().unwrap();
1378		assert_eq!(frame.payload, Bytes::from_static(b"helloworld"));
1379	}
1380
1381	#[test]
1382	fn chunked_frame_streams_partial() {
1383		let mut producer = Info { sequence: 0 }.produce();
1384		let mut consumer = producer.consume();
1385
1386		let mut frame = producer
1387			.create_frame(frame::Info {
1388				size: 6,
1389				timestamp: Timestamp::ZERO,
1390			})
1391			.unwrap();
1392		frame.write(Bytes::from_static(b"foo")).unwrap();
1393
1394		// A consumer can stream the in-flight tail before it's finished.
1395		let mut f = consumer.next_frame().now_or_never().unwrap().unwrap().unwrap();
1396		let c1 = f.read_chunk().now_or_never().unwrap().unwrap();
1397		assert_eq!(c1, Some(Bytes::from_static(b"foo")));
1398		assert!(f.read_chunk().now_or_never().is_none());
1399
1400		frame.write(Bytes::from_static(b"bar")).unwrap();
1401		frame.finish().unwrap();
1402
1403		let c2 = f.read_chunk().now_or_never().unwrap().unwrap();
1404		assert_eq!(c2, Some(Bytes::from_static(b"bar")));
1405		let c3 = f.read_chunk().now_or_never().unwrap().unwrap();
1406		assert_eq!(c3, None);
1407	}
1408
1409	#[test]
1410	fn group_finish_returns_none() {
1411		let mut producer = Info { sequence: 0 }.produce();
1412		producer.finish().unwrap();
1413
1414		let mut consumer = producer.consume();
1415		let end = consumer.next_frame().now_or_never().unwrap().unwrap();
1416		assert!(end.is_none());
1417	}
1418
1419	#[test]
1420	fn abort_propagates() {
1421		let producer = Info { sequence: 0 }.produce();
1422		let mut consumer = producer.consume();
1423		producer.abort(crate::Error::Cancel).unwrap();
1424
1425		let result = consumer.next_frame().now_or_never().unwrap();
1426		assert!(matches!(result, Err(crate::Error::Cancel)));
1427	}
1428
1429	#[test]
1430	fn abort_clears_cached_frames() {
1431		let mut producer = Info { sequence: 0 }.produce();
1432		producer
1433			.write_frame(Timestamp::ZERO, Bytes::from_static(b"data"))
1434			.unwrap();
1435
1436		// A stale consumer that never reads must not pin the cached frames.
1437		let _consumer = producer.consume();
1438		assert_eq!(producer.state.read().frames.len(), 1);
1439
1440		producer.clone().abort(crate::Error::Cancel).unwrap();
1441
1442		let state = producer.state.read();
1443		assert!(state.frames.is_empty(), "cached frames should be dropped on abort");
1444		assert_eq!(state.cache, 0);
1445	}
1446
1447	#[test]
1448	fn drop_unfinished_clears_cached_frames() {
1449		let producer = Info { sequence: 0 }.produce();
1450		let mut writer = producer.clone();
1451		writer
1452			.write_frame(Timestamp::ZERO, Bytes::from_static(b"data"))
1453			.unwrap();
1454
1455		// A stale consumer keeps the channel (and thus the cache) alive.
1456		let mut consumer = producer.consume();
1457		assert_eq!(producer.state.read().frames.len(), 1);
1458
1459		// Drop every producer without finishing: the cache is released.
1460		drop(writer);
1461		drop(producer);
1462
1463		let result = consumer.next_frame().now_or_never().unwrap();
1464		assert!(matches!(result, Err(crate::Error::Dropped)));
1465	}
1466
1467	#[test]
1468	fn drop_after_abort_does_not_warn() {
1469		let warns = count_drop_warnings("group::Producer dropped without finish", || {
1470			let producer = Info { sequence: 0 }.produce();
1471			let keep = producer.clone();
1472			let mut writer = producer.clone();
1473			writer
1474				.write_frame(Timestamp::ZERO, Bytes::from_static(b"data"))
1475				.unwrap();
1476			let _consumer = producer.consume();
1477			writer.abort(crate::Error::Cancel).unwrap();
1478			drop(keep);
1479		});
1480		assert_eq!(warns, 0, "abort-then-drop must not emit unfinished-producer WARN");
1481	}
1482
1483	#[test]
1484	fn drop_unfinished_warns() {
1485		let warns = count_drop_warnings("group::Producer dropped without finish", || {
1486			let producer = Info { sequence: 0 }.produce();
1487			let mut writer = producer.clone();
1488			writer
1489				.write_frame(Timestamp::ZERO, Bytes::from_static(b"data"))
1490				.unwrap();
1491			let _consumer = producer.consume();
1492			drop(writer);
1493			drop(producer);
1494		});
1495		assert!(warns >= 1, "unfinished drop must emit unfinished-producer WARN");
1496	}
1497
1498	#[test]
1499	fn drop_finished_keeps_cached_frames() {
1500		let mut producer = Info { sequence: 0 }.produce();
1501		producer
1502			.write_frame(Timestamp::ZERO, Bytes::from_static(b"data"))
1503			.unwrap();
1504		producer.finish().unwrap();
1505
1506		let mut consumer = producer.consume();
1507		drop(producer);
1508
1509		// A cleanly finished group keeps its cache so the consumer can still drain.
1510		let frame = consumer.read_frame().now_or_never().unwrap().unwrap().unwrap();
1511		assert_eq!(frame.payload, Bytes::from_static(b"data"));
1512	}
1513
1514	#[tokio::test]
1515	async fn pending_then_ready() {
1516		let mut producer = Info { sequence: 0 }.produce();
1517		let mut consumer = producer.consume();
1518
1519		// Consumer blocks because no frames yet.
1520		assert!(consumer.next_frame().now_or_never().is_none());
1521
1522		producer
1523			.write_frame(Timestamp::ZERO, Bytes::from_static(b"data"))
1524			.unwrap();
1525		producer.finish().unwrap();
1526
1527		let frame = consumer.next_frame().now_or_never().unwrap().unwrap().unwrap();
1528		assert_eq!(frame.size, 4);
1529	}
1530
1531	#[test]
1532	fn eviction_drops_old_frames() {
1533		let mut producer = Info { sequence: 0 }.produce();
1534
1535		// Write frames that total more than MAX_CACHE_BYTES.
1536		let big = Bytes::from(vec![0u8; MAX_CACHE_BYTES as usize]);
1537		producer.write_frame(Timestamp::ZERO, big.clone()).unwrap();
1538		producer.write_frame(Timestamp::ZERO, big).unwrap();
1539
1540		// The first frame should have been evicted (tombstoned via offset).
1541		let state = producer.state.read();
1542		assert_eq!(state.offset, 1);
1543		assert_eq!(state.frames.len(), 1);
1544		assert_eq!(state.frames[0].payload.len(), MAX_CACHE_BYTES as usize);
1545	}
1546
1547	#[test]
1548	fn next_frame_returns_cache_full_on_tombstone() {
1549		let mut producer = Info { sequence: 0 }.produce();
1550
1551		let big = Bytes::from(vec![0u8; MAX_CACHE_BYTES as usize]);
1552		producer.write_frame(Timestamp::ZERO, big.clone()).unwrap();
1553		producer.write_frame(Timestamp::ZERO, big).unwrap();
1554
1555		let mut consumer = producer.consume();
1556		// First frame was evicted, next_frame should return Lagged.
1557		let result = consumer.next_frame().now_or_never().unwrap();
1558		assert!(matches!(result, Err(crate::Error::Lagged)));
1559	}
1560
1561	/// A cursor at the eviction boundary never asked for the missing frames, so the read
1562	/// proceeds at the retained tail; a cursor one below it has a gap and lags.
1563	#[test]
1564	fn skip_to_tolerates_an_eviction_below_it() {
1565		let mut producer = Info { sequence: 0 }.produce();
1566
1567		// Two oversized frames overflow the budget once the small frame lands: frames 0
1568		// and 1 evict, frame 2 is retained.
1569		let big = Bytes::from(vec![0u8; MAX_CACHE_BYTES as usize]);
1570		producer.write_frame(Timestamp::ZERO, big.clone()).unwrap();
1571		producer.write_frame(Timestamp::ZERO, big).unwrap();
1572		producer
1573			.write_frame(Timestamp::ZERO, Bytes::from_static(b"tail"))
1574			.unwrap();
1575		assert_eq!(producer.state.read().offset, 2);
1576
1577		let mut reader = producer.consume();
1578		reader.skip_to(2);
1579		let frame = reader.next_frame().now_or_never().unwrap().unwrap().unwrap();
1580		assert_eq!(frame.size, 4);
1581
1582		let mut behind = producer.consume();
1583		behind.skip_to(1);
1584		let result = behind.next_frame().now_or_never().unwrap();
1585		assert!(matches!(result, Err(crate::Error::Lagged)));
1586	}
1587
1588	#[test]
1589	fn no_eviction_under_budget() {
1590		let mut producer = Info { sequence: 0 }.produce();
1591		// Many small frames stay cached: there is no frame-count cap, only a byte budget.
1592		for _ in 0..100_000 {
1593			producer.write_frame(Timestamp::ZERO, Bytes::from_static(b"x")).unwrap();
1594		}
1595		producer.finish().unwrap();
1596
1597		let state = producer.state.read();
1598		assert_eq!(state.offset, 0);
1599		assert_eq!(state.frames.len(), 100_000);
1600	}
1601
1602	#[test]
1603	fn clone_consumer_independent() {
1604		let mut producer = Info { sequence: 0 }.produce();
1605		producer.write_frame(Timestamp::ZERO, Bytes::from_static(b"a")).unwrap();
1606
1607		let mut c1 = producer.consume();
1608		// Read one frame from c1
1609		let _ = c1.next_frame().now_or_never().unwrap().unwrap().unwrap();
1610
1611		// Clone c1, inheriting its index (past first frame).
1612		let mut c2 = c1.clone();
1613
1614		producer.write_frame(Timestamp::ZERO, Bytes::from_static(b"b")).unwrap();
1615		producer.finish().unwrap();
1616
1617		// c2 should get the second frame (inherited index)
1618		let f = c2.next_frame().now_or_never().unwrap().unwrap().unwrap();
1619		assert_eq!(f.size, 1); // "b"
1620
1621		let end = c2.next_frame().now_or_never().unwrap().unwrap();
1622		assert!(end.is_none());
1623	}
1624
1625	/// Refilling a buffer several times drains every frame in order across the batch
1626	/// boundary (each refill starts exactly where the previous batch ended).
1627	#[test]
1628	fn read_frames_crosses_batches() {
1629		const CAP: usize = 8;
1630		let n = CAP * 3 + 5;
1631		let mut producer = Info { sequence: 0 }.produce();
1632		for i in 0..n {
1633			producer
1634				.write_frame(Timestamp::ZERO, Bytes::from(vec![i as u8; 4]))
1635				.unwrap();
1636		}
1637		producer.finish().unwrap();
1638
1639		let mut consumer = producer.consume();
1640		let mut buf = frame::Buffer::<CAP>::new();
1641		let mut seen = 0;
1642		loop {
1643			let batch = consumer.read_frames(&mut buf).now_or_never().unwrap().unwrap();
1644			if batch.is_empty() {
1645				break;
1646			}
1647			for frame in batch.iter() {
1648				assert_eq!(frame.payload, Bytes::from(vec![seen as u8; 4]));
1649				seen += 1;
1650			}
1651		}
1652		assert_eq!(seen, n);
1653		assert!(consumer.read_frame().now_or_never().unwrap().unwrap().is_none());
1654	}
1655
1656	/// A finished group is still aborted once its frames are released to free memory (the
1657	/// track's latency window, or the cache pool). A reader that already drained every frame
1658	/// is missing nothing, so it must see the clean end of group rather than the abort.
1659	#[test]
1660	fn abort_after_finish_keeps_the_clean_end_for_a_drained_reader() {
1661		let mut producer = Info { sequence: 0 }.produce();
1662		producer
1663			.write_frame(Timestamp::ZERO, Bytes::from_static(b"hello"))
1664			.unwrap();
1665		producer.finish().unwrap();
1666
1667		let mut drained = producer.consume();
1668		let mut behind = producer.consume();
1669		let frame = drained.read_frame().now_or_never().unwrap().unwrap().unwrap();
1670		assert_eq!(frame.payload, Bytes::from_static(b"hello"));
1671
1672		producer.abort(Error::Old).unwrap();
1673
1674		// Drained everything before the abort: nothing is missing.
1675		assert!(drained.read_frame().now_or_never().unwrap().unwrap().is_none());
1676		assert!(drained.next_frame().now_or_never().unwrap().unwrap().is_none());
1677
1678		// Never read the frame, and its bytes are gone: a truncated stream, not a clean end.
1679		assert!(matches!(behind.read_frame().now_or_never().unwrap(), Err(Error::Old)));
1680	}
1681
1682	/// `finished` answers for the cursor: a drained reader gets the clean end even after the
1683	/// abort that released the cache, and one that stopped short gets that abort. The
1684	/// producer's total stays available on `frame_count`.
1685	#[test]
1686	fn finished_answers_for_the_cursor() {
1687		let mut producer = Info { sequence: 0 }.produce();
1688		producer.write_frame(Timestamp::ZERO, Bytes::from_static(b"a")).unwrap();
1689		producer.write_frame(Timestamp::ZERO, Bytes::from_static(b"b")).unwrap();
1690		producer.finish().unwrap();
1691
1692		let mut drained = producer.consume();
1693		let mut behind = producer.consume();
1694		while drained.read_frame().now_or_never().unwrap().unwrap().is_some() {}
1695		behind.read_frame().now_or_never().unwrap().unwrap().unwrap();
1696
1697		producer.abort(Error::Old).unwrap();
1698
1699		assert_eq!(drained.finished().now_or_never().unwrap().unwrap(), 2);
1700		assert!(matches!(behind.finished().now_or_never().unwrap(), Err(Error::Old)));
1701		assert_eq!(behind.frame_count(), 2);
1702	}
1703
1704	/// A cursor whose next frame was evicted from the front of a live group can never reach
1705	/// the end, so `finished` reports the gap instead of parking forever.
1706	#[test]
1707	fn finished_reports_a_lagged_cursor() {
1708		let mut producer = Info { sequence: 0 }.produce();
1709		let mut consumer = producer.consume();
1710
1711		// Two frames at the cache budget, so the second write evicts the first.
1712		let big = Bytes::from(vec![0u8; MAX_CACHE_BYTES as usize]);
1713		producer.write_frame(Timestamp::ZERO, big.clone()).unwrap();
1714		producer.write_frame(Timestamp::ZERO, big).unwrap();
1715
1716		assert!(matches!(
1717			consumer.finished().now_or_never().unwrap(),
1718			Err(Error::Lagged)
1719		));
1720	}
1721
1722	/// `next_frame` picks up where a prior `read_frame` left off, preserving order.
1723	#[test]
1724	fn interleave_read_and_next_frame() {
1725		let mut producer = Info { sequence: 0 }.produce();
1726		for i in 0..5u8 {
1727			producer.write_frame(Timestamp::ZERO, Bytes::from(vec![i; 1])).unwrap();
1728		}
1729		producer.finish().unwrap();
1730
1731		let mut consumer = producer.consume();
1732		let f0 = consumer.read_frame().now_or_never().unwrap().unwrap().unwrap();
1733		assert_eq!(f0.payload, Bytes::from(vec![0u8; 1]));
1734
1735		// next_frame must continue from there, not skip ahead or repeat.
1736		for i in 1..5u8 {
1737			let mut f = consumer.next_frame().now_or_never().unwrap().unwrap().unwrap();
1738			let data = f.read_all().now_or_never().unwrap().unwrap();
1739			assert_eq!(data, Bytes::from(vec![i; 1]));
1740		}
1741		assert!(consumer.next_frame().now_or_never().unwrap().unwrap().is_none());
1742	}
1743
1744	/// A `read_frame` whose index sits past the buffered frames (cleared by an abort, or an
1745	/// eviction gap) must surface the error, not panic on an out-of-range `range(local..)`.
1746	#[test]
1747	fn read_frame_past_cleared_frames_does_not_panic() {
1748		let mut producer = Info { sequence: 0 }.produce();
1749		producer.write_frame(Timestamp::ZERO, Bytes::from_static(b"a")).unwrap();
1750		producer.write_frame(Timestamp::ZERO, Bytes::from_static(b"b")).unwrap();
1751
1752		let mut consumer = producer.consume();
1753		consumer.read_frame().now_or_never().unwrap().unwrap().unwrap();
1754		consumer.read_frame().now_or_never().unwrap().unwrap().unwrap();
1755
1756		// Abort clears the cached frames but leaves the consumer's index (2) past them, so the
1757		// refill's `local` (2) exceeds `frames.len()` (0).
1758		producer.abort(Error::Cancel).unwrap();
1759
1760		let result = consumer.read_frame().now_or_never().unwrap();
1761		assert!(matches!(result, Err(Error::Cancel)), "expected Cancel, got {result:?}");
1762	}
1763
1764	/// Dropping a filled buffer must drop its frames rather than leak them
1765	/// (exercises the `MaybeUninit` Drop path; run under miri to catch leaks/UB).
1766	#[test]
1767	fn drop_with_a_filled_buffer() {
1768		const CAP: usize = 8;
1769		let mut producer = Info { sequence: 0 }.produce();
1770		for _ in 0..CAP {
1771			producer.write_frame(Timestamp::ZERO, Bytes::from_static(b"x")).unwrap();
1772		}
1773		producer.finish().unwrap();
1774
1775		let mut consumer = producer.consume();
1776		let mut buf = frame::Buffer::<CAP>::new();
1777		// Fill the buffer, then drop it without taking anything out.
1778		assert_eq!(
1779			consumer.read_frames(&mut buf).now_or_never().unwrap().unwrap().len(),
1780			CAP
1781		);
1782		drop(buf);
1783	}
1784
1785	/// A parked chunk reader is woken by each chunk write. kio only notifies when
1786	/// a write guard was mutably accessed, so `frame_notify` must mark the guard
1787	/// modified; a guard dropped untouched wakes nobody and the reader would
1788	/// stall until the frame completed.
1789	#[tokio::test]
1790	async fn chunk_write_wakes_parked_reader() {
1791		let mut producer = Info { sequence: 0 }.produce();
1792		let mut consumer = producer.consume();
1793		let mut frame = producer
1794			.create_frame(frame::Info {
1795				size: 6,
1796				timestamp: Timestamp::ZERO,
1797			})
1798			.unwrap();
1799		let mut f = consumer.next_frame().await.unwrap().unwrap();
1800		let handle = tokio::spawn(async move { f.read_chunk().await });
1801		// Let the reader park on the empty partial before the chunk lands.
1802		tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1803		frame.write(Bytes::from_static(b"foo")).unwrap();
1804		let chunk = tokio::time::timeout(std::time::Duration::from_secs(2), handle)
1805			.await
1806			.expect("parked chunk reader was never woken by the chunk write")
1807			.unwrap()
1808			.unwrap();
1809		assert_eq!(chunk, Some(Bytes::from_static(b"foo")));
1810	}
1811
1812	/// A frame whose timestamp is at a different scale is converted to the group's
1813	/// scale by `create_frame`.
1814	#[test]
1815	fn create_frame_converts_mismatched_scale() {
1816		use crate::{Timescale, Timestamp};
1817
1818		let mut producer = Producer::new(
1819			Info { sequence: 0 },
1820			track::Info::default().with_timescale(Timescale::MICRO),
1821			Default::default(),
1822		);
1823		let frame = frame::Info {
1824			size: 3,
1825			timestamp: Timestamp::from_millis(1).unwrap(), // 1ms -> 1000µs
1826		};
1827		let writer = producer.create_frame(frame).unwrap();
1828		assert_eq!(writer.timestamp.scale(), Timescale::MICRO);
1829		assert_eq!(writer.timestamp.value(), 1000);
1830	}
1831
1832	/// An explicit current timestamp is converted to the group's scale.
1833	#[tokio::test]
1834	async fn create_frame_converts_current_timestamp() {
1835		use crate::Timescale;
1836
1837		let mut producer = Producer::new(
1838			Info { sequence: 0 },
1839			track::Info::default().with_timescale(Timescale::MICRO),
1840			Default::default(),
1841		);
1842		let writer = producer
1843			.create_frame(frame::Info {
1844				size: 3,
1845				timestamp: Timestamp::now(),
1846			})
1847			.unwrap();
1848		assert_eq!(writer.timestamp.scale(), Timescale::MICRO);
1849		assert!(!writer.timestamp.is_zero(), "local clock should be non-zero");
1850	}
1851
1852	/// The per-frame size cap (the group byte budget) is enforced before allocating.
1853	#[test]
1854	fn create_frame_rejects_oversized() {
1855		let mut producer = Info { sequence: 0 }.produce();
1856		let result = producer.create_frame(frame::Info {
1857			size: MAX_CACHE_BYTES + 1,
1858			timestamp: Timestamp::ZERO,
1859		});
1860		assert!(matches!(result, Err(Error::FrameTooLarge)));
1861	}
1862}