Skip to main content

moq_net/
session.rs

1use std::{
2	future::Future,
3	pin::Pin,
4	sync::Arc,
5	task::{Context, Poll},
6	time::Duration,
7};
8
9use web_transport_trait::Stats;
10
11use crate::{
12	Error, Version, bandwidth,
13	util::{MaybeBoxedExt, MaybeSendBox},
14};
15
16/// A snapshot of connection statistics for a [`Session`].
17///
18/// Every field is optional: availability depends on the transport backend (native QUIC
19/// reports all of them, the browser WebTransport reports few or none) and on the
20/// connection state (e.g. `estimated_send_rate` is `None` until the congestion controller
21/// has a window). `None` means "not reported", not "zero".
22#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
23#[non_exhaustive]
24pub struct ConnectionStats {
25	/// Smoothed round-trip time estimate.
26	pub rtt: Option<Duration>,
27
28	/// Estimated send bandwidth from the congestion controller, in bits per second.
29	pub estimated_send_rate: Option<u64>,
30
31	/// Estimated receive bandwidth from MoQ PROBE, in bits per second.
32	///
33	/// `None` unless the negotiated version supports PROBE (moq-lite-03+).
34	pub estimated_recv_rate: Option<u64>,
35
36	/// Total bytes sent over the connection, including retransmissions and overhead.
37	pub bytes_sent: Option<u64>,
38
39	/// Total bytes received over the connection, including duplicates and overhead.
40	pub bytes_received: Option<u64>,
41
42	/// Total bytes lost (detected via retransmission or acknowledgement).
43	pub bytes_lost: Option<u64>,
44
45	/// Total datagrams sent.
46	pub packets_sent: Option<u64>,
47
48	/// Total datagrams received.
49	pub packets_received: Option<u64>,
50
51	/// Total datagrams detected as lost.
52	pub packets_lost: Option<u64>,
53}
54
55/// A MoQ transport session, wrapping a WebTransport connection.
56///
57/// Returned by [`crate::Client::connect`] and [`crate::Server::accept`], paired with
58/// the [`Driver`] that runs its protocol work. Nothing is spawned behind your back:
59/// the session makes no progress unless its driver is polled.
60///
61/// Like every handle in this library, the lifecycle is reference counted: clones
62/// share the connection, the transport closes when the last clone drops, and
63/// [`abort`](Self::abort) closes it explicitly with an error. The [`Driver`] holds
64/// no `Session` clone, so handing it to an executor never keeps the session alive.
65#[derive(Clone)]
66pub struct Session {
67	shared: Arc<SessionShared>,
68	version: Version,
69	send_bandwidth: Option<bandwidth::Consumer>,
70	recv_bandwidth: Option<bandwidth::Consumer>,
71}
72
73impl Session {
74	/// Returns the negotiated protocol version.
75	pub fn version(&self) -> Version {
76		self.version
77	}
78
79	/// Returns a consumer for the estimated send bitrate (from the congestion controller).
80	///
81	/// Returns `None` if the QUIC backend doesn't support bandwidth estimation.
82	pub fn send_bandwidth(&self) -> Option<bandwidth::Consumer> {
83		self.send_bandwidth.clone()
84	}
85
86	/// Returns a consumer for the estimated receive bitrate (from PROBE).
87	///
88	/// Returns `None` if the MoQ version doesn't support PROBE (requires moq-lite-03+).
89	pub fn recv_bandwidth(&self) -> Option<bandwidth::Consumer> {
90		self.recv_bandwidth.clone()
91	}
92
93	/// Returns a snapshot of the current connection statistics.
94	///
95	/// This is a cheap, non-blocking read of the underlying transport's counters; see
96	/// [`ConnectionStats`] for which metrics each backend reports.
97	pub fn stats(&self) -> ConnectionStats {
98		let mut stats = self.shared.inner.stats();
99		stats.estimated_recv_rate = self.recv_bandwidth.as_ref().and_then(bandwidth::Consumer::peek);
100		stats
101	}
102
103	/// Close the transport with an explicit error, instead of waiting for the last
104	/// clone to drop. Idempotent: the first close wins.
105	pub fn abort(&self, err: Error) {
106		self.shared.close(err.to_code(), err.to_string().as_ref());
107	}
108
109	/// Block until the transport session is closed, returning the reason.
110	pub async fn closed(&self) -> Error {
111		Error::Transport(self.shared.inner.closed().await)
112	}
113}
114
115/// The future driving a [`Session`]'s protocol state.
116///
117/// Poll it for the lifetime of the session, either by `.await`ing it (typically
118/// spawned on an executor) or by stepping [`poll`](Self::poll) from inside another
119/// [`kio`]-style poll function. It holds no [`Session`] clone, so it never keeps
120/// the session alive: once the last session clone drops (or [`Session::abort`]
121/// fires), the transport closes and the driver finishes on its own. Dropping the
122/// driver cancels the protocol work without closing the session. It resolves when
123/// the session ends, and keeps returning that same result if polled again.
124///
125/// On native, driving requires a tokio runtime with a time driver (timers go
126/// through `web_async::time`); see the crate-level Async docs.
127pub struct Driver {
128	protocol: MaybeSendBox<'static, Result<(), Error>>,
129	// Bandwidth sampling, polled alongside the protocol. Its completion never ends
130	// the driver: the protocol owns the teardown. `None` once finished (or when the
131	// transport reports no send-rate estimate), since a completed future must not be
132	// polled again.
133	maintenance: Option<MaybeSendBox<'static, ()>>,
134	// Cached so a poll after completion (e.g. after `wait_ready` consumed the
135	// result) doesn't re-poll a finished future.
136	result: Option<Result<(), Error>>,
137	// Retains the previous poll's waiter so its kio registrations stay live until
138	// the next poll replaces it (same dance as `kio::wait`).
139	waiter: Option<kio::Waiter>,
140}
141
142impl Driver {
143	/// Drive the protocol one step, registering `waiter` for the next wakeup.
144	///
145	/// The `poll_*` counterpart of `.await`ing the driver, for callers composing it
146	/// into their own [`kio`]-style poll functions.
147	pub fn poll(&mut self, waiter: &kio::Waiter) -> Poll<Result<(), Error>> {
148		if let Some(result) = &self.result {
149			return Poll::Ready(result.clone());
150		}
151
152		if let Some(maintenance) = &mut self.maintenance
153			&& waiter.poll_future(maintenance.as_mut()).is_ready()
154		{
155			self.maintenance = None;
156		}
157
158		let result = std::task::ready!(waiter.poll_future(self.protocol.as_mut()));
159		self.result = Some(result.clone());
160		// The session is over; release the maintenance future now rather than on
161		// Drop, since it holds a transport clone.
162		self.maintenance = None;
163		Poll::Ready(result)
164	}
165
166	/// Drive the session until `ready` resolves, so `connect` can block on the
167	/// initial announce set.
168	///
169	/// A session that dies first still resolves `ready`: the connecting producers
170	/// live inside the driver, so its completion drops them (waking `ready`) and
171	/// releases the barrier. The error isn't lost, it's cached for whoever drives
172	/// the session next.
173	pub(super) async fn wait_ready(&mut self, ready: impl Future<Output = ()>) {
174		let mut ready = std::pin::pin!(ready);
175		kio::wait(|waiter| {
176			if waiter.poll_future(ready.as_mut()).is_ready() {
177				return Poll::Ready(());
178			}
179			let _ = self.poll(waiter);
180			Poll::Pending
181		})
182		.await
183	}
184}
185
186impl Future for Driver {
187	type Output = Result<(), Error>;
188
189	fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
190		let this = &mut *self;
191		// Replacing drops the previous waiter, keeping this one live until the next
192		// poll so any kio registrations it made survive (see `kio::wait`).
193		let waiter = kio::Waiter::new(cx.waker().clone());
194		let result = this.poll(&waiter);
195		this.waiter = Some(waiter);
196		result
197	}
198}
199
200// Close-once state shared by every [`Session`] clone: the first close wins,
201// whether it comes from an [`Session::abort`], the protocol teardown, or the
202// last clone dropping.
203struct SessionShared {
204	inner: Box<dyn SessionInner>,
205	closed: std::sync::atomic::AtomicBool,
206}
207
208impl SessionShared {
209	fn close(&self, code: u32, reason: &str) {
210		if !self.closed.swap(true, std::sync::atomic::Ordering::SeqCst) {
211			self.inner.close(code, reason);
212		}
213	}
214}
215
216impl Drop for SessionShared {
217	fn drop(&mut self) {
218		self.close(Error::Cancel.to_code(), "dropped");
219	}
220}
221
222impl Session {
223	pub(super) fn new<S: web_transport_trait::Session>(
224		session: S,
225		version: Version,
226		recv_bandwidth: Option<bandwidth::Consumer>,
227		protocol: MaybeSendBox<'static, Result<(), Error>>,
228	) -> (Self, Driver) {
229		// Send bandwidth is version-agnostic: it depends on QUIC backend support.
230		let (send_bandwidth, maintenance) = if session.stats().estimated_send_rate().is_some() {
231			let producer = bandwidth::Producer::new();
232			let consumer = producer.consume();
233
234			let mut monitor = SendBandwidth::new(session.clone(), producer);
235			let maintenance = async move { kio::wait(|waiter| monitor.poll(waiter)).await }.maybe_boxed();
236
237			(Some(consumer), Some(maintenance))
238		} else {
239			(None, None)
240		};
241
242		let session = Self {
243			shared: Arc::new(SessionShared {
244				inner: Box::new(session),
245				closed: std::sync::atomic::AtomicBool::new(false),
246			}),
247			version,
248			send_bandwidth,
249			recv_bandwidth,
250		};
251		let driver = Driver {
252			protocol,
253			maintenance,
254			result: None,
255			waiter: None,
256		};
257
258		(session, driver)
259	}
260}
261
262/// Samples the QUIC congestion controller's estimated send rate while anyone is
263/// consuming it, pausing when nobody is.
264///
265/// Finishes as soon as the transport or the producer channel closes, so it doesn't
266/// pin the underlying connection after the wrapping [`Session`] is dropped.
267struct SendBandwidth<S> {
268	session: S,
269	producer: bandwidth::Producer,
270	// The transport close, boxed once so it can be re-polled each step.
271	closed: MaybeSendBox<'static, ()>,
272	mode: SendBandwidthMode,
273}
274
275enum SendBandwidthMode {
276	/// No consumers; sampling is paused.
277	Idle,
278	/// At least one consumer; sample when the sleep elapses.
279	Polling { sleep: MaybeSendBox<'static, ()> },
280}
281
282impl<S: web_transport_trait::Session> SendBandwidth<S> {
283	const POLL_INTERVAL: Duration = Duration::from_millis(100);
284
285	fn new(session: S, producer: bandwidth::Producer) -> Self {
286		let closed = {
287			let session = session.clone();
288			async move {
289				session.closed().await;
290			}
291		}
292		.maybe_boxed();
293
294		Self {
295			session,
296			producer,
297			closed,
298			mode: SendBandwidthMode::Idle,
299		}
300	}
301
302	/// Sample the current estimate, arming the next sleep. Errors when the
303	/// producer channel is closed.
304	fn sample(&mut self) -> Result<(), Error> {
305		let bitrate = self.session.stats().estimated_send_rate();
306		self.producer.set(bitrate)?;
307		self.mode = SendBandwidthMode::Polling {
308			sleep: web_async::time::sleep(Self::POLL_INTERVAL).maybe_boxed(),
309		};
310		Ok(())
311	}
312
313	fn poll(&mut self, waiter: &kio::Waiter) -> Poll<()> {
314		if waiter.poll_future(self.closed.as_mut()).is_ready() {
315			return Poll::Ready(());
316		}
317
318		loop {
319			match &mut self.mode {
320				SendBandwidthMode::Idle => {
321					match self.producer.poll_used(waiter) {
322						// A consumer appeared: sample immediately, then on the interval.
323						Poll::Ready(Ok(())) => {}
324						Poll::Ready(Err(_)) => return Poll::Ready(()),
325						Poll::Pending => return Poll::Pending,
326					}
327					if self.sample().is_err() {
328						return Poll::Ready(());
329					}
330				}
331				SendBandwidthMode::Polling { sleep } => {
332					// Pause before sampling: checked first, like the old biased select.
333					match self.producer.poll_unused(waiter) {
334						Poll::Ready(Ok(())) => {
335							self.mode = SendBandwidthMode::Idle;
336							continue;
337						}
338						Poll::Ready(Err(_)) => return Poll::Ready(()),
339						Poll::Pending => {}
340					}
341
342					if waiter.poll_future(sleep.as_mut()).is_pending() {
343						return Poll::Pending;
344					}
345					if self.sample().is_err() {
346						return Poll::Ready(());
347					}
348					// Loop so the fresh sleep registers the waiter.
349				}
350			}
351		}
352	}
353}
354
355// We use a wrapper type that is dyn-compatible to remove the generic bounds from Session.
356// MaybeSend/MaybeSync keep this Send+Sync on native (where transports are) while
357// allowing the !Send browser WebTransport on wasm.
358trait SessionInner: web_transport_trait::MaybeSend + web_transport_trait::MaybeSync {
359	fn close(&self, code: u32, reason: &str);
360	fn closed(&self) -> MaybeSendBox<'_, String>;
361	fn stats(&self) -> ConnectionStats;
362}
363
364impl<S: web_transport_trait::Session> SessionInner for S {
365	fn close(&self, code: u32, reason: &str) {
366		S::close(self, code, reason);
367	}
368
369	fn closed(&self) -> MaybeSendBox<'_, String> {
370		Box::pin(async move { S::closed(self).await.to_string() })
371	}
372
373	fn stats(&self) -> ConnectionStats {
374		// estimated_recv_rate is filled in at the Session level (it comes from MoQ PROBE,
375		// not the transport), so leave it at the Default `None` here.
376		let stats = S::stats(self);
377		ConnectionStats {
378			rtt: stats.rtt(),
379			estimated_send_rate: stats.estimated_send_rate(),
380			bytes_sent: stats.bytes_sent(),
381			bytes_received: stats.bytes_received(),
382			bytes_lost: stats.bytes_lost(),
383			packets_sent: stats.packets_sent(),
384			packets_received: stats.packets_received(),
385			packets_lost: stats.packets_lost(),
386			..Default::default()
387		}
388	}
389}