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