Skip to main content

moq_binary/stream/
producer.rs

1//! Publishing an ordered log of binary payloads over a track.
2
3use std::sync::{Arc, Mutex};
4
5use bytes::Bytes;
6
7use crate::Result;
8
9pub use super::Config;
10
11/// Publishes an ordered log of binary payloads over a track, one payload per frame in a single
12/// group.
13///
14/// Cheaply clonable: clones share one underlying track and publishing state, so multiple owners
15/// append into a single ordered log.
16#[derive(Clone)]
17pub struct Producer {
18	inner: Arc<Mutex<Inner>>,
19}
20
21impl Producer {
22	/// Create a producer that publishes to the given track.
23	pub fn new(track: moq_net::track::Producer, config: Config) -> Self {
24		Self {
25			inner: Arc::new(Mutex::new(Inner {
26				track,
27				group: None,
28				flate: config.compression.is_deflate().then(moq_flate::Encoder::new),
29			})),
30		}
31	}
32
33	/// Create a subscriber for the underlying track.
34	///
35	/// Still hands one back once a failed write has ended the log: the subscriber surfaces the abort
36	/// on its first read, which is what tells a late reader the log is truncated.
37	pub fn consume(&self) -> moq_net::track::Subscriber {
38		self.inner.lock().unwrap().track.subscribe(None)
39	}
40
41	/// Whether any consumer for the underlying track currently exists.
42	///
43	/// The demand signal for a producer serving on request: an unused track is cached state nobody is
44	/// watching, safe to drop and recreate on the next request.
45	pub fn is_used(&self) -> bool {
46		self.inner.lock().unwrap().track.is_used()
47	}
48
49	/// Append one payload to the log.
50	///
51	/// A payload that cannot be written ends the track: a log missing a record is not the lossless
52	/// log this mode promises, so the failure is surfaced rather than papered over with a second
53	/// group. The group is aborted rather than closed cleanly, so a consumer sees the failure
54	/// instead of a log that merely looks complete. Every later append fails on the closed track.
55	pub fn append(&mut self, payload: impl Into<Bytes>) -> Result<()> {
56		self.inner.lock().unwrap().append(payload.into())
57	}
58
59	/// Finish the track, closing the group.
60	pub fn finish(&mut self) -> Result<()> {
61		self.inner.lock().unwrap().finish()
62	}
63}
64
65/// Shared publishing state behind [`Producer`]'s `Arc<Mutex>`.
66struct Inner {
67	track: moq_net::track::Producer,
68
69	/// Opened on the first append and never rolled.
70	group: Option<moq_net::group::Producer>,
71
72	/// The DEFLATE encoder, one window for the whole group, `Some` while compressing.
73	flate: Option<moq_flate::Encoder>,
74}
75
76impl Inner {
77	fn append(&mut self, payload: Bytes) -> Result<()> {
78		// A payload no consumer could decode is as terminal as one the track rejects: the log is
79		// missing a record either way, and carrying on would present that gap as a complete log.
80		// Checked before the group is opened, so nothing is published, and routed through the same
81		// abort so a reader sees the failure rather than a clean end.
82		if self.flate.is_some() && payload.len() as u64 > moq_flate::DEFAULT_MAX_FRAME_SIZE {
83			self.abort(moq_net::Error::FrameTooLarge);
84			return Err(moq_flate::Error::TooLarge(moq_flate::DEFAULT_MAX_FRAME_SIZE).into());
85		}
86
87		// Open the group before compressing: a failure here must not leave the window ahead of a
88		// consumer that never received the frame.
89		if self.group.is_none() {
90			self.group = Some(self.track.append_group()?);
91		}
92
93		let payload = match self.flate.as_mut() {
94			Some(flate) => flate.frame(&payload),
95			None => payload,
96		};
97
98		let group = self.group.as_mut().expect("a group is open");
99		let Err(err) = group.write_frame(moq_net::Timestamp::now(), payload) else {
100			return Ok(());
101		};
102
103		// The payload never reached the wire, so the log has a hole in it, which is not the lossless
104		// log this mode promises. Continuing into a second group would hand consumers a gap dressed up
105		// as a complete log, so end the track and let the caller start a new one. This is also what
106		// keeps "a stream is one group" a real invariant rather than the usual case.
107		//
108		// Abort the track rather than finishing it: a clean close drains a consumer to `None`, which
109		// is exactly what a completed log looks like, so a truncated log would be indistinguishable
110		// from a whole one. Aborting the *track* is what a subscriber observes; aborting only the
111		// group drops it from the cache and the consumer still reads a clean end.
112		self.abort(err.clone());
113
114		Err(err.into())
115	}
116
117	/// End the track with an error, so a consumer sees the failure rather than a clean end.
118	fn abort(&mut self, err: moq_net::Error) {
119		// Abort the group with the same error first. `track::Producer::abort` deliberately leaves an
120		// already-pulled `group::Consumer` independent, so dropping our handle would hand a reader
121		// sitting in the group a generic `Dropped` instead of the failure that ended the log.
122		if let Some(group) = self.group.take() {
123			let _ = group.abort(err.clone());
124		}
125
126		// Abort through a clone, since aborting consumes a handle and the state is shared. Keeping
127		// ours means `consume` still hands back a subscriber, which is how a reader learns the log
128		// ended badly rather than cleanly.
129		let _ = self.track.clone().abort(err);
130	}
131
132	fn finish(&mut self) -> Result<()> {
133		// Finalize both independently rather than short-circuiting on the group. Returning early
134		// would leave the track open with `group` already taken, so a later append would open a
135		// second group, and (with compression) write into it from a window the consumer never
136		// received. That is exactly the split log ending the track exists to prevent.
137		let group = match self.group.take() {
138			Some(group) => group.finish(),
139			None => Ok(()),
140		};
141		let track = self.track.finish();
142
143		group?;
144		track?;
145		Ok(())
146	}
147}