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}