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	state: DriverState,
129	// Retains the waiter across `Future` polls so its kio registrations stay live.
130	// Kept out of `DriverState` so the borrow `hold` hands back doesn't collide with
131	// the `&mut` that polling the state needs.
132	park: kio::Park,
133}
134
135/// Everything the driver polls, split from the park so the two borrow disjointly.
136struct DriverState {
137	protocol: MaybeSendBox<'static, Result<(), Error>>,
138	// Bandwidth sampling, polled alongside the protocol. Its completion never ends
139	// the driver: the protocol owns the teardown. `None` once finished (or when the
140	// transport reports no send-rate estimate), since a completed future must not be
141	// polled again.
142	maintenance: Option<MaybeSendBox<'static, ()>>,
143	// Cached so a poll after completion doesn't re-poll a finished future.
144	result: Option<Result<(), Error>>,
145}
146
147impl Driver {
148	/// Drive the protocol one step, registering `waiter` for the next wakeup.
149	///
150	/// The `poll_*` counterpart of `.await`ing the driver, for callers composing it
151	/// into their own [`kio`]-style poll functions.
152	pub fn poll(&mut self, waiter: &kio::Waiter) -> Poll<Result<(), Error>> {
153		self.state.poll(waiter)
154	}
155}
156
157impl DriverState {
158	fn poll(&mut self, waiter: &kio::Waiter) -> Poll<Result<(), Error>> {
159		if let Some(result) = &self.result {
160			return Poll::Ready(result.clone());
161		}
162
163		if let Some(maintenance) = &mut self.maintenance
164			&& waiter.poll_future(maintenance.as_mut()).is_ready()
165		{
166			self.maintenance = None;
167		}
168
169		let result = std::task::ready!(waiter.poll_future(self.protocol.as_mut()));
170		self.result = Some(result.clone());
171		// The session is over; release the maintenance future now rather than on
172		// Drop, since it holds a transport clone.
173		self.maintenance = None;
174		Poll::Ready(result)
175	}
176}
177
178impl Future for Driver {
179	type Output = Result<(), Error>;
180
181	fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
182		let this = &mut *self;
183		// Disjoint field borrows: `hold` borrows the park for as long as the waiter
184		// lives, while the state is polled through its own `&mut`.
185		let waiter = this.park.hold(cx);
186		this.state.poll(waiter)
187	}
188}
189
190// Close-once state shared by every [`Session`] clone: the first close wins,
191// whether it comes from an [`Session::abort`], the protocol teardown, or the
192// last clone dropping.
193struct SessionShared {
194	inner: Box<dyn SessionInner>,
195	closed: std::sync::atomic::AtomicBool,
196}
197
198impl SessionShared {
199	fn close(&self, code: u32, reason: &str) {
200		if !self.closed.swap(true, std::sync::atomic::Ordering::SeqCst) {
201			self.inner.close(code, reason);
202		}
203	}
204}
205
206impl Drop for SessionShared {
207	fn drop(&mut self) {
208		self.close(Error::Cancel.to_code(), "dropped");
209	}
210}
211
212impl Session {
213	pub(super) fn new<S: web_transport_trait::Session>(
214		session: S,
215		version: Version,
216		recv_bandwidth: Option<bandwidth::Consumer>,
217		protocol: MaybeSendBox<'static, Result<(), Error>>,
218	) -> (Self, Driver) {
219		// Send bandwidth is version-agnostic: it depends on QUIC backend support.
220		let (send_bandwidth, maintenance) = if session.stats().estimated_send_rate().is_some() {
221			let producer = bandwidth::Producer::new();
222			let consumer = producer.consume();
223
224			let mut monitor = SendBandwidth::new(session.clone(), producer);
225			let maintenance = async move { kio::wait(|waiter| monitor.poll(waiter)).await }.maybe_boxed();
226
227			(Some(consumer), Some(maintenance))
228		} else {
229			(None, None)
230		};
231
232		let session = Self {
233			shared: Arc::new(SessionShared {
234				inner: Box::new(session),
235				closed: std::sync::atomic::AtomicBool::new(false),
236			}),
237			version,
238			send_bandwidth,
239			recv_bandwidth,
240		};
241		let driver = Driver {
242			state: DriverState {
243				protocol,
244				maintenance,
245				result: None,
246			},
247			park: kio::Park::default(),
248		};
249
250		(session, driver)
251	}
252}
253
254/// Samples the QUIC congestion controller's estimated send rate while anyone is
255/// consuming it, pausing when nobody is.
256///
257/// Finishes as soon as the transport or the producer channel closes, so it doesn't
258/// pin the underlying connection after the wrapping [`Session`] is dropped.
259struct SendBandwidth<S> {
260	session: S,
261	producer: bandwidth::Producer,
262	// The transport close, boxed once so it can be re-polled each step.
263	closed: MaybeSendBox<'static, ()>,
264	mode: SendBandwidthMode,
265}
266
267enum SendBandwidthMode {
268	/// No consumers; sampling is paused.
269	Idle,
270	/// At least one consumer; sample when the sleep elapses.
271	Polling { sleep: MaybeSendBox<'static, ()> },
272}
273
274impl<S: web_transport_trait::Session> SendBandwidth<S> {
275	const POLL_INTERVAL: Duration = Duration::from_millis(100);
276
277	fn new(session: S, producer: bandwidth::Producer) -> Self {
278		let closed = {
279			let session = session.clone();
280			async move {
281				session.closed().await;
282			}
283		}
284		.maybe_boxed();
285
286		Self {
287			session,
288			producer,
289			closed,
290			mode: SendBandwidthMode::Idle,
291		}
292	}
293
294	/// Sample the current estimate, arming the next sleep. Errors when the
295	/// producer channel is closed.
296	fn sample(&mut self) -> Result<(), Error> {
297		let bitrate = self.session.stats().estimated_send_rate();
298		self.producer.set(bitrate)?;
299		self.mode = SendBandwidthMode::Polling {
300			sleep: web_async::time::sleep(Self::POLL_INTERVAL).maybe_boxed(),
301		};
302		Ok(())
303	}
304
305	fn poll(&mut self, waiter: &kio::Waiter) -> Poll<()> {
306		if waiter.poll_future(self.closed.as_mut()).is_ready() {
307			return Poll::Ready(());
308		}
309
310		loop {
311			match &mut self.mode {
312				SendBandwidthMode::Idle => {
313					match self.producer.poll_used(waiter) {
314						// A consumer appeared: sample immediately, then on the interval.
315						Poll::Ready(Ok(())) => {}
316						Poll::Ready(Err(_)) => return Poll::Ready(()),
317						Poll::Pending => return Poll::Pending,
318					}
319					if self.sample().is_err() {
320						return Poll::Ready(());
321					}
322				}
323				SendBandwidthMode::Polling { sleep } => {
324					// Pause before sampling: checked first, like the old biased select.
325					match self.producer.poll_unused(waiter) {
326						Poll::Ready(Ok(())) => {
327							self.mode = SendBandwidthMode::Idle;
328							continue;
329						}
330						Poll::Ready(Err(_)) => return Poll::Ready(()),
331						Poll::Pending => {}
332					}
333
334					if waiter.poll_future(sleep.as_mut()).is_pending() {
335						return Poll::Pending;
336					}
337					if self.sample().is_err() {
338						return Poll::Ready(());
339					}
340					// Loop so the fresh sleep registers the waiter.
341				}
342			}
343		}
344	}
345}
346
347// We use a wrapper type that is dyn-compatible to remove the generic bounds from Session.
348// MaybeSend/MaybeSync keep this Send+Sync on native (where transports are) while
349// allowing the !Send browser WebTransport on wasm.
350trait SessionInner: web_transport_trait::MaybeSend + web_transport_trait::MaybeSync {
351	fn close(&self, code: u32, reason: &str);
352	fn closed(&self) -> MaybeSendBox<'_, String>;
353	fn stats(&self) -> ConnectionStats;
354}
355
356impl<S: web_transport_trait::Session> SessionInner for S {
357	fn close(&self, code: u32, reason: &str) {
358		S::close(self, code, reason);
359	}
360
361	fn closed(&self) -> MaybeSendBox<'_, String> {
362		Box::pin(async move { S::closed(self).await.to_string() })
363	}
364
365	fn stats(&self) -> ConnectionStats {
366		// estimated_recv_rate is filled in at the Session level (it comes from MoQ PROBE,
367		// not the transport), so leave it at the Default `None` here.
368		let stats = S::stats(self);
369		ConnectionStats {
370			rtt: stats.rtt(),
371			estimated_send_rate: stats.estimated_send_rate(),
372			bytes_sent: stats.bytes_sent(),
373			bytes_received: stats.bytes_received(),
374			bytes_lost: stats.bytes_lost(),
375			packets_sent: stats.packets_sent(),
376			packets_received: stats.packets_received(),
377			packets_lost: stats.packets_lost(),
378			..Default::default()
379		}
380	}
381}