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 [`Config::new`]
9/// and chain the `with_*` setters.
10#[derive(Debug, Clone, Default)]
11#[non_exhaustive]
12pub struct Config {
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 Config {
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: Config,
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: Config) -> 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<Traffic> {
54		let name = traffic_track(tier, role, self.config.compression);
55		Ok(Traffic {
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<Sessions> {
63		let name = sessions_track(tier, self.config.compression);
64		Ok(Sessions {
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 mut config = moq_json::snapshot::consumer::Config::default();
72		if self.config.compression {
73			config.compression = moq_json::Compression::Deflate;
74		}
75		Ok(moq_json::snapshot::Consumer::new(track, config))
76	}
77}
78
79/// A typed reader over one traffic track. Yields the latest [`TrafficFrame`];
80/// intermediate frames a slow reader missed are collapsed, which is safe
81/// because the counters are cumulative.
82pub struct Traffic {
83	inner: moq_json::snapshot::Consumer<TrafficFrame>,
84}
85
86impl Traffic {
87	/// The next frame, or `None` once the track ends (the producer went away).
88	pub async fn next(&mut self) -> Result<Option<TrafficFrame>> {
89		Ok(self.inner.next().await?)
90	}
91}
92
93/// A typed reader over one sessions track; see [`Traffic`].
94pub struct Sessions {
95	inner: moq_json::snapshot::Consumer<SessionsFrame>,
96}
97
98impl Sessions {
99	/// The next frame, or `None` once the track ends (the producer went away).
100	pub async fn next(&mut self) -> Result<Option<SessionsFrame>> {
101		Ok(self.inner.next().await?)
102	}
103}
104
105#[cfg(test)]
106mod tests {
107	/// Build an origin producer, spawning its driver on the ambient runtime.
108	fn produce_origin() -> moq_net::origin::Producer {
109		let (producer, driver) = moq_net::origin::Producer::new(moq_net::origin::Config::default());
110		if tokio::runtime::Handle::try_current().is_ok() {
111			tokio::spawn(moq_net::time::run(driver));
112		} else {
113			// A sync test: nothing polls the driver, and dropping it would tear
114			// the origin down, so leak it and rely on the synchronous half.
115			std::mem::forget(driver);
116		}
117		producer
118	}
119
120	use std::time::Duration;
121
122	use moq_net::{Consume, PathOwned, Timestamp, announce, broadcast, origin, track};
123
124	use crate::{Producer, Tier, produce};
125
126	use super::*;
127
128	fn test_producer() -> (Producer, origin::Producer) {
129		let origin = produce_origin();
130		let producer = Producer::new(
131			produce::Config::new()
132				.with_origin(origin.clone())
133				.with_node(PathOwned::from("sjc")),
134		);
135		(producer, origin)
136	}
137
138	/// A tagged egress feed into a producer's registry, holding the handles needed
139	/// to write more traffic incrementally. Presence is recorded under `root`.
140	struct Feed {
141		track: track::Producer,
142		sub: track::Subscriber,
143		_announced: announce::Consumer,
144		_source: broadcast::Producer,
145		_ctx: moq_net::stats::Session,
146	}
147
148	impl Feed {
149		/// Write one frame of `bytes` bytes into the broadcast and read it out on the
150		/// egress side, so the publisher `bytes`/`frames`/`groups` counters advance.
151		async fn write(&mut self, bytes: usize) {
152			let mut group = self.track.append_group().unwrap();
153			group.write_frame(Timestamp::ZERO, vec![0u8; bytes]).unwrap();
154			group.finish().unwrap();
155			let mut group = self.sub.recv_group().await.unwrap().unwrap();
156			while group.read_frame().await.unwrap().is_some() {}
157		}
158	}
159
160	async fn feed(producer: &Producer, tier: Tier, root: &str, path: &str) -> Feed {
161		let ctx = producer.registry().tier(tier).session(root);
162		let feed_origin = produce_origin();
163		let egress = feed_origin.consume().with_stats(ctx.clone());
164
165		let mut announced = egress.announced();
166		let source = feed_origin.create_broadcast(path).unwrap();
167		source.announce(origin::Route::default()).unwrap();
168		let track = source.clone().create_track("video", None).unwrap();
169
170		let update = announced.next().await.expect("announce");
171		assert!(update.kind.is_active());
172		let consumer = egress.request_broadcast(path).await.expect("resolve");
173		let sub = consumer.track("video").unwrap().subscribe(None).await.unwrap();
174
175		Feed {
176			track,
177			sub,
178			_announced: announced,
179			_source: source,
180			_ctx: ctx,
181		}
182	}
183
184	async fn announced(origin: &origin::Producer) -> moq_net::broadcast::Consumer {
185		let mut consumer = origin.consume().with_hidden(true).announced();
186		tokio::time::advance(Duration::from_millis(1)).await;
187		let update = consumer.next().await.expect("expected announce");
188		assert!(update.kind.is_active());
189		origin
190			.consume()
191			.request_broadcast(moq_net::Path::new(update.prefix.as_str()))
192			.await
193			.expect("resolve")
194	}
195
196	async fn drive_tick() {
197		tokio::time::advance(Duration::from_millis(1100)).await;
198		for _ in 0..4 {
199			tokio::task::yield_now().await;
200		}
201	}
202
203	#[tokio::test(start_paused = true)]
204	async fn plain_and_compressed_round_trip() {
205		// The same drain must decode identically off the plain track and the
206		// compressed sibling, including across an update (the compressed
207		// track's delta path).
208		let (producer, origin) = test_producer();
209		let tier = Tier::default();
210		let mut fed = feed(&producer, tier.clone(), "acme", "foo/bar").await;
211		fed.write(42).await;
212
213		drive_tick().await;
214
215		let broadcast = announced(&origin).await;
216		let plain = Consumer::new(broadcast.consume(), Config::new());
217		let compressed = Consumer::new(broadcast.consume(), Config::new().with_compression(true));
218
219		let mut plain_traffic = plain.traffic(&tier, Role::Publisher).await.expect("subscribe plain");
220		let mut z_traffic = compressed
221			.traffic(&tier, Role::Publisher)
222			.await
223			.expect("subscribe compressed");
224
225		let plain_frame = plain_traffic.next().await.expect("read").expect("frame");
226		let z_frame = z_traffic.next().await.expect("read").expect("frame");
227		assert_eq!(plain_frame, z_frame, "both flavors carry the same data");
228		assert_eq!(plain_frame.get("foo/bar").expect("entry").bytes, 42);
229
230		// A later drain updates both flavors; the compressed one rides a delta.
231		fed.write(8).await;
232		drive_tick().await;
233		let plain_frame = plain_traffic.next().await.expect("read").expect("frame");
234		let z_frame = z_traffic.next().await.expect("read").expect("frame");
235		assert_eq!(plain_frame.get("foo/bar").expect("entry").bytes, 50);
236		assert_eq!(plain_frame, z_frame, "delta reconstructs the same frame");
237
238		let mut sessions = compressed.sessions(&tier).await.expect("subscribe sessions");
239		let frame = sessions.next().await.expect("read").expect("frame");
240		assert_eq!(frame.get("acme").expect("root").active(), 1);
241	}
242}