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};