Skip to main content

moq_stats/
consume.rs

1//! The consuming half: typed readers over one published stats broadcast.
2
3use moq_net::broadcast;
4use moq_net::stats::{Role, Tier};
5
6use crate::{Result, SessionsFrame, TrafficFrame, sessions_track, traffic_track};
7
8/// Configuration for a [`Consumer`]. Construct with [`ConsumerConfig::new`]
9/// and chain the `with_*` setters.
10#[derive(Debug, Clone, Default)]
11#[non_exhaustive]
12pub struct ConsumerConfig {
13	/// Read the compressed `.json.z` tracks instead of the plain `.json` ones.
14	/// Same data for a fraction of the bytes, but requires a producer that
15	/// publishes them. Defaults to `false`.
16	pub compression: bool,
17}
18
19impl ConsumerConfig {
20	/// A config with default settings: the plain `.json` tracks.
21	pub fn new() -> Self {
22		Self::default()
23	}
24
25	/// Read the compressed `.json.z` tracks instead of the plain `.json` ones.
26	pub fn with_compression(mut self, compression: bool) -> Self {
27		self.compression = compression;
28		self
29	}
30}
31
32/// Reads one published stats broadcast (a `<prefix>/node/<node>` announce),
33/// yielding typed frames per track.
34///
35/// Subscribe to the traffic and session tracks you care about with
36/// [`Self::traffic`] / [`Self::sessions`]; a track that the producer never
37/// created (e.g. a named tier that saw no traffic) fails to subscribe or ends
38/// immediately, so callers typically subscribe the tiers they know exist.
39pub struct Consumer {
40	broadcast: broadcast::Consumer,
41	config: ConsumerConfig,
42}
43
44impl Consumer {
45	/// Wrap a stats broadcast. The broadcast is whatever the announce at a
46	/// stats path resolved to; parse the path with [`crate::parse_node_path`].
47	pub fn new(broadcast: broadcast::Consumer, config: ConsumerConfig) -> Self {
48		Self { broadcast, config }
49	}
50
51	/// Subscribe to the traffic track for `(tier, role)`, awaiting the
52	/// subscription handshake.
53	pub async fn traffic(&self, tier: &Tier, role: Role) -> Result<TrafficConsumer> {
54		let name = traffic_track(tier, role, self.config.compression);
55		Ok(TrafficConsumer {
56			inner: self.subscribe(&name).await?,
57		})
58	}
59
60	/// Subscribe to the sessions track for `tier`, awaiting the subscription
61	/// handshake.
62	pub async fn sessions(&self, tier: &Tier) -> Result<SessionsConsumer> {
63		let name = sessions_track(tier, self.config.compression);
64		Ok(SessionsConsumer {
65			inner: self.subscribe(&name).await?,
66		})
67	}
68
69	async fn subscribe<T: serde::de::DeserializeOwned>(&self, name: &str) -> Result<moq_json::snapshot::Consumer<T>> {
70		let track = self.broadcast.track(name)?.subscribe(None).await?;
71		let config = moq_json::snapshot::ConsumerConfig::default().with_compression(self.config.compression);
72		Ok(moq_json::snapshot::Consumer::new(track, config))
73	}
74}
75
76/// A typed reader over one traffic track. Yields the latest [`TrafficFrame`];
77/// intermediate frames a slow reader missed are collapsed, which is safe
78/// because the counters are cumulative.
79pub struct TrafficConsumer {
80	inner: moq_json::snapshot::Consumer<TrafficFrame>,
81}
82
83impl TrafficConsumer {
84	/// The next frame, or `None` once the track ends (the producer went away).
85	pub async fn next(&mut self) -> Result<Option<TrafficFrame>> {
86		Ok(self.inner.next().await?)
87	}
88}
89
90/// A typed reader over one sessions track; see [`TrafficConsumer`].
91pub struct SessionsConsumer {
92	inner: moq_json::snapshot::Consumer<SessionsFrame>,
93}
94
95impl SessionsConsumer {
96	/// The next frame, or `None` once the track ends (the producer went away).
97	pub async fn next(&mut self) -> Result<Option<SessionsFrame>> {
98		Ok(self.inner.next().await?)
99	}
100}
101
102#[cfg(test)]
103mod tests {
104	use std::time::Duration;
105
106	use moq_net::{Consume, Origin, PathOwned, Timestamp, announce, broadcast, origin, track};
107
108	use crate::{Producer, ProducerConfig, Tier};
109
110	use super::*;
111
112	fn test_producer() -> (Producer, origin::Producer) {
113		let origin = Origin::random().produce();
114		let producer = Producer::new(
115			ProducerConfig::new()
116				.with_origin(origin.clone())
117				.with_node(PathOwned::from("sjc")),
118		);
119		(producer, origin)
120	}
121
122	/// A tagged egress feed into a producer's registry, holding the handles needed
123	/// to write more traffic incrementally. Presence is recorded under `root`.
124	struct Feed {
125		track: track::Producer,
126		sub: track::Subscriber,
127		_announced: announce::Consumer,
128		_source: broadcast::Producer,
129		_ctx: moq_net::stats::Session,
130	}
131
132	impl Feed {
133		/// Write one frame of `bytes` bytes into the broadcast and read it out on the
134		/// egress side, so the publisher `bytes`/`frames`/`groups` counters advance.
135		async fn write(&mut self, bytes: usize) {
136			let mut group = self.track.append_group().unwrap();
137			group.write_frame(Timestamp::ZERO, vec![0u8; bytes]).unwrap();
138			group.finish().unwrap();
139			let mut group = self.sub.recv_group().await.unwrap().unwrap();
140			while group.read_frame().await.unwrap().is_some() {}
141		}
142	}
143
144	async fn feed(producer: &Producer, tier: Tier, root: &str, path: &str) -> Feed {
145		let ctx = producer.registry().tier(tier).session(root);
146		let feed_origin = Origin::random().produce();
147		let egress = feed_origin.consume().with_stats(ctx.clone());
148
149		let mut announced = egress.announced();
150		let mut source = feed_origin
151			.create_broadcast(path, broadcast::Route::announced())
152			.unwrap();
153		let track = source.create_track("video", None).unwrap();
154
155		tokio::time::sleep(Duration::from_millis(1)).await;
156		tokio::time::sleep(Duration::from_millis(1)).await;
157
158		let announce::Update { broadcast, .. } = announced.next().await.expect("announce");
159		let consumer = broadcast.expect("active");
160		let sub = consumer.track("video").unwrap().subscribe(None).await.unwrap();
161
162		Feed {
163			track,
164			sub,
165			_announced: announced,
166			_source: source,
167			_ctx: ctx,
168		}
169	}
170
171	async fn announced(origin: &origin::Producer) -> moq_net::broadcast::Consumer {
172		let mut consumer = origin.consume().announced();
173		tokio::time::advance(Duration::from_millis(1)).await;
174		let announce::Update { broadcast, .. } = consumer.next().await.expect("expected announce");
175		broadcast.expect("active")
176	}
177
178	async fn drive_tick() {
179		tokio::time::advance(Duration::from_millis(1100)).await;
180		for _ in 0..4 {
181			tokio::task::yield_now().await;
182		}
183	}
184
185	#[tokio::test(start_paused = true)]
186	async fn plain_and_compressed_round_trip() {
187		// The same drain must decode identically off the plain track and the
188		// compressed sibling, including across an update (the compressed
189		// track's delta path).
190		let (producer, origin) = test_producer();
191		let tier = Tier::default();
192		let mut fed = feed(&producer, tier.clone(), "acme", "foo/bar").await;
193		fed.write(42).await;
194
195		drive_tick().await;
196
197		let broadcast = announced(&origin).await;
198		let plain = Consumer::new(broadcast.consume(), ConsumerConfig::new());
199		let compressed = Consumer::new(broadcast.consume(), ConsumerConfig::new().with_compression(true));
200
201		let mut plain_traffic = plain.traffic(&tier, Role::Publisher).await.expect("subscribe plain");
202		let mut z_traffic = compressed
203			.traffic(&tier, Role::Publisher)
204			.await
205			.expect("subscribe compressed");
206
207		let plain_frame = plain_traffic.next().await.expect("read").expect("frame");
208		let z_frame = z_traffic.next().await.expect("read").expect("frame");
209		assert_eq!(plain_frame, z_frame, "both flavors carry the same data");
210		assert_eq!(plain_frame.get("foo/bar").expect("entry").bytes, 42);
211
212		// A later drain updates both flavors; the compressed one rides a delta.
213		fed.write(8).await;
214		drive_tick().await;
215		let plain_frame = plain_traffic.next().await.expect("read").expect("frame");
216		let z_frame = z_traffic.next().await.expect("read").expect("frame");
217		assert_eq!(plain_frame.get("foo/bar").expect("entry").bytes, 50);
218		assert_eq!(plain_frame, z_frame, "delta reconstructs the same frame");
219
220		let mut sessions = compressed.sessions(&tier).await.expect("subscribe sessions");
221		let frame = sessions.next().await.expect("read").expect("frame");
222		assert_eq!(frame.get("acme").expect("root").active(), 1);
223	}
224}