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 the readiness condition resolves, so `connect` can block on the
167	/// initial announce set.
168	///
169	/// A session that dies first still resolves readiness: the connecting producers
170	/// live inside the driver, so its completion drops them and releases the barrier.
171	/// The error isn't lost, it's cached for whoever drives the session next.
172	pub(super) async fn wait_ready(&mut self, poll_ready: impl Fn(&kio::Waiter) -> Poll<()>) {
173		kio::wait(|waiter| {
174			if poll_ready(waiter).is_ready() {
175				return Poll::Ready(());
176			}
177			let _ = self.poll(waiter);
178			Poll::Pending
179		})
180		.await
181	}
182}
183
184impl Future for Driver {
185	type Output = Result<(), Error>;
186
187	fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
188		let this = &mut *self;
189		// Replacing drops the previous waiter, keeping this one live until the next
190		// poll so any kio registrations it made survive (see `kio::wait`).
191		let waiter = kio::Waiter::new(cx.waker().clone());
192		let result = this.poll(&waiter);
193		this.waiter = Some(waiter);
194		result
195	}
196}
197
198// Close-once state shared by every [`Session`] clone: the first close wins,
199// whether it comes from an [`Session::abort`], the protocol teardown, or the
200// last clone dropping.
201struct SessionShared {
202	inner: Box<dyn SessionInner>,
203	closed: std::sync::atomic::AtomicBool,
204}
205
206impl SessionShared {
207	fn close(&self, code: u32, reason: &str) {
208		if !self.closed.swap(true, std::sync::atomic::Ordering::SeqCst) {
209			self.inner.close(code, reason);
210		}
211	}
212}
213
214impl Drop for SessionShared {
215	fn drop(&mut self) {
216		self.close(Error::Cancel.to_code(), "dropped");
217	}
218}
219
220impl Session {
221	pub(super) fn new<S: web_transport_trait::Session>(
222		session: S,
223		version: Version,
224		recv_bandwidth: Option<bandwidth::Consumer>,
225		protocol: MaybeSendBox<'static, Result<(), Error>>,
226	) -> (Self, Driver) {
227		// Send bandwidth is version-agnostic: it depends on QUIC backend support.
228		let (send_bandwidth, maintenance) = if session.stats().estimated_send_rate().is_some() {
229			let producer = bandwidth::Producer::new();
230			let consumer = producer.consume();
231
232			let mut monitor = SendBandwidth::new(session.clone(), producer);
233			let maintenance = async move { kio::wait(|waiter| monitor.poll(waiter)).await }.maybe_boxed();
234
235			(Some(consumer), Some(maintenance))
236		} else {
237			(None, None)
238		};
239
240		let session = Self {
241			shared: Arc::new(SessionShared {
242				inner: Box::new(session),
243				closed: std::sync::atomic::AtomicBool::new(false),
244			}),
245			version,
246			send_bandwidth,
247			recv_bandwidth,
248		};
249		let driver = Driver {
250			protocol,
251			maintenance,
252			result: None,
253			waiter: None,
254		};
255
256		(session, driver)
257	}
258}
259
260/// Samples the QUIC congestion controller's estimated send rate while anyone is
261/// consuming it, pausing when nobody is.
262///
263/// Finishes as soon as the transport or the producer channel closes, so it doesn't
264/// pin the underlying connection after the wrapping [`Session`] is dropped.
265struct SendBandwidth<S> {
266	session: S,
267	producer: bandwidth::Producer,
268	// The transport close, boxed once so it can be re-polled each step.
269	closed: MaybeSendBox<'static, ()>,
270	mode: SendBandwidthMode,
271}
272
273enum SendBandwidthMode {
274	/// No consumers; sampling is paused.
275	Idle,
276	/// At least one consumer; sample when the sleep elapses.
277	Polling { sleep: MaybeSendBox<'static, ()> },
278}
279
280impl<S: web_transport_trait::Session> SendBandwidth<S> {
281	const POLL_INTERVAL: Duration = Duration::from_millis(100);
282
283	fn new(session: S, producer: bandwidth::Producer) -> Self {
284		let closed = {
285			let session = session.clone();
286			async move {
287				session.closed().await;
288			}
289		}
290		.maybe_boxed();
291
292		Self {
293			session,
294			producer,
295			closed,
296			mode: SendBandwidthMode::Idle,
297		}
298	}
299
300	/// Sample the current estimate, arming the next sleep. Errors when the
301	/// producer channel is closed.
302	fn sample(&mut self) -> Result<(), Error> {
303		let bitrate = self.session.stats().estimated_send_rate();
304		self.producer.set(bitrate)?;
305		self.mode = SendBandwidthMode::Polling {
306			sleep: web_async::time::sleep(Self::POLL_INTERVAL).maybe_boxed(),
307		};
308		Ok(())
309	}
310
311	fn poll(&mut self, waiter: &kio::Waiter) -> Poll<()> {
312		if waiter.poll_future(self.closed.as_mut()).is_ready() {
313			return Poll::Ready(());
314		}
315
316		loop {
317			match &mut self.mode {
318				SendBandwidthMode::Idle => {
319					match self.producer.poll_used(waiter) {
320						// A consumer appeared: sample immediately, then on the interval.
321						Poll::Ready(Ok(())) => {}
322						Poll::Ready(Err(_)) => return Poll::Ready(()),
323						Poll::Pending => return Poll::Pending,
324					}
325					if self.sample().is_err() {
326						return Poll::Ready(());
327					}
328				}
329				SendBandwidthMode::Polling { sleep } => {
330					// Pause before sampling: checked first, like the old biased select.
331					match self.producer.poll_unused(waiter) {
332						Poll::Ready(Ok(())) => {
333							self.mode = SendBandwidthMode::Idle;
334							continue;
335						}
336						Poll::Ready(Err(_)) => return Poll::Ready(()),
337						Poll::Pending => {}
338					}
339
340					if waiter.poll_future(sleep.as_mut()).is_pending() {
341						return Poll::Pending;
342					}
343					if self.sample().is_err() {
344						return Poll::Ready(());
345					}
346					// Loop so the fresh sleep registers the waiter.
347				}
348			}
349		}
350	}
351}
352
353// We use a wrapper type that is dyn-compatible to remove the generic bounds from Session.
354// MaybeSend/MaybeSync keep this Send+Sync on native (where transports are) while
355// allowing the !Send browser WebTransport on wasm.
356trait SessionInner: web_transport_trait::MaybeSend + web_transport_trait::MaybeSync {
357	fn close(&self, code: u32, reason: &str);
358	fn closed(&self) -> MaybeSendBox<'_, String>;
359	fn stats(&self) -> ConnectionStats;
360}
361
362impl<S: web_transport_trait::Session> SessionInner for S {
363	fn close(&self, code: u32, reason: &str) {
364		S::close(self, code, reason);
365	}
366
367	fn closed(&self) -> MaybeSendBox<'_, String> {
368		Box::pin(async move { S::closed(self).await.to_string() })
369	}
370
371	fn stats(&self) -> ConnectionStats {
372		// estimated_recv_rate is filled in at the Session level (it comes from MoQ PROBE,
373		// not the transport), so leave it at the Default `None` here.
374		let stats = S::stats(self);
375		ConnectionStats {
376			rtt: stats.rtt(),
377			estimated_send_rate: stats.estimated_send_rate(),
378			bytes_sent: stats.bytes_sent(),
379			bytes_received: stats.bytes_received(),
380			bytes_lost: stats.bytes_lost(),
381			packets_sent: stats.packets_sent(),
382			packets_received: stats.packets_received(),
383			packets_lost: stats.packets_lost(),
384			..Default::default()
385		}
386	}
387}