Skip to main content

moq_net/
session.rs

1//! A MoQ session handle and a snapshot of its connection statistics.
2
3use std::{sync::Arc, task::Poll, time::Duration};
4
5use web_transport_trait::Stats as _;
6
7use crate::{Error, SessionError, Version, bandwidth, goaway};
8
9/// A close requested by a session handle, executed by the driver.
10#[derive(Clone)]
11struct Close {
12	code: u32,
13	reason: String,
14}
15
16/// The stats cell shared between the driver's sampler and the handles.
17struct StatsState {
18	/// The latest sample the driver took (or the construction-time snapshot).
19	sample: Stats,
20	/// A handle read the stats since the last sample: keep sampling.
21	demanded: bool,
22}
23
24/// A snapshot of connection statistics for a [`Session`].
25///
26/// Every field is optional: availability depends on the transport backend (native QUIC
27/// reports all of them, the browser WebTransport reports few or none) and on the
28/// connection state (e.g. `estimated_send_rate` is `None` until the congestion controller
29/// has a window). `None` means "not reported", not "zero".
30#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
31#[non_exhaustive]
32pub struct Stats {
33	/// Smoothed round-trip time estimate.
34	pub rtt: Option<Duration>,
35
36	/// Estimated send bandwidth from the congestion controller.
37	pub estimated_send_rate: Option<bandwidth::Rate>,
38
39	/// Estimated receive bandwidth from MoQ PROBE.
40	///
41	/// `None` unless the negotiated version supports PROBE (moq-lite-03+).
42	pub estimated_recv_rate: Option<bandwidth::Rate>,
43
44	/// Total bytes sent over the connection, including retransmissions and overhead.
45	pub bytes_sent: Option<u64>,
46
47	/// Total bytes received over the connection, including duplicates and overhead.
48	pub bytes_received: Option<u64>,
49
50	/// Total bytes lost (detected via retransmission or acknowledgement).
51	pub bytes_lost: Option<u64>,
52
53	/// Total datagrams sent.
54	pub packets_sent: Option<u64>,
55
56	/// Total datagrams received.
57	pub packets_received: Option<u64>,
58
59	/// Total datagrams detected as lost.
60	pub packets_lost: Option<u64>,
61}
62
63/// A MoQ transport session, wrapping a WebTransport connection.
64///
65/// Returned with a [`Driver`](crate::Driver) by [`crate::Client::connect`] and
66/// [`crate::Server::accept`]. The caller must poll or spawn that driver to run
67/// the session.
68///
69/// Like every handle in this library, the lifecycle is reference counted: clones
70/// share the connection, the transport closes when the last clone drops, and
71/// [`abort`](Self::abort) closes it explicitly with an error. The handle and the
72/// driver are severed in both directions: the driver holds no `Session` clone,
73/// so running it never keeps the session alive, and the `Session`
74/// holds no transport, so the handle is `Send + Sync` whatever transport the
75/// driver uses. Everything transport-shaped (the close, the close reason,
76/// the stats sample) is relayed through the driver.
77#[derive(Clone)]
78pub struct Session {
79	/// Handle side to driver: `Some` once [`abort`](Self::abort) ran; the
80	/// channel closing (the last handle dropping) is the implicit Cancel.
81	close: kio::Producer<Option<Close>>,
82	/// Driver to handle side: the transport's terminal error.
83	closed: kio::Consumer<Option<Error>>,
84	stats: kio::Shared<StatsState>,
85	version: Version,
86	send_bandwidth: Option<bandwidth::Consumer>,
87	recv_bandwidth: Option<bandwidth::Consumer>,
88	goaway: Arc<goaway::Handle>,
89}
90
91impl Session {
92	/// Returns the negotiated protocol version.
93	pub fn version(&self) -> Version {
94		self.version
95	}
96
97	/// Returns a consumer for the estimated send bitrate (from the congestion controller).
98	///
99	/// Returns `None` if the QUIC backend doesn't support bandwidth estimation.
100	pub fn send_bandwidth(&self) -> Option<bandwidth::Consumer> {
101		self.send_bandwidth.clone()
102	}
103
104	/// Returns a consumer for the estimated receive bitrate (from PROBE).
105	///
106	/// Returns `None` if the MoQ version doesn't support PROBE (requires moq-lite-03+).
107	pub fn recv_bandwidth(&self) -> Option<bandwidth::Consumer> {
108		self.recv_bandwidth.clone()
109	}
110
111	/// Returns a snapshot of the current connection statistics.
112	///
113	/// Cheap and non-blocking: this reads the latest sample the session's
114	/// driver took, and schedules a refresh, so periodic polling observes
115	/// fresh counters (100ms cadence). See [`Stats`] for which
116	/// metrics each backend reports.
117	pub fn stats(&self) -> Stats {
118		let mut stats = {
119			let mut state = self.stats.lock();
120			// A read is demand: wake the sampler, but only mutate (and so wake)
121			// when the flag actually flips.
122			if !state.demanded {
123				state.demanded = true;
124			}
125			state.sample
126		};
127		stats.estimated_recv_rate = self.recv_bandwidth.as_ref().and_then(bandwidth::Consumer::peek);
128		stats
129	}
130
131	/// Close the transport with an explicit error, instead of waiting for the last
132	/// clone to drop. Idempotent: the first close wins.
133	///
134	/// The close is executed by the session's driver, so it reaches the wire
135	/// once the runtime polls it (immediately on a live runtime).
136	pub fn abort(&self, err: Error) {
137		if let Ok(mut close) = self.close.write()
138			&& close.is_none()
139		{
140			*close = Some(Close {
141				code: SessionError::from(&err).to_code(),
142				reason: err.to_string(),
143			});
144		}
145	}
146
147	/// Block until the transport session is closed, returning the reason.
148	///
149	/// A close code the peer sent is decoded through the session registry (so an auth
150	/// rejection arrives as `Error::Session(SessionError::Unauthorized)`); every peer code is
151	/// preserved as [`Error::Session`], and a close carrying no application code surfaces as
152	/// [`Error::Transport`]. See [`Error::from_transport`]. If the runtime drops
153	/// the driver instead of running it to completion, this resolves with
154	/// [`Error::Cancel`].
155	pub async fn closed(&self) -> Error {
156		match self
157			.closed
158			.wait(|state| match &**state {
159				Some(err) => Poll::Ready(err.clone()),
160				None => Poll::Pending,
161			})
162			.await
163		{
164			Ok(err) => err,
165			// The driver was dropped before it could observe the close.
166			Err(kio::Closed) => Error::Cancel,
167		}
168	}
169
170	/// Drain the peer gracefully: the handle for sending this session's single
171	/// GOAWAY.
172	///
173	/// The graceful counterpart to [`abort`](Self::abort). Send the message with
174	/// [`goaway::Producer::send`], then await [`closed`](Self::closed) to observe
175	/// the peer leaving.
176	///
177	/// Only a [`Goaway`](goaway::Goaway) carrying a [`timeout`](goaway::Goaway::timeout)
178	/// schedules a close of our own, so without one this waits for a peer that may
179	/// never leave. Set a deadline when the drain has to finish.
180	///
181	/// Available on every version. A version with no GOAWAY message (moq-lite-03
182	/// and earlier) simply carries no explanation to the peer; the deadline is the
183	/// sender's own timer either way, so the session still closes on schedule and
184	/// the caller does not branch on the negotiated version.
185	pub fn drain(&self) -> goaway::Producer {
186		self.goaway.producer()
187	}
188
189	/// Observe a GOAWAY from the peer, telling us to migrate elsewhere.
190	///
191	/// [`peek`](goaway::Consumer::peek) is the cheap synchronous check;
192	/// [`recv`](goaway::Consumer::recv) waits for one. Once a GOAWAY arrives, new
193	/// subscribe and announce-interest requests on this session are refused (both
194	/// drafts forbid opening new streams afterward); existing subscriptions keep
195	/// flowing until the session closes.
196	pub fn draining(&self) -> goaway::Consumer {
197		self.goaway.consumer()
198	}
199}
200
201impl Session {
202	pub(super) fn new<S>(
203		runtime: crate::time::Clock,
204		session: S,
205		version: Version,
206		recv_bandwidth: Option<bandwidth::Consumer>,
207		protocol: crate::driver::Protocol<S>,
208		goaway: goaway::Handle,
209	) -> (Self, crate::Driver<S>)
210	where
211		S: crate::transport::poll::Session,
212	{
213		let sample = snapshot(&session);
214
215		// Send bandwidth is version-agnostic: it depends on QUIC backend support.
216		let (send_bandwidth, send_producer) = if sample.estimated_send_rate.is_some() {
217			let producer = bandwidth::Producer::new();
218			(Some(producer.consume()), Some(producer))
219		} else {
220			(None, None)
221		};
222
223		let close = kio::Producer::new(None);
224		let closed = kio::Producer::new(None);
225		let closed_consumer = closed.consume();
226		let stats = kio::Shared::new(StatsState {
227			sample,
228			demanded: false,
229		});
230
231		let supervisor = Supervisor {
232			runtime: runtime.clone(),
233			closed_watch: session.clone(),
234			session,
235			close: Some(close.consume()),
236			closed,
237			stats: stats.clone(),
238			send_bandwidth: send_producer,
239			mode: SamplerMode::Idle,
240		};
241
242		let session = Self {
243			close,
244			closed: closed_consumer,
245			stats,
246			version,
247			send_bandwidth,
248			recv_bandwidth,
249			goaway: Arc::new(goaway),
250		};
251		let driver = crate::Driver::new(
252			runtime.clone(),
253			crate::driver::State {
254				protocol,
255				supervisor: Some(supervisor),
256				result: None,
257			},
258		);
259
260		(session, driver)
261	}
262}
263
264/// The driver's transport-facing half of a [`Session`]: it executes the
265/// handles' close requests, publishes the transport's terminal error, and
266/// samples the connection stats (including the send-bandwidth estimate) while
267/// anyone is consuming them.
268///
269/// Finishes once the transport reports closed; everything else is moot then.
270pub(crate) struct Supervisor<S> {
271	runtime: crate::time::Clock,
272	session: S,
273	// A dedicated clone for the close watch, since each pending poll operation
274	// needs its own handle.
275	closed_watch: S,
276	/// Handle-side close requests; `None` once one was executed (only the
277	/// first close matters, and the channel closing is the last handle
278	/// dropping).
279	close: Option<kio::Consumer<Option<Close>>>,
280	/// Where the transport's terminal error is published for [`Session::closed`].
281	closed: kio::Producer<Option<Error>>,
282	stats: kio::Shared<StatsState>,
283	/// The send-rate estimate channel, when the backend reports one. `None`
284	/// also once every consumer is gone for good.
285	send_bandwidth: Option<bandwidth::Producer>,
286	mode: SamplerMode,
287}
288
289enum SamplerMode {
290	/// Nobody wants stats; sampling is paused.
291	Idle,
292	/// Someone does; sample when the deadline elapses.
293	Polling {
294		deadline: crate::runtime::Deadline<crate::time::Clock>,
295	},
296}
297
298impl<S: crate::transport::poll::Session> Supervisor<S> {
299	const POLL_INTERVAL: Duration = Duration::from_millis(100);
300
301	pub(crate) fn poll(&mut self, waiter: &kio::Waiter) -> Poll<()> {
302		let mut cx = std::task::Context::from_waker(waiter.waker());
303
304		// The transport's terminal error ends the supervisor.
305		if let Poll::Ready(err) = self.closed_watch.poll_closed(&mut cx) {
306			// Nothing samples once this returns, but `stats()` keeps serving
307			// this cell, so leave it holding the session's final counters
308			// rather than whichever sample the last demand happened to catch.
309			self.stats.lock().sample = snapshot(&self.session);
310			if let Ok(mut closed) = self.closed.write() {
311				*closed = Some(Error::from_transport(err));
312			}
313			return Poll::Ready(());
314		}
315
316		// Execute the first handle-side close request. The channel closing is
317		// the last handle dropping, with an abort written just before winning
318		// over the implicit cancel.
319		if let Some(close) = &self.close {
320			let request = match close.poll(waiter, |state| match &**state {
321				Some(request) => Poll::Ready(request.clone()),
322				None => Poll::Pending,
323			}) {
324				Poll::Ready(Ok(request)) => Some(request),
325				Poll::Ready(Err(last)) => Some(last.clone().unwrap_or_else(|| Close {
326					code: SessionError::Cancel.to_code(),
327					reason: "dropped".to_string(),
328				})),
329				Poll::Pending => None,
330			};
331			if let Some(request) = request {
332				self.session.close(request.code, &request.reason);
333				self.close = None;
334			}
335		}
336
337		self.poll_sampler(waiter);
338		Poll::Pending
339	}
340
341	/// Take one sample and arm the next deadline.
342	fn sample(&mut self) {
343		let sample = snapshot(&self.session);
344		if let Some(producer) = &self.send_bandwidth {
345			// An error means every consumer is gone for good; the stats cell
346			// still wants the sample.
347			if producer.set(sample.estimated_send_rate).is_err() {
348				self.send_bandwidth = None;
349			}
350		}
351		let mut stats = self.stats.lock();
352		stats.sample = sample;
353		stats.demanded = false;
354		drop(stats);
355		self.mode = SamplerMode::Polling {
356			deadline: crate::runtime::Deadline::after(&self.runtime, Self::POLL_INTERVAL),
357		};
358	}
359
360	fn poll_sampler(&mut self, waiter: &kio::Waiter) {
361		loop {
362			match &mut self.mode {
363				SamplerMode::Idle => {
364					// Demand is a bandwidth consumer appearing or a stats read.
365					let mut demanded = match &self.send_bandwidth {
366						Some(producer) => match producer.poll_used(waiter) {
367							Poll::Ready(Ok(())) => true,
368							Poll::Ready(Err(_)) => {
369								self.send_bandwidth = None;
370								false
371							}
372							Poll::Pending => false,
373						},
374						None => false,
375					};
376					demanded |= self
377						.stats
378						.poll(waiter, |state| match state.demanded {
379							true => Poll::Ready(()),
380							false => Poll::Pending,
381						})
382						.is_ready();
383					if !demanded {
384						return;
385					}
386					self.sample();
387				}
388				SamplerMode::Polling { deadline } => {
389					if deadline.poll(waiter).is_pending() {
390						return;
391					}
392					// The interval elapsed: pause unless someone still cares.
393					let used = self.send_bandwidth.as_ref().is_some_and(bandwidth::Producer::is_used);
394					if !used && !self.stats.read().demanded {
395						self.mode = SamplerMode::Idle;
396						continue;
397					}
398					self.sample();
399					// Loop so the fresh deadline registers the waiter.
400				}
401			}
402		}
403	}
404}
405
406/// A [`Stats`] snapshot of the transport's counters.
407///
408/// `estimated_recv_rate` is filled in at the [`Session`] level (it comes from
409/// MoQ PROBE, not the transport), so it stays `None` here.
410fn snapshot<S: crate::transport::poll::Session>(session: &S) -> Stats {
411	let stats = session.stats();
412	Stats {
413		rtt: stats.rtt(),
414		estimated_send_rate: stats.estimated_send_rate().map(bandwidth::Rate::from_bps),
415		bytes_sent: stats.bytes_sent(),
416		bytes_received: stats.bytes_received(),
417		bytes_lost: stats.bytes_lost(),
418		packets_sent: stats.packets_sent(),
419		packets_received: stats.packets_received(),
420		packets_lost: stats.packets_lost(),
421		..Default::default()
422	}
423}
424
425// The point of the sever: the handle's auto-traits no longer depend on which
426// transport the runtime drives, so every consumer (moq-ffi needs Send + Sync)
427// works over every transport, pinned `!Send` ones included.
428const _: () = {
429	const fn assert_send_sync<T: Send + Sync>() {}
430	assert_send_sync::<Session>();
431};