Skip to main content

moq_net/model/
frame.rs

1//! Frames are the leaf of the model: a sized, timestamped payload within a group.
2//!
3//! A group is a single ordered stream, so at most one frame is ever in flight.
4//! Completed frames are plain data ([`Frame`]); the in-flight frame is written
5//! through [`Producer`], which borrows its parent [`group::Producer`] exclusively so
6//! the borrow checker enforces that only one frame is open at a time. A [`Consumer`]
7//! reads one frame, sharing the group's channel rather than a per-frame one.
8use std::ops::Range;
9use std::sync::Arc;
10use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
11use std::sync::{Mutex, OnceLock};
12use std::task::{Poll, ready};
13
14use arrayvec::ArrayVec;
15use bytes::Bytes;
16
17use crate::group::{self, GroupState};
18use crate::{Error, IntoBytes, Result, Timestamp, stats};
19
20/// A chunk of data with an upfront size and a presentation timestamp.
21///
22/// This is just the header; the payload is carried separately (as a completed
23/// [`Frame`] or streamed via [`Producer`] / [`Consumer`]).
24#[derive(Clone, Copy, Debug)]
25pub struct Info {
26	/// Total payload size in bytes. Declared up front so consumers can preallocate.
27	pub size: u64,
28	/// Presentation timestamp.
29	///
30	/// [`group::Producer::create_frame`] converts it into the parent track's
31	/// timescale, so the scale you build it with doesn't have to match the track.
32	/// For data without a presentation time, pass [`Timestamp::now`] explicitly.
33	pub timestamp: Timestamp,
34}
35
36/// A completed frame: a timestamp and its full, contiguous payload.
37///
38/// This is the stored form of every finished frame in a group. The payload is a
39/// single [`Bytes`], so a consumer gets it with one zero-copy slice.
40#[derive(Clone, Debug)]
41pub struct Frame {
42	/// Presentation timestamp, at the parent track's timescale.
43	pub timestamp: Timestamp,
44	/// The full frame payload.
45	pub payload: Bytes,
46}
47
48/// A reusable batch of frames, filled by [`group::Consumer::read_frames`] and drained
49/// by [`group::Producer::write_frames`].
50///
51/// A fixed-capacity inline buffer: `N` frames of stack storage, never a heap
52/// allocation and never a spill. Allocate one per task and reuse it for the life of
53/// the group rather than one per read.
54///
55/// `N` defaults to 8. Most reads are not big batches: at the live edge a frame arrives
56/// at a time, so the capacity past the first frame or two is idle stack, and a
57/// publisher holds one buffer per in-flight group. 8 costs 384 bytes and still reads
58/// ~5x faster than a frame at a time (`benches/group.rs`). Ask for a larger `N` when
59/// you know you are draining a backlog: 32 is ~8x, for 1.5 KB.
60///
61/// A default on a const parameter only applies in type position, so name the type to
62/// get it: `let buf: frame::Buffer = Buffer::new()`.
63///
64/// A fill stamps the group's cache access once for the whole batch, which bounds
65/// frames rather than elapsed time. A reader that may take longer than the track's
66/// `max_age` to work through one batch calls
67/// [`group::Consumer::keep_alive`] between frames, or the rest of the group is
68/// expired out from under it.
69#[derive(Debug, Default)]
70pub struct Buffer<const N: usize = 8>(ArrayVec<Frame, N>);
71
72impl<const N: usize> Buffer<N> {
73	/// An empty buffer with room for `N` frames.
74	pub fn new() -> Self {
75		Self(ArrayVec::new())
76	}
77
78	/// How many frames a single fill can hold.
79	pub const fn capacity(&self) -> usize {
80		N
81	}
82
83	/// How many frames the buffer currently holds.
84	pub fn len(&self) -> usize {
85		self.0.len()
86	}
87
88	/// Whether the buffer holds no frames.
89	pub fn is_empty(&self) -> bool {
90		self.0.is_empty()
91	}
92
93	/// Whether the buffer is at capacity, so [`Self::push`] would refuse.
94	pub fn is_full(&self) -> bool {
95		self.0.is_full()
96	}
97
98	/// The frames from the most recent fill, in order.
99	pub fn filled(&self) -> &[Frame] {
100		&self.0
101	}
102
103	/// The frames from the most recent fill, mutably (to take payloads out, say).
104	pub fn filled_mut(&mut self) -> &mut [Frame] {
105		&mut self.0
106	}
107
108	/// Append a frame, handing it back if the buffer is already full.
109	///
110	/// Fill a buffer this way to hand a whole batch to
111	/// [`group::Producer::write_frames`].
112	pub fn push(&mut self, frame: Frame) -> std::result::Result<(), Frame> {
113		self.0.try_push(frame).map_err(|err| err.element())
114	}
115
116	/// Move every frame out, leaving the buffer empty.
117	///
118	/// Frames left in the iterator when it drops are dropped with it, so the buffer
119	/// ends up empty either way.
120	pub fn drain(&mut self) -> impl ExactSizeIterator<Item = Frame> + '_ {
121		self.0.drain(..)
122	}
123
124	/// Drop the current batch, leaving the buffer empty.
125	pub fn clear(&mut self) {
126		self.0.clear();
127	}
128}
129
130/// Bytes one session may allocate up front for the frames it is still receiving.
131///
132/// A peer declares each frame's size before sending it, and allocating that size up
133/// front saves a reallocation and copy per doubling as the payload arrives. The
134/// declaration costs the peer nothing, though, so without a bound it could commit up to
135/// [`group::MAX_CACHE_BYTES`] per stream without sending a byte. A frame whose declared
136/// size fits what remains is allocated up front; past that, its buffer grows with the
137/// bytes received. Each frame returns its share once it completes or aborts.
138#[derive(Clone)]
139pub(crate) struct Budget(Arc<AtomicUsize>);
140
141impl Budget {
142	/// Enough for a handful of large keyframes in flight at once.
143	const DEFAULT: usize = 16 * 1024 * 1024;
144
145	pub(crate) fn new(bytes: usize) -> Self {
146		Self(Arc::new(AtomicUsize::new(bytes)))
147	}
148
149	/// Take `size` bytes of the budget, or `None` if that is more than remains.
150	pub(crate) fn reserve(&self, size: usize) -> Option<Reservation> {
151		self.0
152			.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |left| left.checked_sub(size))
153			.ok()?;
154		Some(Reservation {
155			budget: self.clone(),
156			size,
157		})
158	}
159}
160
161impl Default for Budget {
162	fn default() -> Self {
163		Self::new(Self::DEFAULT)
164	}
165}
166
167/// A frame's share of a [`Budget`], returned on drop.
168pub(crate) struct Reservation {
169	budget: Budget,
170	size: usize,
171}
172
173impl Drop for Reservation {
174	fn drop(&mut self) {
175		self.budget.0.fetch_add(self.size, Ordering::Relaxed);
176	}
177}
178
179/// Payload storage for the single in-flight frame, shared between the writing
180/// [`Producer`] and any streaming [`Consumer`]s.
181///
182/// A whole-frame [`Bytes`] write is stored directly. Chunked writes are copied into a
183/// heap [`Segment`], either sized to the declared frame up front or grown as bytes
184/// arrive (see [`Budget`]). The producer writes through the raw pointer (sole writer,
185/// guaranteed by the exclusive borrow of the parent group); `written` provides
186/// happens-before for cross-thread reads.
187#[derive(Clone)]
188pub(crate) struct FrameBuf(Arc<FrameBufInner>);
189
190struct FrameBufInner {
191	size: usize,
192	grow: bool,
193	written: AtomicUsize,
194	storage: OnceLock<FrameStorage>,
195}
196
197enum FrameStorage {
198	Shared(Bytes),
199	Fixed(Arc<Segment>),
200	// Replaced by a larger copy whenever a write outgrows it. A reader clones the current
201	// segment under the lock, so a slice it already handed out keeps the old one alive.
202	Growing(Mutex<Arc<Segment>>),
203}
204
205/// An owned, zero-initialized heap allocation that chunked writes are copied into.
206struct Segment {
207	data: *mut u8,
208	capacity: usize,
209}
210
211// Safety: `data` is owned (Box-allocated, freed in Drop). The producer is the sole
212// writer and consumers only read bytes `< written`.
213unsafe impl Send for Segment {}
214unsafe impl Sync for Segment {}
215
216impl Drop for Segment {
217	fn drop(&mut self) {
218		// Safety: data was obtained from `Box::into_raw` of a `Box<[u8]>` of length
219		// `capacity` and is not aliased at drop (Arc refcount hit 0).
220		unsafe {
221			let slice = std::ptr::slice_from_raw_parts_mut(self.data, self.capacity);
222			drop(Box::from_raw(slice));
223		}
224	}
225}
226
227impl Segment {
228	fn new(capacity: usize) -> Self {
229		let boxed: Box<[u8]> = vec![0u8; capacity].into_boxed_slice();
230		let capacity = boxed.len();
231		let data = Box::into_raw(boxed) as *mut u8;
232		Self { data, capacity }
233	}
234
235	/// Copy `src` in at `offset`.
236	///
237	/// Safety: caller must be the sole writer, `offset + src.len()` must be within
238	/// `capacity`, and no reader may have been handed bytes at or past `offset`.
239	unsafe fn write(&self, offset: usize, src: &[u8]) {
240		debug_assert!(offset + src.len() <= self.capacity);
241		unsafe { std::ptr::copy_nonoverlapping(src.as_ptr(), self.data.add(offset), src.len()) };
242	}
243}
244
245/// The first `len` bytes of a [`Segment`], all written, so it can back a
246/// [`Bytes::from_owner`].
247struct Filled {
248	segment: Arc<Segment>,
249	len: usize,
250}
251
252impl AsRef<[u8]> for Filled {
253	fn as_ref(&self) -> &[u8] {
254		// Safety: `len` never exceeds what the producer had published when this was
255		// built, and the producer never rewrites published bytes. The Arc keeps the
256		// allocation alive while any reference to the slice lives.
257		unsafe { std::slice::from_raw_parts(self.segment.data, self.len) }
258	}
259}
260
261impl FrameBuf {
262	/// A buffer for a frame of `size` bytes, allocated at that size on the first write.
263	///
264	/// The oversized-frame guard lives in [`group::Producer`], which rejects a declared
265	/// size larger than the group's byte budget before calling this.
266	pub(crate) fn new(size: usize) -> Self {
267		Self::with_growth(size, false)
268	}
269
270	/// A buffer for a frame of `size` bytes that allocates only as bytes arrive.
271	pub(crate) fn growing(size: usize) -> Self {
272		Self::with_growth(size, true)
273	}
274
275	fn with_growth(size: usize, grow: bool) -> Self {
276		Self(Arc::new(FrameBufInner {
277			size,
278			grow,
279			written: AtomicUsize::new(0),
280			storage: OnceLock::new(),
281		}))
282	}
283
284	/// The frame's declared size.
285	pub(crate) fn size(&self) -> usize {
286		self.0.size
287	}
288
289	pub(crate) fn written(&self, ord: Ordering) -> usize {
290		self.0.written.load(ord)
291	}
292
293	fn try_set_bytes(&self, bytes: Bytes) -> std::result::Result<(), Bytes> {
294		if bytes.len() != self.size() || self.written(Ordering::Acquire) != 0 {
295			return Err(bytes);
296		}
297		self.0
298			.storage
299			.set(FrameStorage::Shared(bytes))
300			.map_err(|storage| match storage {
301				FrameStorage::Shared(bytes) => bytes,
302				_ => unreachable!("try_set_bytes only installs shared storage"),
303			})
304	}
305
306	/// Safety: caller must be the sole producer and `new_written` must be `<= size`.
307	unsafe fn store_written(&self, new_written: usize) {
308		// Release pairs with consumers' Acquire load to publish prior writes.
309		self.0.written.store(new_written, Ordering::Release);
310	}
311
312	/// Append `src` at the current write offset and publish it.
313	///
314	/// Safety relies on the single-producer invariant: only one [`Producer`] exists for
315	/// a frame (it holds the exclusive borrow of the parent group), so this is the sole
316	/// writer even though it takes `&self`.
317	fn append(&self, src: &[u8]) {
318		if src.is_empty() {
319			return;
320		}
321		let prev = self.written(Ordering::Relaxed);
322		let storage = self.0.storage.get_or_init(|| match self.0.grow {
323			true => FrameStorage::Growing(Mutex::new(Arc::new(Segment::new(0)))),
324			false => FrameStorage::Fixed(Arc::new(Segment::new(self.size()))),
325		});
326		// Safety (every write below): sole writer; the caller bounds-checked `src` against
327		// the remaining size, and consumers only read `[..written]`.
328		match storage {
329			// Only reachable if the frame is already complete via shared storage, which
330			// `Producer::write` rejects for a non-empty chunk. Nothing to copy.
331			FrameStorage::Shared(_) => return,
332			FrameStorage::Fixed(segment) => unsafe { segment.write(prev, src) },
333			FrameStorage::Growing(current) => {
334				let mut current = current.lock().expect("mutex poisoned");
335				let needed = prev + src.len();
336				if current.capacity < needed {
337					// Doubling keeps the copies linear in the frame size, and the cap makes
338					// the last segment exactly the frame, so it freezes without a copy.
339					let next = Segment::new(needed.max(current.capacity * 2).min(self.size()));
340					let filled = Filled {
341						segment: current.clone(),
342						len: prev,
343					};
344					unsafe { next.write(0, filled.as_ref()) };
345					*current = Arc::new(next);
346				}
347				unsafe { current.write(prev, src) };
348			}
349		}
350		// Safety: sole writer, and the caller bounds-checked `src` against the size.
351		unsafe { self.store_written(prev + src.len()) };
352	}
353
354	/// Freeze the buffer into the completed payload.
355	///
356	/// Returns the shared [`Bytes`] directly for a whole-frame write (zero-copy), or
357	/// wraps the mutable allocation otherwise.
358	fn freeze(&self) -> Bytes {
359		self.slice(0, self.size())
360	}
361
362	/// A zero-copy slice of the written region `[start..end]`.
363	///
364	/// `end` must not exceed a `written` value already loaded with `Acquire`, which is
365	/// also what makes the segment read here at least as new as those bytes.
366	fn slice(&self, start: usize, end: usize) -> Bytes {
367		let segment = match self.0.storage.get() {
368			Some(FrameStorage::Shared(bytes)) => return bytes.slice(start..end),
369			Some(FrameStorage::Fixed(segment)) => segment.clone(),
370			Some(FrameStorage::Growing(current)) => current.lock().expect("mutex poisoned").clone(),
371			// Nothing written, so the range is empty.
372			None => return Bytes::new(),
373		};
374		debug_assert!(end <= segment.capacity);
375		Bytes::from_owner(Filled { segment, len: end }).slice(start..)
376	}
377
378	/// Heap bytes the buffer currently holds for the payload.
379	#[cfg(test)]
380	pub(crate) fn allocated(&self) -> usize {
381		match self.0.storage.get() {
382			Some(FrameStorage::Shared(bytes)) => bytes.len(),
383			Some(FrameStorage::Fixed(segment)) => segment.capacity,
384			Some(FrameStorage::Growing(current)) => current.lock().expect("mutex poisoned").capacity,
385			None => 0,
386		}
387	}
388}
389
390/// The writer behind [`Producer`] and [`ProducerOwned`], generic over how it
391/// reaches the parent group (an exclusive borrow, or an owned clone).
392struct Raw<G: std::borrow::BorrowMut<group::Producer>> {
393	group: G,
394	buf: FrameBuf,
395	info: Info,
396	// Set once the frame is committed (finished) or aborted, so Drop is a no-op.
397	done: bool,
398	// Ingress payload meter, inherited from the parent group. Counts each written
399	// chunk's bytes. Empty (no-op) for an untagged group.
400	stats: stats::Meter,
401}
402
403impl<G: std::borrow::BorrowMut<group::Producer>> Raw<G> {
404	fn remaining(&self) -> usize {
405		self.buf.size() - self.buf.written(Ordering::Acquire)
406	}
407
408	fn write<B: IntoBytes>(&mut self, chunk: B) -> Result<()> {
409		let len = chunk.as_ref().len();
410		if len > self.remaining() {
411			return Err(Error::WrongSize);
412		}
413		// Ingress payload: count the chunk's bytes as they're written.
414		self.stats.bytes(len as u64);
415		// Fast path: a single whole-frame write keeps the caller's allocation.
416		if len == self.buf.size() && self.buf.written(Ordering::Acquire) == 0 {
417			match self.buf.try_set_bytes(chunk.into_bytes()) {
418				Ok(()) => {
419					let size = self.buf.size();
420					// Safety: `try_set_bytes` checked the buffer exactly matches the declared
421					// size, so publishing all bytes is in bounds.
422					unsafe { self.buf.store_written(size) };
423				}
424				Err(chunk) => self.buf.append(&chunk),
425			}
426		} else {
427			self.buf.append(chunk.as_ref());
428		}
429		Ok(())
430	}
431
432	fn finish(&mut self) -> Result<()> {
433		if self.buf.written(Ordering::Acquire) != self.buf.size() {
434			return Err(Error::WrongSize);
435		}
436		let payload = self.buf.freeze();
437		self.group.borrow_mut().frame_commit(Frame {
438			timestamp: self.info.timestamp,
439			payload,
440		})?;
441		self.done = true;
442		Ok(())
443	}
444
445	fn abort(&mut self, err: Error) -> Result<()> {
446		self.group.borrow_mut().frame_abort(err);
447		self.done = true;
448		Ok(())
449	}
450}
451
452impl<G: std::borrow::BorrowMut<group::Producer>> Drop for Raw<G> {
453	fn drop(&mut self) {
454		if !self.done {
455			// An unfinished frame leaves the group stream broken; fail the group so
456			// consumers surface an error instead of hanging on the partial forever.
457			// A group already aborted (superseded, evicted, cancelled) carries its own
458			// reason, so cutting its in-flight frame short is expected.
459			let group = self.group.borrow_mut();
460			if !group.is_aborted() {
461				tracing::warn!(
462					group = group.info().sequence,
463					"frame::Producer dropped before writing all bytes"
464				);
465			}
466			group.frame_abort(Error::Dropped);
467		}
468	}
469}
470
471/// Writes the payload of the single in-flight frame in one or more chunks.
472///
473/// Borrows the parent [`group::Producer`] exclusively, so no other frame can be
474/// opened while this one is live. The total bytes written must exactly match
475/// [`Info::size`]; call [`Self::finish`] to commit the frame (or [`Self::abort`] to
476/// fail it). Dropping without either aborts the group, since an unfinished frame
477/// leaves the group's stream broken.
478///
479/// A single whole-frame [`write`](Self::write) keeps the caller's allocation
480/// (zero-copy); chunked writes copy into one buffer sized to the declared frame.
481pub struct Producer<'a>(Raw<&'a mut group::Producer>);
482
483impl std::ops::Deref for Producer<'_> {
484	type Target = Info;
485
486	fn deref(&self) -> &Self::Target {
487		&self.0.info
488	}
489}
490
491impl<'a> Producer<'a> {
492	pub(crate) fn new(group: &'a mut group::Producer, buf: FrameBuf, info: Info) -> Self {
493		Self(Raw {
494			group,
495			buf,
496			info,
497			done: false,
498			stats: stats::Meter::default(),
499		})
500	}
501
502	/// Attach the parent group's ingress meter, so written chunks bump `bytes`.
503	pub(crate) fn with_meter(mut self, meter: stats::Meter) -> Self {
504		self.0.stats = meter;
505		self
506	}
507
508	/// The parent group this frame belongs to.
509	pub fn group(&self) -> group::Info {
510		self.0.group.info()
511	}
512
513	/// Bytes still needed to complete the frame.
514	pub fn remaining(&self) -> usize {
515		self.0.remaining()
516	}
517
518	/// Write a chunk of data to the frame.
519	///
520	/// Returns [`Error::WrongSize`] if the chunk would exceed the remaining bytes.
521	pub fn write<B: IntoBytes>(&mut self, chunk: B) -> Result<()> {
522		self.0.write(chunk)?;
523		self.0.group.frame_notify();
524		Ok(())
525	}
526
527	/// Commit the frame, verifying that all bytes were written.
528	///
529	/// Returns [`Error::WrongSize`] if the bytes written don't match [`Info::size`].
530	pub fn finish(mut self) -> Result<()> {
531		self.0.finish()
532	}
533
534	/// Abort the frame (and its group) with the given error.
535	pub fn abort(mut self, err: Error) -> Result<()> {
536		self.0.abort(err)
537	}
538}
539
540/// The owned counterpart of [`Producer`], for the wire drivers that stream a
541/// frame across polls and cannot hold the group borrowed inside their state.
542///
543/// Crate-private on purpose: the exclusivity the public borrow enforces (one
544/// live frame per group) becomes the holder's promise here. Do not open another
545/// frame on the group until this one is finished or aborted.
546///
547/// Holds the frame's share of the session's [`Budget`], if it got one, until then.
548pub(crate) struct ProducerOwned {
549	raw: Raw<group::Producer>,
550	_reserved: Option<Reservation>,
551}
552
553impl std::ops::Deref for ProducerOwned {
554	type Target = Info;
555
556	fn deref(&self) -> &Self::Target {
557		&self.raw.info
558	}
559}
560
561impl ProducerOwned {
562	pub(crate) fn new(group: group::Producer, buf: FrameBuf, info: Info, reserved: Option<Reservation>) -> Self {
563		Self {
564			raw: Raw {
565				group,
566				buf,
567				info,
568				done: false,
569				stats: stats::Meter::default(),
570			},
571			_reserved: reserved,
572		}
573	}
574
575	/// Attach the parent group's ingress meter, so written chunks bump `bytes`.
576	pub(crate) fn with_meter(mut self, meter: stats::Meter) -> Self {
577		self.raw.stats = meter;
578		self
579	}
580
581	/// Bytes still needed to complete the frame.
582	pub fn remaining(&self) -> usize {
583		self.raw.remaining()
584	}
585
586	/// Write a chunk of payload *without* waking consumers; pair it with [`Self::notify`].
587	///
588	/// The wake is split out because the wire ingest drains every chunk the transport
589	/// has already buffered in one poll turn, and a consumer parked on the group cannot
590	/// run until that turn yields. Waking per chunk pays a group lock and a clock read
591	/// to publish bytes nobody can observe yet.
592	///
593	/// `coding::Reader::poll_read_frame` owns the pairing and is the only caller.
594	pub(crate) fn write<B: IntoBytes>(&mut self, chunk: B) -> Result<()> {
595		self.raw.write(chunk)
596	}
597
598	/// Publish what has been written so far, waking consumers parked on the group.
599	pub(crate) fn notify(&self) {
600		self.raw.group.frame_notify();
601	}
602
603	/// Commit the frame, verifying that all bytes were written.
604	pub fn finish(mut self) -> Result<()> {
605		self.raw.finish()
606	}
607
608	/// Abort the frame (and its group) with the given error.
609	pub fn abort(mut self, err: Error) -> Result<()> {
610		self.raw.abort(err)
611	}
612
613	/// Heap bytes the frame's buffer currently holds for the payload.
614	#[cfg(test)]
615	pub(crate) fn allocated(&self) -> usize {
616		self.raw.buf.allocated()
617	}
618}
619
620/// The source of a [`Consumer`]'s payload: a finished frame (whole) or the in-flight
621/// tail (streamed).
622#[derive(Clone)]
623pub(crate) enum Source {
624	Complete(Bytes),
625	Partial(FrameBuf),
626}
627
628/// Subscriber expiry state carried across the group-to-frame handoff.
629#[derive(Clone)]
630pub(crate) struct Expiry {
631	policy: Arc<dyn group::Expiry>,
632	stale_stats: stats::Meter,
633	stale_counted: Arc<AtomicBool>,
634	tail: Range<usize>,
635	count_payload: bool,
636}
637
638impl Expiry {
639	pub(crate) fn new(
640		policy: Arc<dyn group::Expiry>,
641		stale_stats: stats::Meter,
642		stale_counted: Arc<AtomicBool>,
643	) -> Self {
644		Self {
645			policy,
646			stale_stats,
647			stale_counted,
648			tail: 0..0,
649			count_payload: false,
650		}
651	}
652
653	pub(crate) fn for_frame(mut self, tail: Range<usize>, count_payload: bool) -> Self {
654		self.tail = tail;
655		self.count_payload = count_payload;
656		self
657	}
658}
659
660/// Reads one frame's payload, streaming as bytes arrive for the in-flight tail.
661///
662/// Owns a handle to the parent group's channel (not a per-frame one), so a group with
663/// many frames doesn't allocate a channel per frame. Cloning yields an independent
664/// reader of the same frame.
665#[derive(Clone)]
666pub struct Consumer {
667	// The group's channel, used to park while a partial frame fills.
668	state: kio::Consumer<GroupState>,
669	info: Info,
670	source: Source,
671	// Byte offset consumed so far.
672	read_idx: usize,
673	// Egress payload meter, so chunks bump `bytes` exactly once as they're read out.
674	// Empty (no-op) for an untagged group.
675	stats: stats::Meter,
676	// The parent subscription can expire after this frame handle is returned.
677	expiry: Option<Expiry>,
678	expired: bool,
679}
680
681impl std::ops::Deref for Consumer {
682	type Target = Info;
683
684	fn deref(&self) -> &Self::Target {
685		&self.info
686	}
687}
688
689impl Consumer {
690	pub(crate) fn new(state: kio::Consumer<GroupState>, info: Info, source: Source) -> Self {
691		Self {
692			state,
693			info,
694			source,
695			read_idx: 0,
696			stats: stats::Meter::default(),
697			expiry: None,
698			expired: false,
699		}
700	}
701
702	/// Attach an egress meter so read-out chunks bump `bytes`. Used only for frames
703	/// read directly from the group (whose bytes weren't counted at a batch fill).
704	pub(crate) fn with_meter(mut self, meter: stats::Meter) -> Self {
705		self.stats = meter;
706		self
707	}
708
709	pub(crate) fn with_expiry(mut self, expiry: Expiry) -> Self {
710		self.expiry = Some(expiry);
711		self
712	}
713
714	fn size(&self) -> usize {
715		match &self.source {
716			Source::Complete(bytes) => bytes.len(),
717			Source::Partial(_) => self.info.size as usize,
718		}
719	}
720
721	/// Evaluate the parent subscription's drift budget.
722	///
723	/// Only called once a read has nothing buffered to return: the budget bounds a
724	/// *stalled* payload, so bytes already in hand are always drained rather than
725	/// truncated. `self.expired` is sticky, so the answer is only ever computed once.
726	fn poll_expired(&mut self, waiter: &kio::Waiter) -> bool {
727		if self.expired || self.read_idx >= self.size() {
728			return self.expired;
729		}
730		let Some(expiry) = &self.expiry else {
731			return false;
732		};
733		if !expiry.policy.is_expired(waiter) {
734			return false;
735		}
736
737		self.expired = true;
738		if !expiry.stale_counted.swap(true, Ordering::Relaxed) {
739			let mut stale = self.state.read().content_range(expiry.tail.start, expiry.tail.end);
740			if expiry.count_payload {
741				stale.bytes += self.size().saturating_sub(self.read_idx) as u64;
742			}
743			expiry.stale_stats.stale(stale);
744		}
745		true
746	}
747
748	/// Poll for the next chunk of bytes since the last read.
749	///
750	/// Returns `None` once the frame is finished and all bytes have been consumed.
751	pub fn poll_read_chunk(&mut self, waiter: &kio::Waiter) -> Poll<Result<Option<Bytes>>> {
752		if self.expired {
753			return Poll::Ready(Err(Error::Old));
754		}
755		let buf = match &self.source {
756			Source::Complete(bytes) => {
757				if self.read_idx >= bytes.len() {
758					return Poll::Ready(Ok(None));
759				}
760				let out = bytes.slice(self.read_idx..);
761				self.read_idx = bytes.len();
762				self.stats.bytes(out.len() as u64);
763				return Poll::Ready(Ok(Some(out)));
764			}
765			Source::Partial(buf) => buf.clone(),
766		};
767
768		let size = self.info.size as usize;
769		loop {
770			let written = buf.written(Ordering::Acquire);
771			if written > self.read_idx {
772				let out = buf.slice(self.read_idx, written);
773				self.read_idx = written;
774				self.stats.bytes(out.len() as u64);
775				return Poll::Ready(Ok(Some(out)));
776			}
777			if written >= size {
778				return Poll::Ready(Ok(None));
779			}
780			// Nothing buffered and the frame isn't finished: this park is the stall the
781			// drift budget exists to bound, and the only place it can apply.
782			if self.poll_expired(waiter) {
783				return Poll::Ready(Err(Error::Old));
784			}
785			let read_idx = self.read_idx;
786			// Park on the group's channel; the producer notifies it on each write and
787			// on abort. Re-check the atomic on wake.
788			ready!(poll_state(&self.state, waiter, |state| {
789				if let Some(err) = &state.abort {
790					return Poll::Ready(Err(err.clone()));
791				}
792				let w = buf.written(Ordering::Acquire);
793				if w > read_idx || w >= size {
794					Poll::Ready(Ok(()))
795				} else {
796					Poll::Pending
797				}
798			})?);
799		}
800	}
801
802	/// Return the next chunk of bytes since the last read.
803	pub async fn read_chunk(&mut self) -> Result<Option<Bytes>> {
804		kio::wait(|waiter| self.poll_read_chunk(waiter)).await
805	}
806
807	/// Poll for all remaining bytes, resolving once the frame is finished.
808	pub fn poll_read_all(&mut self, waiter: &kio::Waiter) -> Poll<Result<Bytes>> {
809		if self.expired {
810			return Poll::Ready(Err(Error::Old));
811		}
812		let buf = match &self.source {
813			Source::Complete(bytes) => {
814				let out = bytes.slice(self.read_idx..);
815				self.read_idx = bytes.len();
816				self.stats.bytes(out.len() as u64);
817				return Poll::Ready(Ok(out));
818			}
819			Source::Partial(buf) => buf.clone(),
820		};
821
822		let size = self.info.size as usize;
823		let read_idx = self.read_idx;
824		// Waiting on the rest of the payload is a stall; see `poll_read_chunk`.
825		if buf.written(Ordering::Acquire) < size && self.poll_expired(waiter) {
826			return Poll::Ready(Err(Error::Old));
827		}
828		ready!(poll_state(&self.state, waiter, |state| {
829			if let Some(err) = &state.abort {
830				return Poll::Ready(Err(err.clone()));
831			}
832			if buf.written(Ordering::Acquire) >= size {
833				Poll::Ready(Ok(()))
834			} else {
835				Poll::Pending
836			}
837		})?);
838		let out = buf.slice(read_idx, size);
839		self.read_idx = size;
840		self.stats.bytes(out.len() as u64);
841		Poll::Ready(Ok(out))
842	}
843
844	/// Return all remaining bytes, blocking until the frame is finished.
845	pub async fn read_all(&mut self) -> Result<Bytes> {
846		kio::wait(|waiter| self.poll_read_all(waiter)).await
847	}
848}
849
850/// Poll the group channel, mapping a terminal close without an error to
851/// [`Error::Dropped`]. Mirrors [`group::Consumer`]'s internal helper.
852fn poll_state<F, R>(state: &kio::Consumer<GroupState>, waiter: &kio::Waiter, f: F) -> Poll<Result<R>>
853where
854	F: Fn(&kio::Ref<'_, GroupState>) -> Poll<Result<R>>,
855{
856	Poll::Ready(match ready!(state.poll(waiter, f)) {
857		Ok(res) => res,
858		Err(state) => Err(state.abort.clone().unwrap_or(Error::Dropped)),
859	})
860}
861
862#[cfg(test)]
863mod tests {
864	use super::*;
865
866	/// Each reallocation leaves the slices already handed out intact, and the last
867	/// segment is exactly the frame, so the payload freezes without another copy.
868	#[test]
869	fn growing_buffer_keeps_handed_out_slices() {
870		let buf = FrameBuf::growing(10);
871		buf.append(b"abc");
872		assert_eq!(buf.allocated(), 3);
873		let early = buf.slice(0, 3);
874
875		buf.append(b"defg");
876		assert_eq!(buf.allocated(), 7);
877		buf.append(b"hij");
878		assert_eq!(buf.allocated(), 10, "doubling stops at the declared size");
879
880		assert_eq!(early, &b"abc"[..]);
881		assert_eq!(buf.slice(3, 7), &b"defg"[..]);
882		assert_eq!(buf.freeze(), &b"abcdefghij"[..]);
883	}
884
885	/// A reader on another thread slicing while the writer reallocates only ever sees
886	/// the bytes that were written.
887	#[test]
888	fn growing_buffer_reads_across_threads() {
889		const SIZE: usize = 256 * 1024;
890		let buf = FrameBuf::growing(SIZE);
891		let reader = std::thread::spawn({
892			let buf = buf.clone();
893			move || {
894				let mut read = 0;
895				while read < SIZE {
896					let written = buf.written(Ordering::Acquire);
897					let chunk = buf.slice(read, written);
898					for (i, byte) in chunk.iter().enumerate() {
899						assert_eq!(*byte, ((read + i) % 251) as u8);
900					}
901					read = written;
902					std::thread::yield_now();
903				}
904			}
905		});
906
907		let payload: Vec<u8> = (0..SIZE).map(|i| (i % 251) as u8).collect();
908		for chunk in payload.chunks(1000) {
909			buf.append(chunk);
910		}
911		reader.join().unwrap();
912		assert_eq!(buf.freeze(), payload);
913	}
914
915	/// A reservation holds its bytes until it drops, and a request larger than what
916	/// remains takes nothing.
917	#[test]
918	fn budget_returns_reservations() {
919		let budget = Budget::new(100);
920		let a = budget.reserve(60).unwrap();
921		assert!(budget.reserve(41).is_none());
922		let b = budget.reserve(40).unwrap();
923		assert!(budget.reserve(1).is_none());
924
925		drop(a);
926		drop(b);
927		assert!(budget.reserve(100).is_some());
928	}
929}