From 0351d8ffc38ca34c5640e540b385d6e957a957ba Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 3 Nov 2026 14:47:23 -0800 Subject: [PATCH 01/17] chore(quest): claim quest/m1/quic-api/native-web.md Quest-Claim: 547c0cc827367560 From 37733c241f533956d8496539c4342e41727f35df Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 3 Nov 2026 15:23:17 -0800 Subject: [PATCH 02/17] refactor(quic)!: implement native WebTransport poll transports Co-Authored-By: GPT-6.1 Sol --- Cargo.lock | 4 +- bench/README.md | 23 + doc/concept/transport.md | 5 +- rs/moq-relay/Cargo.toml | 1 - rs/moq-relay/src/uring.rs | 15 +- rs/moq-relay/tests/integration/refusals.rs | 2 +- .../tests/integration/runtime_uring.rs | 2 +- rs/moq-tokio/Cargo.toml | 3 +- rs/moq-tokio/MIGRATE.md | 26 + rs/moq-tokio/README.md | 1 + rs/moq-tokio/benches/fanout.rs | 114 +- rs/moq-tokio/src/client.rs | 15 +- rs/moq-tokio/src/quic.rs | 5 +- rs/moq-tokio/src/quic/backend.rs | 19 +- rs/moq-tokio/src/quic/driver/recv.rs | 30 +- rs/moq-tokio/src/quic/driver/send.rs | 6 + rs/moq-tokio/src/quic/web/connect.rs | 8 + rs/moq-tokio/src/quic/web/error.rs | 177 +- rs/moq-tokio/src/quic/web/mod.rs | 6 +- rs/moq-tokio/src/quic/web/recv.rs | 200 ++- rs/moq-tokio/src/quic/web/send.rs | 293 ++- rs/moq-tokio/src/quic/web/server.rs | 61 +- rs/moq-tokio/src/quic/web/session.rs | 523 +++--- rs/moq-tokio/src/quic/web/settings.rs | 5 +- rs/moq-tokio/src/server.rs | 194 +- rs/moq-tokio/src/transport.rs | 1582 +---------------- rs/moq-tokio/src/transport/grouped.rs | 82 - rs/moq-tokio/src/transport/owned.rs | 326 ---- .../tests/integration/datagram_delivery.rs | 3 +- .../tests/integration/receive_credit.rs | 30 +- rs/moq-tokio/tests/integration/sim.rs | 4 +- rs/moq-uring/Cargo.toml | 8 +- rs/moq-uring/MIGRATE.md | 34 +- rs/moq-uring/README.md | 11 +- rs/moq-uring/benches/echo_quic.rs | 22 +- rs/moq-uring/benches/session_lite.rs | 10 +- rs/moq-uring/benches/web_headers.rs | 39 + rs/moq-uring/src/lib.rs | 2 +- rs/moq-uring/src/quic/client.rs | 3 +- rs/moq-uring/src/quic/driver/connection.rs | 33 +- rs/moq-uring/src/quic/driver/recv.rs | 2 +- rs/moq-uring/src/quic/driver/send.rs | 10 +- rs/moq-uring/src/quic/mod.rs | 43 +- rs/moq-uring/src/quic/web.rs | 1262 ++++++------- rs/moq-uring/src/quic/web/recv.rs | 295 +++ rs/moq-uring/src/quic/web/send.rs | 359 ++++ rs/moq-uring/src/transport.rs | 114 +- rs/moq-uring/src/transport/adapter.rs | 529 ------ rs/moq-uring/src/transport/local.rs | 4 +- rs/moq-uring/tests/integration/echo.rs | 22 +- rs/moq-uring/tests/integration/endpoint.rs | 10 +- rs/moq-uring/tests/integration/main.rs | 6 +- .../tests/integration/receive_credit.rs | 20 +- rs/moq-uring/tests/integration/session.rs | 20 +- rs/moq-uring/tests/integration/transport.rs | 3 - rs/moq-uring/tests/integration/web.rs | 186 +- rs/moq-uring/tests/integration/web_headers.rs | 125 ++ sh/gh/nightly.sh | 2 +- 58 files changed, 2664 insertions(+), 4275 deletions(-) delete mode 100644 rs/moq-tokio/src/transport/grouped.rs delete mode 100644 rs/moq-tokio/src/transport/owned.rs create mode 100644 rs/moq-uring/benches/web_headers.rs create mode 100644 rs/moq-uring/src/quic/web/recv.rs create mode 100644 rs/moq-uring/src/quic/web/send.rs delete mode 100644 rs/moq-uring/src/transport/adapter.rs delete mode 100644 rs/moq-uring/tests/integration/transport.rs create mode 100644 rs/moq-uring/tests/integration/web_headers.rs diff --git a/Cargo.lock b/Cargo.lock index 8f558297c9f..863d471fcde 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5092,7 +5092,6 @@ dependencies = [ "tracing-subscriber", "url", "usage-rs", - "web-transport-trait", "workspace-hack", "x509-parser", ] @@ -5342,7 +5341,6 @@ dependencies = [ "url", "usage-rs", "web-transport-proto", - "web-transport-trait", "webpki-roots 1.0.10", "workspace-hack", "x509-parser", @@ -5378,6 +5376,7 @@ dependencies = [ "anyhow", "bench-harness", "bytes", + "futures", "http", "io-uring", "kio", @@ -5403,7 +5402,6 @@ dependencies = [ "tracing", "url", "web-transport-proto", - "web-transport-trait", "workspace-hack", ] diff --git a/bench/README.md b/bench/README.md index fc8897240b9..c7e24326ca9 100644 --- a/bench/README.md +++ b/bench/README.md @@ -85,6 +85,29 @@ It reports offered and lost datagrams, bottleneck queue drops, QUIC DATAGRAM fra that delivered no datagram, reordered arrivals and append-to-receive latency per viewer, and fails if the loopback viewer loses any. It runs nightly. +Native transport allocation cost is measured through moq-net's real poll interface, +including QUIC opens, writes, receive leases and FINs, on long-lived loopback connections: + +```bash +nix develop --command cargo bench --locked -p moq-tokio --bench fanout -- native_transport +nix develop --command env NATIVE_ALLOCS=1 cargo bench --locked -p moq-tokio --bench fanout -- native_transport +``` + +The allocation mode warms each shape, then counts allocations and requested allocation/reallocation +bytes over 64 batches, per delivered stream. Setup and destruction are outside the counters; +all QUIC tasks run on the benchmark's current-thread runtime. Payload size and streams per batch +vary independently. Every delivered payload is compared byte for byte. The ordinary timing +series runs nightly with `fanout`. + +The io\_uring `web_headers` target varies stalled partial headers independently of ready uni/bi +streams. It then completes every fragmented header and checks its payload and FIN, so a busy +peer cannot hide a parser or stream-credit failure behind fast ready streams. It runs in the +nightly io\_uring leg: + +```bash +nix develop --command cargo bench --locked -p moq-uring --bench web_headers +``` + Measure 50 fps audio at per-packet, 100 and 200 ms grouping, plus 400 fps (2.5 ms packets) at per-packet and the 20 ms default. Frames are 200 bytes: diff --git a/doc/concept/transport.md b/doc/concept/transport.md index e4bc766e0b6..a6fcc7bef01 100644 --- a/doc/concept/transport.md +++ b/doc/concept/transport.md @@ -33,7 +33,10 @@ support it. The TypeScript client is pickier than the browsers: it uses WebTransport on Firefox only from 153 (earlier releases allow too few incoming streams) and keeps Safari on the WebSocket fallback, since WebKit bugs stall long sessions even though Safari 26.4 ships the API. Native -clients can use WebTransport too, but usually skip it. +clients can use WebTransport too, but usually skip it. Both Rust runtime +crates expose `quic::web` as a WebTransport library usable without MoQ; +see [moq-tokio](https://docs.rs/moq-tokio/latest/moq_tokio/quic/web/) and +[moq-uring](https://docs.rs/moq-uring/latest/moq_uring/quic/web/). ## WebSocket fallback diff --git a/rs/moq-relay/Cargo.toml b/rs/moq-relay/Cargo.toml index 7c465619f16..1002f93db5e 100644 --- a/rs/moq-relay/Cargo.toml +++ b/rs/moq-relay/Cargo.toml @@ -99,7 +99,6 @@ tower-service = "0.3" tracing = { workspace = true } url = { workspace = true, features = ["serde"] } usage-rs = { workspace = true, features = ["completions", "config"] } -web-transport-trait = { workspace = true } x509-parser = { workspace = true } [target.'cfg(target_os = "linux")'.dependencies] diff --git a/rs/moq-relay/src/uring.rs b/rs/moq-relay/src/uring.rs index 865e2b88f07..9db8522db37 100644 --- a/rs/moq-relay/src/uring.rs +++ b/rs/moq-relay/src/uring.rs @@ -709,7 +709,6 @@ impl Deadline { &mut self, handshake: impl Future>, ) -> anyhow::Result { - use web_transport_trait::poll::Session as _; let timer = std::pin::pin!(self.timer.wait()); let handshake = std::pin::pin!(handshake); match futures::future::select(timer, handshake).await { @@ -756,13 +755,13 @@ async fn serve_connection( .bound(moq_uring::quic::web::Request::accept(conn)) .await .context("WebTransport handshake failed")?; - let url = request.url().clone(); + let url = request.url.clone(); let protocol = request - .protocols() + .protocols .iter() .find(|offered| serve.alpns.iter().any(|alpn| alpn == *offered)) .cloned(); - let mut response = moq_uring::quic::web::Response::default(); + let mut response = moq_uring::quic::web::proto::ConnectResponse::OK; if let Some(protocol) = &protocol { response = response.with_protocol(protocol); } @@ -779,13 +778,7 @@ async fn serve_connection( deadline.close = Some(transport.clone()); let conn = transport.clone(); let server = serve.moq.clone(); - let address = match url { - Some(_) => moq_net::transport::Address::Uri, - None => moq_net::transport::Address::Setup, - }; - // moq-net boxes the transport with a native `Send` bound, which only a `Local` - // session meets; it still never leaves this worker. - let session = moq_uring::transport::Local::new(moq_uring::transport::Session::new(transport, address)); + let session = moq_uring::transport::Local::new(transport); let now = moq_net::time::Instant::now(); let mut accepted = Accepted { handle, diff --git a/rs/moq-relay/tests/integration/refusals.rs b/rs/moq-relay/tests/integration/refusals.rs index 83e66e392a6..17f22c28a03 100644 --- a/rs/moq-relay/tests/integration/refusals.rs +++ b/rs/moq-relay/tests/integration/refusals.rs @@ -505,7 +505,7 @@ async fn uri_setup_paths(io_uring: bool) { let mut setup = peer.open_uni(Default::default()).await.unwrap(); setup.write_all(&bytes[..]).await.unwrap(); if version.is_lite() { - setup.finish().unwrap(); + drop(setup.finish()); } if path.is_some() { let error = kio::clock::timeout(TIMEOUT, async { diff --git a/rs/moq-relay/tests/integration/runtime_uring.rs b/rs/moq-relay/tests/integration/runtime_uring.rs index baa79df6865..e531f086b66 100644 --- a/rs/moq-relay/tests/integration/runtime_uring.rs +++ b/rs/moq-relay/tests/integration/runtime_uring.rs @@ -1118,8 +1118,8 @@ async fn uring_workers_offer_the_configured_extensions() { #[cfg(feature = "quic")] #[test] fn uring_workers_bound_stalled_handshakes() { + use moq_net::transport::Error as _; use moq_tokio::quic::{driver, web}; - use web_transport_trait::Error as _; const LISTEN: Duration = Duration::from_millis(200); diff --git a/rs/moq-tokio/Cargo.toml b/rs/moq-tokio/Cargo.toml index 02e20355d08..811bf17ed00 100644 --- a/rs/moq-tokio/Cargo.toml +++ b/rs/moq-tokio/Cargo.toml @@ -16,7 +16,7 @@ categories = ["multimedia", "network-programming", "web-programming"] default = ["net", "quic", "aws-lc-rs", "websocket", "tcp", "uds"] # MoQ sessions over moq-net: `Client`, `Server`, `origin`, and the `connect`, `listen`, # and `transport` modules. Every transport feature turns it on, since none works without it. -net = ["dep:moq-net", "dep:web-transport-trait"] +net = ["dep:moq-net"] # The async QUIC driver alone (`quic::driver`): moq-quic on tokio, without moq-net or # the MoQ backend. Needs a crypto-provider feature. driver = ["dep:hmac", "dep:moq-congestion", "dep:moq-quic", "dep:rustc-hash", "dep:sha2"] @@ -108,7 +108,6 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] } url = { version = "2", features = ["serde"] } usage-rs = { workspace = true, features = ["config"] } web-transport-proto = { workspace = true, optional = true } -web-transport-trait = { workspace = true, optional = true } x509-parser = { workspace = true } [target.'cfg(target_os = "android")'.dependencies] diff --git a/rs/moq-tokio/MIGRATE.md b/rs/moq-tokio/MIGRATE.md index 3797c0906d1..7f0a11787ad 100644 --- a/rs/moq-tokio/MIGRATE.md +++ b/rs/moq-tokio/MIGRATE.md @@ -4,6 +4,32 @@ How to update your code for each breaking change, newest first. ## Unreleased +### Native WebTransport implements moq-net's poll traits ([#7771](https://github.com/moq-dev/moq/pull/7771)) + +`quic::web` is supported as a WebTransport library without running MoQ. +Pass its `Session` directly to moq-net. +Remove the generic `transport::Session::new(session, address)` wrapper; a WebTransport session reports `Address::Uri` and `Session::raw(connection)` reports `Address::Setup`. +The generic send/receive adapters, their error wrapper, and the sealed `Grouped`/`GroupedSend` traits are removed, along with every `web_transport_trait` implementation and dependency. +Custom transports implement `moq_net::transport::poll` directly. + +Use `quic::web::send::Stream` and `recv::Stream` instead of `SendStream` and `RecvStream`. +Their errors are `send::Error` and `recv::Error`. +`finish(self)` returns a `send::Finished`, which can be awaited for acknowledgements or a peer stop; `reset(self, code)` and `stop(self, code)` consume their stream. +Code using moq-net's poll traits keeps their mutable terminal methods. +Session polls take `&self` and a caller-owned `&kio::Waiter`; opens also take the driver's `send::Options`, preserving full `i64` priorities and real send groups. + +`Session` no longer dereferences to its QUIC connection. Use `session.connection()` for QUIC-specific operations; `session.send_datagram` always applies WebTransport framing. +`Session::stats` returns the driver's statistics snapshot. Use moq-net's `Session::metrics` for transport metrics. + +`Request` dereferences to `proto::ConnectRequest`; read its `url` and `protocols` fields there. +`Request::connection()` names the underlying QUIC connection. +`Request::respond` takes `impl Into` and requires status OK; `reject` takes an `http::StatusCode`. +The Tokio `web::Server` helper and `Request::connect()` accessor are removed; accept from the driver endpoint, then call `Request::accept(connection)`. +`send::Stream`, `send::Finished`, and `recv::Stream` expose their stream ID as `id()`. + +A receive stream's `closed()` observes an empty FIN without a read. When there is payload, read it before waiting for close. `poll_reset` still watches only failures, so it remains pending after a clean end. +Invalid stream error codes fail as `InvalidReset` or `InvalidStopped`, rather than looking like a clean end. + ### QUIC waits have caller-owned poll forms ([#7707](https://github.com/moq-dev/moq/pull/7707)) The QUIC driver connection's stream opens, accepts, datagram receive and close waits, diff --git a/rs/moq-tokio/README.md b/rs/moq-tokio/README.md index 32508473995..7b933d0b7ea 100644 --- a/rs/moq-tokio/README.md +++ b/rs/moq-tokio/README.md @@ -17,6 +17,7 @@ Establishes MoQ connections over a few different transports, selectable via carg - **WebSocket** as a fallback when QUIC isn't available Also handles TLS, certificate generation, logging setup, and reconnection logic, with Usage-derived configuration ready for binaries. +`quic::web` exposes WebTransport streams, datagrams, and send groups without requiring a MoQ session; its session also implements moq-net's poll traits directly. ## Examples diff --git a/rs/moq-tokio/benches/fanout.rs b/rs/moq-tokio/benches/fanout.rs index 8a6d091b3f2..bfb0f1add38 100644 --- a/rs/moq-tokio/benches/fanout.rs +++ b/rs/moq-tokio/benches/fanout.rs @@ -150,5 +150,117 @@ fn bench_fanout(c: &mut Criterion) { group.finish(); } -criterion_group!(benches, bench_fanout); +#[cfg(feature = "quic")] +mod native { + use super::*; + use bytes::Bytes; + use moq_net::transport::poll::{RecvStream as _, SendStream as _}; + use std::alloc::{GlobalAlloc, Layout, System}; + use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; + + struct Counter; + static COUNTING: AtomicBool = AtomicBool::new(false); + static ALLOCATIONS: AtomicU64 = AtomicU64::new(0); + static ALLOCATED: AtomicU64 = AtomicU64::new(0); + #[global_allocator] + static ALLOCATOR: Counter = Counter; + + fn count(size: usize) { + if COUNTING.load(Ordering::Relaxed) { + ALLOCATIONS.fetch_add(1, Ordering::Relaxed); + ALLOCATED.fetch_add(size as u64, Ordering::Relaxed); + } + } + unsafe impl GlobalAlloc for Counter { + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + count(layout.size()); + unsafe { System.alloc(layout) } + } + unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 { + count(layout.size()); + unsafe { System.alloc_zeroed(layout) } + } + unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, size: usize) -> *mut u8 { + count(size); + unsafe { System.realloc(ptr, layout, size) } + } + unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { + unsafe { System.dealloc(ptr, layout) } + } + } + + fn transfer( + pair: &Pair, + client: &S, + server: &S, + streams: usize, + message: &Bytes, + ) { + pair.runtime.block_on(async { + let send = async { + for _ in 0..streams { + let mut send = client.open_uni(Default::default()).await.expect("open"); + send.write_chunk(message.clone()).await.expect("write"); + send.finish().expect("finish"); + } + }; + let recv = async { + for _ in 0..streams { + let mut recv = server.accept_uni().await.expect("accept"); + let mut offset = 0; + while let Some(chunk) = recv.read_chunk(message.len()).await.expect("read") { + assert_eq!(chunk, message.slice(offset..offset + chunk.len())); + offset += chunk.len(); + } + assert_eq!(offset, message.len()); + } + }; + tokio::join!(send, recv); + }); + } + + pub fn benchmark(c: &mut Criterion) { + let pair = Pair::new(); + let client = moq_tokio::quic::web::Session::raw(pair.client.clone()); + let server = moq_tokio::quic::web::Session::raw(pair.server.clone()); + let allocations = std::env::var_os("NATIVE_ALLOCS").is_some(); + let mut group = c.benchmark_group("native_transport"); + for size in [64, 64 * 1024] { + let message = Bytes::from(vec![0x5a; size]); + for streams in [1, 16, 256] { + if allocations { + for _ in 0..16 { + transfer(&pair, &client, &server, streams, &message); + } + ALLOCATIONS.store(0, Ordering::Relaxed); + ALLOCATED.store(0, Ordering::Relaxed); + COUNTING.store(true, Ordering::Relaxed); + for _ in 0..64 { + transfer(&pair, &client, &server, streams, &message); + } + COUNTING.store(false, Ordering::Relaxed); + let count = (streams * 64) as f64; + println!( + "native_transport bytes={size} streams={streams} allocations_per_stream={} allocated_bytes_per_stream={}", + ALLOCATIONS.load(Ordering::Relaxed) as f64 / count, + ALLOCATED.load(Ordering::Relaxed) as f64 / count + ); + } else { + group.throughput(Throughput::Elements(streams as u64)); + group.bench_function(format!("bytes-{size}/streams-{streams}"), |b| { + b.iter(|| transfer(&pair, &client, &server, streams, &message)); + }); + } + } + } + group.finish(); + } +} + +#[cfg(not(feature = "quic"))] +mod native { + pub fn benchmark(_: &mut criterion::Criterion) {} +} + +criterion_group!(benches, bench_fanout, native::benchmark); criterion_main!(benches); diff --git a/rs/moq-tokio/src/client.rs b/rs/moq-tokio/src/client.rs index ff542619839..95cd2729587 100644 --- a/rs/moq-tokio/src/client.rs +++ b/rs/moq-tokio/src/client.rs @@ -463,14 +463,9 @@ impl Client { #[cfg(feature = "quic")] if let Some(quic) = self.quic.as_ref() { let quic_addr = addr.clone(); - let address = match quic_transport(&url) { - crate::Transport::Quic => moq_net::transport::Address::Setup, - _ => moq_net::transport::Address::Uri, - }; let quic_handle = async { quic.connect(&self.tls, quic_addr, &self.versions) .await - .map(|session| crate::transport::Session::new(session, address)) .map_err(Error::from) }; @@ -1064,10 +1059,7 @@ mod tests { // Hold QUIC until the fallback has been refused, so the 403 is always exercised. let quic = Box::pin(async move { forbid.await.unwrap().expect("fallback listener failed"); - quic.connect(&tls, quic_addr, &versions) - .await - .map(|session| crate::transport::Session::new(session, moq_net::transport::Address::Uri)) - .map_err(Error::from) + quic.connect(&tls, quic_addr, &versions).await.map_err(Error::from) }); let dialed = kio::clock::timeout( @@ -1159,10 +1151,7 @@ mod tests { let quic = dialer.quic.as_ref().expect("QUIC backend"); let session = quic.connect(&dialer.tls, quic_addr, &dialer.versions).await?; connected.send(()).expect("fallback listener alive"); - Ok::<_, crate::Error>(crate::transport::Session::new( - session, - moq_net::transport::Address::Uri, - )) + Ok::<_, crate::Error>(session) }); let cc = client .race_moq_connect(&client.moq, ws_addr, quic) diff --git a/rs/moq-tokio/src/quic.rs b/rs/moq-tokio/src/quic.rs index 834e1b08f1e..003a1d4a547 100644 --- a/rs/moq-tokio/src/quic.rs +++ b/rs/moq-tokio/src/quic.rs @@ -25,11 +25,8 @@ pub mod demux; #[cfg(feature = "driver")] pub mod driver; -/// WebTransport over [`driver`], formerly the `web-transport-moq` crate. -/// -/// Public only so the workspace's tests can dial a bare session; not a stable API. +/// WebTransport over the moq-quic [`driver`], usable without a MoQ session. #[cfg(feature = "quic")] -#[doc(hidden)] pub mod web; /// The routable server ID a QUIC-LB load balancer encodes into connection IDs. diff --git a/rs/moq-tokio/src/quic/backend.rs b/rs/moq-tokio/src/quic/backend.rs index 6a8b5e06b2a..318762aba95 100644 --- a/rs/moq-tokio/src/quic/backend.rs +++ b/rs/moq-tokio/src/quic/backend.rs @@ -799,7 +799,10 @@ pub(crate) async fn accept( .await .map_err(|err| Error::RecvRequest(crate::error::message(err)))?; let url = Some(request.url.clone()); - let identity = request.conn().peer_identity().map(crate::tls::PeerIdentity::from_chain); + let identity = request + .connection() + .peer_identity() + .map(crate::tls::PeerIdentity::from_chain); // The authority the client put in its CONNECT URL. let authority = request.url.host_str().filter(|h| !h.is_empty()).map(str::to_owned); @@ -1219,11 +1222,17 @@ mod tests { .await .expect("connect failed"); - // web::Session derefs to the driver connection. - assert!(session.congestion_state().probes(), "client controller cannot probe"); + // Inspect the driver through the explicit connection accessor. + assert!( + session.connection().congestion_state().probes(), + "client controller cannot probe" + ); // BBRv3 can probe, so moq-net gets a target to write. #[cfg(feature = "net")] - assert!(session.probe().is_some(), "BBRv3 connection has no probe target"); + assert!( + session.connection().probe().is_some(), + "BBRv3 connection has no probe target" + ); assert!( accepted.await.expect("server task panicked"), "server controller cannot probe" @@ -1308,7 +1317,7 @@ mod tests { .expect("connect failed"); assert!( - session.congestion_state().probes(), + session.connection().congestion_state().probes(), "client connection is not running its own controller" ); assert!( diff --git a/rs/moq-tokio/src/quic/driver/recv.rs b/rs/moq-tokio/src/quic/driver/recv.rs index 40a2339ce79..209cd8063f8 100644 --- a/rs/moq-tokio/src/quic/driver/recv.rs +++ b/rs/moq-tokio/src/quic/driver/recv.rs @@ -89,6 +89,16 @@ impl Stream { kio::wait(|waiter| self.reader.poll_read_buf(waiter, buf)).await } + /// Poll a read without retaining the destination buffer. + #[cfg(feature = "quic")] + pub(crate) fn poll_read_buf( + &mut self, + waiter: &kio::Waiter, + buf: &mut impl BufMut, + ) -> Poll, Error>> { + self.reader.poll_read_buf(waiter, buf) + } + /// Fill `buf` with data from the stream /// /// Fails with [`Error::FinishedEarly`] if the stream ends first. This operation is *not* @@ -202,7 +212,7 @@ impl Stream { /// /// Yields `Some` with the reset error code when the stream is reset by the peer. Yields `None` /// when the stream was [`finish()`](crate::quic::driver::send::Stream::finish)ed by the peer - /// and all data has been received, after which it is no longer meaningful for the stream to + /// and all data has been read, after which it is no longer meaningful for the stream to /// be reset. Fails with [`Error::Evicted`] once the stream was evicted to free a stuck receive /// window, until it is stopped. /// @@ -218,6 +228,24 @@ impl Stream { let waiter = self.park.hold(cx); self.reader.poll_received_reset(waiter) } + /// Observe FIN without consuming application bytes, including an empty stream's end. + #[cfg(feature = "quic")] + pub(crate) fn poll_closed(&mut self, cx: &Context<'_>) -> Poll, Error>> { + let waiter = self.park.hold(cx); + let read = self + .reader + .poll_read_generic(waiter, |chunks, waiter| match chunks.poll_next(waiter, 0) { + Poll::Ready(Ok(Some(_))) => ReadStatus::Readable(()), + Poll::Ready(res) => (None, res.err()).into(), + Poll::Pending => ReadStatus::Failed(None, moq_quic::recv::Error::Blocked), + }); + match ready!(read) { + Ok(Some(())) => self.reader.poll_received_reset(waiter), + Ok(None) => Poll::Ready(Ok(None)), + Err(Error::Reset(code)) => Poll::Ready(Ok(Some(code))), + Err(err) => Poll::Ready(Err(err)), + } + } } impl Reader { diff --git a/rs/moq-tokio/src/quic/driver/send.rs b/rs/moq-tokio/src/quic/driver/send.rs index 49c59be562e..7690bed33b9 100644 --- a/rs/moq-tokio/src/quic/driver/send.rs +++ b/rs/moq-tokio/src/quic/driver/send.rs @@ -107,6 +107,12 @@ impl Stream { kio::wait(|waiter| self.half.poll_write(waiter, buf)).await } + /// Poll a write without retaining or copying the caller's buffer. + #[cfg(feature = "quic")] + pub(crate) fn poll_write_buf(&mut self, cx: &Context<'_>, buf: &mut impl Buf) -> Poll> { + self.half.poll_write(self.park.hold(cx), buf) + } + /// Write all of `buf` into this stream /// /// # Cancel safety diff --git a/rs/moq-tokio/src/quic/web/connect.rs b/rs/moq-tokio/src/quic/web/connect.rs index 04b5cf4f964..e85f7493f89 100644 --- a/rs/moq-tokio/src/quic/web/connect.rs +++ b/rs/moq-tokio/src/quic/web/connect.rs @@ -18,25 +18,33 @@ impl std::fmt::Debug for Request<'_> { #[derive(Error, Debug, Clone)] #[non_exhaustive] +/// An error exchanging HTTP/3 CONNECT headers. pub enum ConnectError { + /// The HTTP/3 handshake failed at this step. #[error("quic stream was closed early")] UnexpectedEnd, + /// The HTTP/3 handshake failed at this step. #[error("protocol error: {0}")] ProtoError(#[from] web_transport_proto::ConnectError), + /// The QUIC connection failed. #[error("connection error")] ConnectionError(#[from] crate::quic::driver::connection::Error), + /// The QUIC receive stream failed. #[error("read error")] ReadError(#[from] crate::quic::driver::recv::Error), + /// The QUIC send stream failed. #[error("write error")] WriteError(#[from] crate::quic::driver::send::Error), + /// The HTTP/3 handshake failed at this step. #[error("http error status: {0}")] ErrorStatus(http::StatusCode), + /// The HTTP/3 handshake failed at this step. #[error("server returned protocol not in request: {0}")] ProtocolMismatch(String), } diff --git a/rs/moq-tokio/src/quic/web/error.rs b/rs/moq-tokio/src/quic/web/error.rs index a23b5b505cb..f3d29d8cd79 100644 --- a/rs/moq-tokio/src/quic/web/error.rs +++ b/rs/moq-tokio/src/quic/web/error.rs @@ -8,24 +8,31 @@ use crate::quic::web::{ConnectError, SettingsError}; #[derive(Error, Debug, Clone)] #[non_exhaustive] pub enum ClientError { + /// The peer ended the handshake before it was complete. #[error("unexpected end of stream")] UnexpectedEnd, + /// The QUIC connection failed. #[error("connection error: {0}")] Connection(#[from] crate::quic::driver::connection::Error), + /// The handshake could not write its stream. #[error("failed to write: {0}")] WriteError(#[from] crate::quic::driver::send::Error), + /// The handshake could not read its stream. #[error("failed to read: {0}")] ReadError(#[from] crate::quic::driver::recv::Error), + /// The HTTP/3 SETTINGS exchange failed. #[error("failed to exchange h3 settings: {0}")] SettingsError(#[from] SettingsError), + /// The HTTP/3 CONNECT exchange failed. #[error("failed to exchange h3 connect: {0}")] HttpError(#[from] ConnectError), + /// The QUIC handshake could not start. #[error("quic error: {0}")] ConnectError(#[from] crate::quic::driver::endpoint::ConnectError), } @@ -35,12 +42,15 @@ pub enum ClientError { #[derive(Clone, Error, Debug)] #[non_exhaustive] pub enum SessionError { + /// The QUIC connection failed. #[error("connection error: {0}")] ConnectionError(crate::quic::driver::connection::Error), + /// The WebTransport session failed. #[error("webtransport error: {0}")] WebTransportError(#[from] WebTransportError), + /// The connection refused the datagram. #[error("send datagram error: {0}")] SendDatagramError(#[from] crate::quic::driver::send::Error), } @@ -154,9 +164,11 @@ impl CloseReason { #[derive(Clone, Error, Debug)] #[non_exhaustive] pub enum WebTransportError { + /// The peer closed the session with its application code and reason. #[error("closed: code={0} reason={1}")] Closed(u32, String), + /// The stream or datagram named a different session. #[error("unknown session")] UnknownSession, @@ -164,177 +176,102 @@ pub enum WebTransportError { #[error("session gone")] SessionGone, + /// The QUIC receive stream failed. #[error("read error: {0}")] ReadError(#[from] crate::quic::driver::recv::Error), + /// The QUIC send stream failed. #[error("write error: {0}")] WriteError(#[from] crate::quic::driver::send::Error), } -/// An error when writing to [`crate::quic::web::SendStream`]. Similar to [`crate::quic::driver::send::Error`]. -#[derive(Clone, Error, Debug)] -#[non_exhaustive] -pub enum WriteError { - #[error("STOP_SENDING: {0}")] - Stopped(u32), - - #[error("invalid STOP_SENDING: {0}")] - InvalidStopped(crate::quic::driver::VarInt), - - #[error("session error: {0}")] - SessionError(#[from] SessionError), - - /// The stream was already finished or reset. - #[error("stream closed")] - ClosedStream, -} - -impl From for WriteError { - fn from(e: crate::quic::driver::send::Error) -> Self { - match e { - crate::quic::driver::send::Error::Stopped(code) => { - match web_transport_proto::error_from_http3(code.into_inner()) { - Some(code) => WriteError::Stopped(code), - None => WriteError::InvalidStopped(code), - } - } - crate::quic::driver::send::Error::Connection(e) => WriteError::SessionError(e.into()), - // Only datagrams fail otherwise, and they report a session error. - e => WriteError::SessionError(e.into()), - } - } -} - -/// An error when reading from [`crate::quic::web::RecvStream`]. Similar to [`crate::quic::driver::recv::Error`]. -#[derive(Clone, Error, Debug)] -#[non_exhaustive] -pub enum ReadError { - #[error("session error: {0}")] - SessionError(#[from] SessionError), - - #[error("RESET_STREAM: {0}")] - Reset(u32), - - #[error("invalid RESET_STREAM: {0}")] - InvalidReset(crate::quic::driver::VarInt), - - /// The stream was already stopped. - #[error("stream already closed")] - ClosedStream, - - /// The connection's receive window was stuck and this stream was discarded to free it. - #[error("evicted")] - Evicted, - - /// The stream finished before [`read_exact`](crate::quic::web::RecvStream::read_exact) - /// filled its buffer, after this many bytes. - #[error("finished early")] - FinishedEarly(usize), - - /// The stream is longer than [`read_to_end`](crate::quic::web::RecvStream::read_to_end)'s limit. - #[error("too long")] - TooLong, - - /// A lease wanted more than the connection receive window can hold. - #[error("want exceeds the receive window")] - TooLarge, -} - -impl From for ReadError { - fn from(value: crate::quic::driver::recv::Error) -> Self { - use crate::quic::driver::recv::Error; - match value { - Error::Reset(code) => match web_transport_proto::error_from_http3(code.into_inner()) { - Some(code) => ReadError::Reset(code), - None => ReadError::InvalidReset(code), - }, - Error::Connection(e) => ReadError::SessionError(e.into()), - Error::Evicted => ReadError::Evicted, - Error::FinishedEarly(n) => ReadError::FinishedEarly(n), - Error::TooLong => ReadError::TooLong, - Error::TooLarge => ReadError::TooLarge, - } - } -} - /// An error returned when receiving a new WebTransport session. #[derive(Error, Debug, Clone)] #[non_exhaustive] pub enum ServerError { + /// The peer ended the handshake before it was complete. #[error("unexpected end of stream")] UnexpectedEnd, + /// The QUIC connection failed. #[error("connection error")] Connection(#[from] crate::quic::driver::connection::Error), + /// The handshake could not write its stream. #[error("failed to write")] WriteError(#[from] crate::quic::driver::send::Error), + /// The handshake could not read its stream. #[error("failed to read")] ReadError(#[from] crate::quic::driver::recv::Error), + /// The HTTP/3 SETTINGS exchange failed. #[error("failed to exchange h3 settings")] SettingsError(#[from] SettingsError), + /// The HTTP/3 CONNECT exchange failed. #[error("failed to exchange h3 connect")] ConnectError(#[from] ConnectError), + /// The endpoint socket failed. #[error("io error: {0}")] IoError(Arc), #[cfg(any(feature = "aws-lc-rs", feature = "ring"))] + /// The TLS configuration was invalid. #[error("rustls error: {0}")] Rustls(#[from] rustls::Error), } -impl web_transport_trait::Error for SessionError { - fn session_error(&self) -> Option<(u32, String)> { - if let SessionError::WebTransportError(WebTransportError::Closed(code, reason)) = self { - return Some((*code, reason.to_string())); - } - - None - } -} - -impl web_transport_trait::Error for WriteError { - fn session_error(&self) -> Option<(u32, String)> { - if let WriteError::SessionError(e) = self { - return e.session_error(); - } - - None - } - - fn stream_error(&self) -> Option { +impl moq_net::transport::Error for SessionError { + fn close_kind(&self) -> moq_net::transport::CloseKind { + use crate::quic::driver::connection::Error; match self { - WriteError::Stopped(code) => Some(*code), - _ => None, + Self::ConnectionError(Error::TimedOut) => moq_net::transport::CloseKind::IdleTimeout, + Self::ConnectionError(Error::LocallyClosed { .. }) => moq_net::transport::CloseKind::Closed, + _ => moq_net::transport::CloseKind::Transport, } } -} - -impl web_transport_trait::Error for ReadError { fn session_error(&self) -> Option<(u32, String)> { - if let ReadError::SessionError(e) = self { - return e.session_error(); + if let SessionError::WebTransportError(WebTransportError::Closed(code, reason)) = self { + return Some((*code, reason.to_string())); } None } - - fn stream_error(&self) -> Option { - match self { - ReadError::Reset(code) => Some(*code), - _ => None, - } - } } #[cfg(test)] mod tests { use super::*; + #[test] + fn transport_error_kinds_preserve_stream_and_session_registries() { + use moq_net::transport::{CloseKind, Error as _}; + let idle = SessionError::ConnectionError(crate::quic::driver::connection::Error::TimedOut); + assert_eq!(idle.close_kind(), CloseKind::IdleTimeout); + assert_eq!( + super::super::recv::Error::SessionError(idle.clone()).close_kind(), + CloseKind::IdleTimeout + ); + assert_eq!( + super::super::send::Error::SessionError(idle).close_kind(), + CloseKind::IdleTimeout + ); + assert_eq!(super::super::recv::Error::Evicted.close_kind(), CloseKind::Evicted); + let local = SessionError::ConnectionError(crate::quic::driver::connection::Error::LocallyClosed { + code: 7u32.into(), + reason: String::new(), + }); + assert_eq!(local.close_kind(), CloseKind::Closed); + let session = SessionError::WebTransportError(WebTransportError::Closed(7, "session".into())); + assert_eq!(session.session_error(), Some((7, "session".into()))); + assert_eq!(session.stream_error(), None); + let stream = super::super::recv::Error::Reset(7); + assert_eq!(stream.session_error(), None); + assert_eq!(stream.stream_error(), Some(7)); + } + /// WebTransport keeps the HTTP/3 mapping in both directions, and a code outside its /// range is not a WebTransport code. #[test] diff --git a/rs/moq-tokio/src/quic/web/mod.rs b/rs/moq-tokio/src/quic/web/mod.rs index c26309fb6b7..d5082baab09 100644 --- a/rs/moq-tokio/src/quic/web/mod.rs +++ b/rs/moq-tokio/src/quic/web/mod.rs @@ -28,14 +28,12 @@ // External mod error; -mod recv; -mod send; +pub mod recv; +pub mod send; mod server; mod session; pub use error::*; -pub use recv::*; -pub use send::*; pub use server::*; pub use session::*; diff --git a/rs/moq-tokio/src/quic/web/recv.rs b/rs/moq-tokio/src/quic/web/recv.rs index 7ede7c7201d..9b7b71d9e1a 100644 --- a/rs/moq-tokio/src/quic/web/recv.rs +++ b/rs/moq-tokio/src/quic/web/recv.rs @@ -1,3 +1,5 @@ +//! Incoming WebTransport streams, their leases, and errors. + use std::{ io, pin::Pin, @@ -8,20 +10,20 @@ use std::{ use bytes::{BufMut, Bytes}; use crate::quic::driver::recv; -use crate::quic::web::{CloseReason, ReadError}; +use crate::quic::web::{CloseReason, SessionError}; /// A stream that can be used to receive bytes. See [`crate::quic::driver::recv::Stream`]. /// /// Dropping it before the end stops it with code 0. #[derive(Debug)] -pub struct RecvStream { - /// Taken by [`Self::stop`], since `web_transport_trait` stops through `&mut self`. +pub struct Stream { + /// Taken when moq-net stops through its mutable poll trait. inner: Option, close: Arc, id: crate::quic::driver::stream::Id, } -impl RecvStream { +impl Stream { pub(crate) fn new(mut stream: recv::Stream, close: Arc) -> Self { // The driver's raw 0 is outside the HTTP/3 range, so a WebTransport peer can't decode it. stream.set_drop_stop(close.encode_stream_code(0)); @@ -32,26 +34,26 @@ impl RecvStream { } } - fn inner(&mut self) -> Result<&mut recv::Stream, ReadError> { - self.inner.as_mut().ok_or(ReadError::ClosedStream) + fn inner(&mut self) -> Result<&mut recv::Stream, Error> { + self.inner.as_mut().ok_or(Error::ClosedStream) } /// Lease the stream's unreleased bytes. See [`recv::Stream::poll_lease`]. - pub fn poll_lease(&mut self, cx: &mut Context<'_>, want: usize) -> Poll, ReadError>> { + pub fn poll_lease(&mut self, cx: &mut Context<'_>, want: usize) -> Poll, Error>> { let close = &self.close; match &mut self.inner { Some(inner) => inner.poll_lease(cx, want).map_err(|e| map_error(close, e)), - None => Poll::Ready(Err(ReadError::ClosedStream)), + None => Poll::Ready(Err(Error::ClosedStream)), } } /// Poll until the peer resets the stream, the receiver evicts it, or the session closes, /// without reading it; pending for good once every byte arrived. See /// [`recv::Stream::received_reset`]. - pub fn poll_reset(&mut self, cx: &mut Context<'_>) -> Poll { + pub fn poll_reset(&mut self, cx: &mut Context<'_>) -> Poll { let close = &self.close; let Some(inner) = &mut self.inner else { - return Poll::Ready(ReadError::ClosedStream); + return Poll::Ready(Error::ClosedStream); }; match ready!(inner.poll_received_reset(cx)) { Ok(None) => Poll::Pending, @@ -70,32 +72,63 @@ impl RecvStream { /// Tell the other end to stop sending data with the given error code. See /// [`recv::Stream::stop`]. This is a u32 with WebTransport since it shares the error /// space with HTTP/3. A raw QUIC session sends the code as is. Does nothing once stopped. - pub fn stop(&mut self, code: u32) { + fn stop_mut(&mut self, code: u32) { if let Some(inner) = self.inner.take() { inner.stop(self.close.encode_stream_code(code)); } } + /// Stop receiving and tell the peer why. + pub fn stop(mut self, code: u32) { + self.stop_mut(code); + } + + /// Wait until the stream ends after its bytes are read, the peer resets it, or the session closes. + pub async fn closed(&mut self) -> Result<(), Error> { + kio::wait(|waiter| self.poll_closed(waiter)).await + } + + /// Poll until the stream ends after its bytes are read, the peer resets it, or the session closes. + pub fn poll_closed(&mut self, waiter: &kio::Waiter) -> Poll> { + let close = &self.close; + let Some(inner) = &mut self.inner else { + return Poll::Ready(Ok(())); + }; + match ready!(inner.poll_closed(&waiter.context())) { + Ok(None) => Poll::Ready(Ok(())), + Ok(Some(code)) => Poll::Ready(Err(map_error(close, recv::Error::Reset(code)))), + Err(err) => Poll::Ready(Err(map_error(close, err))), + } + } + + /// Poll for some bytes without retaining the destination buffer. + pub fn poll_read(&mut self, waiter: &kio::Waiter, mut buf: &mut [u8]) -> Poll, Error>> { + let close = self.close.clone(); + self.inner()? + .poll_read_buf(waiter, &mut buf) + .map_err(|err| map_error(&close, err)) + } + /// Read some data into `buf` and return the amount read. See [`recv::Stream::read`]. - pub async fn read(&mut self, buf: &mut impl BufMut) -> Result, ReadError> { + pub async fn read(&mut self, buf: &mut impl BufMut) -> Result, Error> { let result = self.inner()?.read(buf).await; result.map_err(|e| map_error(&self.close, e)) } /// Fill the entire buffer with data. See [`recv::Stream::read_exact`]. - pub async fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), ReadError> { + pub async fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), Error> { let result = self.inner()?.read_exact(buf).await; result.map_err(|e| map_error(&self.close, e)) } /// Read a chunk of data from the stream. See [`recv::Stream::read_chunk`]. - pub async fn read_chunk(&mut self, max: usize) -> Result, ReadError> { + pub async fn read_chunk(&mut self, max: usize) -> Result, Error> { let result = self.inner()?.read_chunk(max).await; result.map_err(|e| map_error(&self.close, e)) } /// Read until the end of the stream or the limit is hit. See [`recv::Stream::read_to_end`]. - pub async fn read_to_end(&mut self, limit: usize) -> Result, ReadError> { + pub async fn read_to_end(&mut self, limit: usize) -> Result, Error> { let result = self.inner()?.read_to_end(limit).await; result.map_err(|e| map_error(&self.close, e)) } @@ -104,16 +137,16 @@ impl RecvStream { /// [`recv::Stream::received_reset`]. /// /// Returns the stream's reset code, or fails with its session error or - /// [`ReadError::Evicted`] once the receiver evicted it to free a stuck window. - pub async fn received_reset(&mut self) -> Result, ReadError> { + /// [`Error::Evicted`] once the receiver evicted it to free a stuck window. + pub async fn received_reset(&mut self) -> Result, Error> { let Some(inner) = &mut self.inner else { return Ok(None); }; match inner.received_reset().await { Ok(None) => Ok(None), - Ok(Some(code)) => match self.close.session_gone(code) { - Some(err) => Err(ReadError::SessionError(err)), - None => Ok(self.close.decode_stream_code(code)), + Ok(Some(code)) => match map_error(&self.close, recv::Error::Reset(code)) { + Error::Reset(code) => Ok(Some(code)), + err => Err(err), }, Err(err) => Err(map_error(&self.close, err)), } @@ -127,7 +160,7 @@ impl RecvStream { /// > sessions. /// > The [crate::quic::driver::stream::Id::index] might not increment by 1 like expected when using raw QUIC. /// > This is why the Javascript WebTransport API does not expose the Stream ID. - pub fn quic_id(&self) -> crate::quic::driver::stream::Id { + pub fn id(&self) -> crate::quic::driver::stream::Id { self.id } @@ -136,23 +169,23 @@ impl RecvStream { /// Decode the peer's reset code, and report a session error, or a reset for a session /// that is gone, as the session's close reason. -fn map_error(close: &CloseReason, e: recv::Error) -> ReadError { +fn map_error(close: &CloseReason, e: recv::Error) -> Error { match e { recv::Error::Reset(code) => match close.decode_stream_code(code) { - Some(code) => ReadError::Reset(code), + Some(code) => Error::Reset(code), None => match close.session_gone(code) { - Some(err) => ReadError::SessionError(err), - None => ReadError::InvalidReset(code), + Some(err) => Error::SessionError(err), + None => Error::InvalidReset(code), }, }, - e => match ReadError::from(e) { - ReadError::SessionError(e) => ReadError::SessionError(close.map(e)), + e => match Error::from(e) { + Error::SessionError(e) => Error::SessionError(close.map(e)), e => e, }, } } -impl tokio::io::AsyncRead for RecvStream { +impl tokio::io::AsyncRead for Stream { fn poll_read(mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut tokio::io::ReadBuf) -> Poll> { match &mut self.inner { Some(inner) => Pin::new(inner).poll_read(cx, buf), @@ -161,23 +194,112 @@ impl tokio::io::AsyncRead for RecvStream { } } -impl web_transport_trait::RecvStream for RecvStream { - type Error = ReadError; - +impl moq_net::transport::poll::RecvStream for Stream { + type Error = Error; + type Lease<'a> = recv::Lease<'a>; + fn poll_lease(&mut self, waiter: &kio::Waiter, want: usize) -> Poll, Self::Error>> { + Self::poll_lease(self, &mut waiter.context(), want) + } + fn set_priority(&mut self, priority: i64) { + Self::set_priority(self, priority); + } fn stop(&mut self, code: u32) { - Self::stop(self, code); + Self::stop_mut(self, code); + } + fn poll_reset(&mut self, waiter: &kio::Waiter) -> Poll { + Self::poll_reset(self, &mut waiter.context()) + } +} +impl moq_net::transport::poll::Lease for recv::Lease<'_> { + fn is_fin(&self) -> bool { + Self::is_fin(self) + } + fn chunks(&self) -> impl Iterator { + Self::chunks(self) } + fn release(self) { + Self::release(self); + } +} + +/// An error when reading from [`crate::quic::web::recv::Stream`]. Similar to [`crate::quic::driver::recv::Error`]. +#[derive(Clone, thiserror::Error, Debug)] +#[non_exhaustive] +pub enum Error { + /// The session carrying the stream failed. + #[error("session error: {0}")] + SessionError(#[from] SessionError), + + /// The peer reset this stream with its application code. + #[error("RESET_STREAM: {0}")] + Reset(u32), + + /// The peer reset the stream with a code outside the WebTransport registry. + #[error("invalid RESET_STREAM: {0}")] + InvalidReset(crate::quic::driver::VarInt), + + /// The stream was already stopped. + #[error("stream already closed")] + ClosedStream, + + /// The connection's receive window was stuck and this stream was discarded to free it. + #[error("evicted")] + Evicted, + + /// The stream finished before [`read_exact`](crate::quic::web::recv::Stream::read_exact) + /// filled its buffer, after this many bytes. + #[error("finished early")] + FinishedEarly(usize), + + /// The stream is longer than [`read_to_end`](crate::quic::web::recv::Stream::read_to_end)'s limit. + #[error("too long")] + TooLong, + + /// A lease wanted more than the connection receive window can hold. + #[error("want exceeds the receive window")] + TooLarge, +} + +impl From for Error { + fn from(value: crate::quic::driver::recv::Error) -> Self { + use crate::quic::driver::recv::Error as DriverError; + match value { + DriverError::Reset(code) => match web_transport_proto::error_from_http3(code.into_inner()) { + Some(code) => Error::Reset(code), + None => Error::InvalidReset(code), + }, + DriverError::Connection(e) => Error::SessionError(e.into()), + DriverError::Evicted => Error::Evicted, + DriverError::FinishedEarly(n) => Error::FinishedEarly(n), + DriverError::TooLong => Error::TooLong, + DriverError::TooLarge => Error::TooLarge, + } + } +} - async fn read(&mut self, mut dst: &mut [u8]) -> Result, Self::Error> { - self.read(&mut dst).await +impl moq_net::transport::Error for Error { + fn close_kind(&self) -> moq_net::transport::CloseKind { + match self { + Self::Evicted => moq_net::transport::CloseKind::Evicted, + Self::SessionError(e) => e.close_kind(), + _ => moq_net::transport::CloseKind::Transport, + } } + fn session_error(&self) -> Option<(u32, String)> { + if let Error::SessionError(e) = self { + return e.session_error(); + } - async fn read_chunk(&mut self, max: usize) -> Result, Self::Error> { - self.read_chunk(max).await + None } - async fn closed(&mut self) -> Result<(), Self::Error> { - self.received_reset().await?; - Ok(()) + fn stream_error(&self) -> Option { + match self { + Error::Reset(code) => Some(*code), + _ => None, + } } } + +/// The driver's receive lease, which retains flow-control credit until released. +pub use crate::quic::driver::recv::Lease; diff --git a/rs/moq-tokio/src/quic/web/send.rs b/rs/moq-tokio/src/quic/web/send.rs index c7eaaa7f80e..3800c169c17 100644 --- a/rs/moq-tokio/src/quic/web/send.rs +++ b/rs/moq-tokio/src/quic/web/send.rs @@ -1,3 +1,5 @@ +//! Outgoing WebTransport streams, their finish handles, and errors. + use std::{ io, pin::Pin, @@ -5,11 +7,10 @@ use std::{ task::{Context, Poll}, }; -use bytes::{Buf, Bytes}; -use futures::FutureExt; +use bytes::Buf; use crate::quic::driver::send; -use crate::quic::web::{CloseReason, SessionError, WriteError}; +use crate::quic::web::{CloseReason, SessionError}; /// A stream that can be used to send bytes. See [`crate::quic::driver::send::Stream`]. /// @@ -19,7 +20,7 @@ use crate::quic::web::{CloseReason, SessionError, WriteError}; /// This wrapper is mainly needed for error codes, which is unfortunate. /// WebTransport uses u32 error codes and they're mapped in a reserved HTTP/3 error space. #[derive(Debug)] -pub struct SendStream { +pub struct Stream { state: State, close: Arc, /// The end of the WebTransport header this side wrote first, which a reset must still deliver. @@ -27,7 +28,7 @@ pub struct SendStream { id: crate::quic::driver::stream::Id, } -/// Where the stream is, since `web_transport_trait` finishes and resets through `&mut self`. +/// The mutable terminal state required by moq-net's poll trait. #[derive(Debug)] enum State { Open(send::Stream), @@ -36,7 +37,7 @@ enum State { Reset(Result, send::Error>), } -impl SendStream { +impl Stream { pub(crate) fn new(mut stream: send::Stream, close: Arc, header: Option) -> Self { stream.set_drop_reset(header, close.encode_stream_code(0)); Self { @@ -49,26 +50,26 @@ impl SendStream { /// Decode the peer's stop code, and report a session error, or a stop for a session /// that is gone, as the session's close reason. - fn map_error(&self, e: send::Error) -> WriteError { + fn map_error(&self, e: send::Error) -> Error { match e { send::Error::Stopped(code) => match self.close.decode_stream_code(code) { - Some(code) => WriteError::Stopped(code), + Some(code) => Error::Stopped(code), None => match self.close.session_gone(code) { - Some(err) => WriteError::SessionError(err), - None => WriteError::InvalidStopped(code), + Some(err) => Error::SessionError(err), + None => Error::InvalidStopped(code), }, }, - e => match WriteError::from(e) { - WriteError::SessionError(e) => WriteError::SessionError(self.close.map(e)), + e => match Error::from(e) { + Error::SessionError(e) => Error::SessionError(self.close.map(e)), e => e, }, } } - fn open(&mut self) -> Result<&mut send::Stream, WriteError> { + fn open(&mut self) -> Result<&mut send::Stream, Error> { match &mut self.state { State::Open(stream) => Ok(stream), - _ => Err(WriteError::ClosedStream), + _ => Err(Error::ClosedStream), } } @@ -78,80 +79,87 @@ impl SendStream { /// /// The WebTransport header still reaches the peer, so it can tell which session the reset /// belongs to. - pub fn reset(&mut self, code: u32) { + fn reset_mut(&mut self, code: u32) { let code = self.close.encode_stream_code(code); - let header = self.header; - // The peer's stop code still wins if it arrived first, which only a `stopped` future - // taken before the reset learns, and the reset settles it. - let stopped = match std::mem::replace(&mut self.state, State::Reset(Ok(None))) { - State::Open(stream) => { - let stopped = stream.stopped(); - match header { - Some(header) => stream.reset_at(header, code), - None => stream.reset(code), - } - stopped.now_or_never() - } - State::Finished(stream) => { - let stopped = stream.stopped(); - match header { - Some(header) => stream.reset_at(header, code), - None => stream.reset(code), - } - stopped.now_or_never() - } - state @ State::Reset(_) => { - self.state = state; - return; - } + let stopped = match &self.state { + State::Open(stream) => stream.poll_closed(&kio::Waiter::noop()), + State::Finished(stream) => stream.poll_closed(&kio::Waiter::noop()), + State::Reset(_) => return, }; - self.state = State::Reset(stopped.expect("a reset settles its stopped future")); + let stopped = match stopped { + Poll::Ready(result) => result, + Poll::Pending => Ok(None), + }; + match std::mem::replace(&mut self.state, State::Reset(stopped)) { + State::Open(stream) => match self.header { + Some(header) => stream.reset_at(header, code), + None => stream.reset(code), + }, + State::Finished(stream) => match self.header { + Some(header) => stream.reset_at(header, code), + None => stream.reset(code), + }, + State::Reset(_) => unreachable!(), + } } - /// Wait until the stream has been stopped and return the error code. See - /// [`send::Finished::stopped`]. - /// - /// Unlike the driver, this returns None if the code is not a valid WebTransport error code. - pub async fn stopped(&self) -> Result, SessionError> { - let stopped = match &self.state { - State::Open(stream) => stream.stopped().await, - State::Finished(stream) => stream.stopped().await, - State::Reset(stopped) => stopped.clone(), + /// Poll until all data was acknowledged, the peer stopped the stream, or the session closed. + pub fn poll_closed(&self, waiter: &kio::Waiter) -> Poll, Error>> { + let result = match &self.state { + State::Open(stream) => stream.poll_closed(waiter), + State::Finished(stream) => stream.poll_closed(waiter), + State::Reset(result) => Poll::Ready(result.clone()), }; - match stopped { - Ok(Some(code)) => match self.close.session_gone(code) { - Some(err) => Err(err), - None => Ok(self.close.decode_stream_code(code)), - }, + result.map(|result| match result { Ok(None) => Ok(None), - Err(err) => match WriteError::from(err) { - WriteError::SessionError(err) => Err(self.close.map(err)), - err => unreachable!("only the connection fails a stopped wait: {err}"), + Ok(Some(code)) => match self.map_error(send::Error::Stopped(code)) { + Error::Stopped(code) => Ok(Some(code)), + err => Err(err), }, + Err(err) => Err(self.map_error(err)), + }) + } + + /// Wait until all data was acknowledged, the peer stopped the stream, or the session closed. + pub async fn stopped(&self) -> Result, Error> { + kio::wait(|w| self.poll_closed(w)).await + } + + /// Finish sending, retaining a handle to acknowledgements and peer cancellation. + pub fn finish(mut self) -> Finished { + self.finish_mut(); + Finished { + stream: self, + park: Default::default(), } } + /// Reset the stream while still delivering its WebTransport header. + pub fn reset(mut self, code: u32) { + self.reset_mut(code); + } + /// Write from `buf`, returning the size written. See [`send::Stream::write`]. - pub async fn write(&mut self, buf: &mut impl Buf) -> Result { + pub async fn write(&mut self, buf: &mut impl Buf) -> Result { let result = self.open()?.write(buf).await; result.map_err(|e| self.map_error(e)) } /// Write all of `buf` to the stream. See [`send::Stream::write_all`]. - pub async fn write_all(&mut self, buf: impl Buf) -> Result<(), WriteError> { + pub async fn write_all(&mut self, buf: impl Buf) -> Result<(), Error> { let result = self.open()?.write_all(buf).await; result.map_err(|e| self.map_error(e)) } /// Mark the stream as finished, such that no more data can be written. See /// [`send::Stream::finish`]. Fails if it was already finished or reset. - pub fn finish(&mut self) -> Result<(), WriteError> { - self.open()?; - let State::Open(stream) = std::mem::replace(&mut self.state, State::Reset(Ok(None))) else { - unreachable!("checked above"); - }; - self.state = State::Finished(stream.finish()); - Ok(()) + fn finish_mut(&mut self) { + if matches!(self.state, State::Open(_)) { + let State::Open(stream) = std::mem::replace(&mut self.state, State::Reset(Ok(None))) else { + unreachable!() + }; + self.state = State::Finished(stream.finish()); + } } /// Set the priority within the stream's send group, or session-wide when ungrouped. @@ -189,12 +197,12 @@ impl SendStream { /// > sessions. /// > The [crate::quic::driver::stream::Id::index] might not increment by 1 like expected when using raw QUIC. /// > This is why the Javascript WebTransport API does not expose the Stream ID. - pub fn quic_id(&self) -> crate::quic::driver::stream::Id { + pub fn id(&self) -> crate::quic::driver::stream::Id { self.id } } -impl tokio::io::AsyncWrite for SendStream { +impl tokio::io::AsyncWrite for Stream { fn poll_write(mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll> { match &mut self.state { State::Open(stream) => Pin::new(stream).poll_write(cx, buf), @@ -208,42 +216,153 @@ impl tokio::io::AsyncWrite for SendStream { fn poll_shutdown(mut self: Pin<&mut Self>, _cx: &mut Context) -> Poll> { // Already finished or reset, so there is nothing left to shut. - let _ = self.finish(); + self.finish_mut(); Poll::Ready(Ok(())) } } -impl web_transport_trait::SendStream for SendStream { - type Error = WriteError; +impl moq_net::transport::poll::SendStream for Stream { + type Error = Error; + type Group = send::Group; - fn set_priority(&mut self, order: i32) { - Self::set_priority(self, order.into()); + fn poll_write(&mut self, waiter: &kio::Waiter, mut buf: &[u8]) -> Poll> { + self.poll_write_buf(waiter, &mut buf) + } + fn poll_write_buf(&mut self, waiter: &kio::Waiter, buf: &mut B) -> Poll> { + let result = self.open()?.poll_write_buf(&waiter.context(), buf); + result.map_err(|e| self.map_error(e)) + } + fn set_priority(&mut self, priority: i64) { + Self::set_priority(self, priority); + } + fn set_group(&mut self, group: &Self::Group) { + Self::set_group(self, Some(group)); + } + fn finish(&mut self) -> Result<(), Self::Error> { + Self::finish_mut(self); + Ok(()) } - fn reset(&mut self, code: u32) { - Self::reset(self, code); + Self::reset_mut(self, code); } + fn poll_closed(&self, waiter: &kio::Waiter) -> Poll> { + Self::poll_closed(self, waiter).map(|result| match result { + Ok(None) => Ok(()), + Ok(Some(code)) => Err(Error::Stopped(code)), + Err(err) => Err(err), + }) + } +} - fn finish(&mut self) -> Result<(), Self::Error> { - Self::finish(self) +impl moq_net::transport::poll::SendGroup for send::Group { + fn set_priority(&mut self, priority: i64) { + Self::set_priority(self, priority); + } +} + +/// A finished stream whose data keeps transmitting until acknowledged or stopped. +#[derive(Debug)] +pub struct Finished { + stream: Stream, + park: kio::Park, +} +impl Finished { + /// Poll until the peer acknowledges or stops the stream, or the session closes. + pub fn poll_closed(&self, waiter: &kio::Waiter) -> Poll, Error>> { + self.stream.poll_closed(waiter) + } + /// Wait until the peer acknowledges or stops the stream, or the session closes. + pub async fn stopped(&self) -> Result, Error> { + self.stream.stopped().await + } + /// Reset the stream while still delivering its WebTransport header. + pub fn reset(self, code: u32) { + self.stream.reset(code); + } + /// Set the stream's priority within its send group. + pub fn set_priority(&mut self, priority: i64) { + self.stream.set_priority(priority); + } + /// Return the stream's priority within its send group. + pub fn priority(&self) -> i64 { + self.stream.priority() + } + /// Move the stream to a send group, or out with `None`. + pub fn set_group(&mut self, group: Option<&send::Group>) { + self.stream.set_group(group); + } + /// Return the underlying QUIC stream ID. + pub fn id(&self) -> crate::quic::driver::stream::Id { + self.stream.id() } +} +impl Future for Finished { + type Output = Result, Error>; + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = &mut *self; + this.stream.poll_closed(this.park.hold(cx)) + } +} + +/// An error when writing to [`crate::quic::web::send::Stream`]. Similar to [`crate::quic::driver::send::Error`]. +#[derive(Clone, thiserror::Error, Debug)] +#[non_exhaustive] +pub enum Error { + /// The peer stopped accepting this stream with its application code. + #[error("STOP_SENDING: {0}")] + Stopped(u32), + + /// The peer stopped the stream with a code outside the WebTransport registry. + #[error("invalid STOP_SENDING: {0}")] + InvalidStopped(crate::quic::driver::VarInt), + + /// The session carrying the stream failed. + #[error("session error: {0}")] + SessionError(#[from] SessionError), + + /// The stream was already finished or reset. + #[error("stream closed")] + ClosedStream, +} - async fn write(&mut self, mut buf: &[u8]) -> Result { - Self::write(self, &mut buf).await +impl From for Error { + fn from(e: crate::quic::driver::send::Error) -> Self { + match e { + crate::quic::driver::send::Error::Stopped(code) => { + match web_transport_proto::error_from_http3(code.into_inner()) { + Some(code) => Error::Stopped(code), + None => Error::InvalidStopped(code), + } + } + crate::quic::driver::send::Error::Connection(e) => Error::SessionError(e.into()), + // Only datagrams fail otherwise, and they report a session error. + e => Error::SessionError(e.into()), + } } +} - async fn write_buf(&mut self, buf: &mut B) -> Result { - Self::write(self, buf).await +impl moq_net::transport::Error for Error { + fn close_kind(&self) -> moq_net::transport::CloseKind { + match self { + Self::SessionError(e) => e.close_kind(), + _ => moq_net::transport::CloseKind::Transport, + } } + fn session_error(&self) -> Option<(u32, String)> { + if let Error::SessionError(e) = self { + return e.session_error(); + } - async fn write_chunk(&mut self, chunk: Bytes) -> Result<(), Self::Error> { - self.write_all(chunk).await + None } - async fn closed(&mut self) -> Result<(), Self::Error> { - match self.stopped().await? { - Some(code) => Err(WriteError::Stopped(code)), - None => Ok(()), + fn stream_error(&self) -> Option { + match self { + Error::Stopped(code) => Some(*code), + _ => None, } } } + +/// The driver's send groups and scheduling options. +pub use crate::quic::driver::send::{Group, Options}; diff --git a/rs/moq-tokio/src/quic/web/server.rs b/rs/moq-tokio/src/quic/web/server.rs index 3b69ec91ed8..48955255b2f 100644 --- a/rs/moq-tokio/src/quic/web/server.rs +++ b/rs/moq-tokio/src/quic/web/server.rs @@ -1,56 +1,8 @@ -use futures::{StreamExt, future::BoxFuture, stream::FuturesUnordered}; - use crate::quic::web::{ Connecting, ServerError, Session, Settings, proto::{ConnectRequest, ConnectResponse}, }; -/// A WebTransport server that accepts new sessions. -pub struct Server { - endpoint: crate::quic::driver::endpoint::Endpoint, - accept: FuturesUnordered>>, -} - -impl core::ops::Deref for Server { - type Target = crate::quic::driver::endpoint::Endpoint; - - fn deref(&self) -> &Self::Target { - &self.endpoint - } -} - -impl Server { - /// Manually create a new server with a manually constructed Endpoint. - /// - /// NOTE: The ALPN must be set to `crate::quic::web::ALPN` for WebTransport to work. - pub fn new(endpoint: crate::quic::driver::endpoint::Endpoint) -> Self { - Self { - endpoint, - accept: Default::default(), - } - } - - /// Accept a new WebTransport session Request from a client. - pub async fn accept(&mut self) -> Option { - loop { - tokio::select! { - res = self.endpoint.accept() => { - let conn = res?; - self.accept.push(Box::pin(async move { - let conn = conn.accept()?.await?; - Request::accept(conn).await - })); - } - Some(res) = self.accept.next() => { - if let Ok(session) = res { - return Some(session) - } - } - } - } - } -} - /// A mostly complete WebTransport handshake, just awaiting the server's decision on whether to /// accept or reject the session based on the URL. pub struct Request { @@ -76,6 +28,7 @@ impl Request { }) } + /// Accept the request without selecting a subprotocol. pub async fn ok(self) -> Result { self.respond(ConnectResponse::OK).await } @@ -85,6 +38,9 @@ impl Request { /// [ConnectResponse::with_protocol] can be used to select a subprotocol. pub async fn respond(self, response: impl Into) -> Result { let response = response.into(); + if response.status != http::StatusCode::OK { + return Err(super::ConnectError::ErrorStatus(response.status).into()); + } let connect = self.connect.respond(response).await?; Ok(Session::new(self.conn, self.settings, connect)) } @@ -96,16 +52,9 @@ impl Request { } /// Returns the underlying QUIC connection. - pub fn conn(&self) -> &crate::quic::driver::connection::Connection { + pub fn connection(&self) -> &crate::quic::driver::connection::Connection { &self.conn } - - /// Returns the CONNECT request that was sent by the client. - /// - /// DEPRECATED: You can access this via the Deref impl. - pub fn connect(&self) -> &ConnectRequest { - &self.connect - } } impl core::ops::Deref for Request { diff --git a/rs/moq-tokio/src/quic/web/session.rs b/rs/moq-tokio/src/quic/web/session.rs index 4e0caf146ba..c97a1e5f830 100644 --- a/rs/moq-tokio/src/quic/web/session.rs +++ b/rs/moq-tokio/src/quic/web/session.rs @@ -2,7 +2,6 @@ use std::{ fmt, future::Future, io::Cursor, - ops::Deref, pin::Pin, sync::{Arc, Mutex}, task::{Context, Poll, Waker}, @@ -10,14 +9,16 @@ use std::{ }; use bytes::{Buf, Bytes}; -use futures::stream::{FuturesUnordered, Stream, StreamExt}; +use futures::stream::{FuturesUnordered, StreamExt}; use kio::Waiter; use crate::quic::driver::send; use crate::quic::web::fan::Fan; use crate::quic::web::{ - ClientError, CloseReason, Connected, RecvStream, SendStream, SessionError, Settings, WebTransportError, + ClientError, CloseReason, Connected, SessionError, Settings, WebTransportError, proto::{ConnectRequest, ConnectResponse, Frame, StreamUni, VarInt}, + recv::Stream as RecvStream, + send::Stream as SendStream, }; /// An established WebTransport session, acting like a full QUIC connection. See @@ -28,8 +29,6 @@ use crate::quic::web::{ /// 2. Errors codes are encoded with the session ID, so they aren't full QUIC error codes. /// 3. Stream IDs may have gaps in them, used by HTTP/3 transparent to the application. /// -/// Deref is used to expose non-overloaded methods on [`crate::quic::driver::connection::Connection`]. -/// These should be safe to use with WebTransport, but file a PR if you find one that isn't. #[derive(Clone)] pub struct Session { conn: crate::quic::driver::connection::Connection, @@ -41,9 +40,9 @@ pub struct Session { accept: Option>>, // Cache the headers in front of each stream we open. - header_uni: Vec, - header_bi: Vec, - header_datagram: Vec, + header_uni: Bytes, + header_bi: Bytes, + header_datagram: Bytes, #[allow( dead_code, @@ -69,6 +68,7 @@ pub struct Session { // connection's lock so `protocol()` can borrow it. HTTP/3 sessions read the // subprotocol out of the response instead. alpn: Option>, + opening: Arc>, } impl Session { @@ -102,15 +102,16 @@ impl Session { conn, accept: Some(Arc::new(Mutex::new(accept))), session_id: Some(session_id), - header_uni, - header_bi, - header_datagram, + header_uni: header_uni.into(), + header_bi: header_bi.into(), + header_datagram: header_datagram.into(), settings: Some(Arc::new(settings)), connect_send: Arc::new(Mutex::new(Some(connect.send))), close: close.clone(), request: Some(connect.request.clone()), response: Some(connect.response.clone()), alpn: None, + opening: Default::default(), }; // Run a background task to read capsules from the CONNECT recv stream. @@ -211,124 +212,156 @@ impl Session { .await } - /// Accept a new unidirectional stream. See [`crate::quic::driver::connection::Connection::accept_uni`]. + /// Accept the next unidirectional stream after its WebTransport header. pub async fn accept_uni(&self) -> Result { + kio::wait(|waiter| self.poll_accept_uni(waiter)).await + } + + /// Accept the next bidirectional stream after its WebTransport header. + pub async fn accept_bi(&self) -> Result<(SendStream, RecvStream), SessionError> { + kio::wait(|waiter| self.poll_accept_bi(waiter)).await + } + + /// Open a unidirectional stream scheduled by `options`. + pub async fn open_uni(&self, options: send::Options<'_>) -> Result { + kio::wait(|waiter| self.poll_open_uni(waiter, options)).await + } + + /// Open a bidirectional stream scheduled by `options`. + pub async fn open_bi(&self, options: send::Options<'_>) -> Result<(SendStream, RecvStream), SessionError> { + kio::wait(|waiter| self.poll_open_bi(waiter, options)).await + } + + /// Poll for the next unidirectional stream after its WebTransport header. + pub fn poll_accept_uni(&self, waiter: &Waiter) -> Poll> { if let Some(accept) = &self.accept { - // `kio::wait` owns the waiter, so dropping this future - a `timeout` that - // expires, say - also drops its registration in `SessionAccept`. - kio::wait(|waiter| poll_accept_uni_shared(accept, waiter)) - .await - .map_err(|e| self.map_error(e)) + poll_accept_uni_shared(accept, waiter).map_err(|e| self.map_error(e)) } else { - let recv = self.conn.accept_uni().await.map_err(|e| self.map_error(e))?; - Ok(RecvStream::new(recv, self.close.clone())) + self.conn + .poll_accept_uni(waiter) + .map_ok(|recv| RecvStream::new(recv, self.close.clone())) + .map_err(|e| self.map_error(e)) } } - /// Accept a new bidirectional stream. See [`crate::quic::driver::connection::Connection::accept_bi`]. - pub async fn accept_bi(&self) -> Result<(SendStream, RecvStream), SessionError> { + /// Poll for the next bidirectional stream after its WebTransport header. + pub fn poll_accept_bi(&self, waiter: &Waiter) -> Poll> { if let Some(accept) = &self.accept { - kio::wait(|waiter| poll_accept_bi_shared(accept, waiter)) - .await - .map_err(|e| self.map_error(e)) + poll_accept_bi_shared(accept, waiter).map_err(|e| self.map_error(e)) } else { - let (send, recv) = self.conn.accept_bi().await.map_err(|e| self.map_error(e))?; - Ok(( - SendStream::new(send, self.close.clone(), None), - RecvStream::new(recv, self.close.clone()), - )) + self.conn + .poll_accept_bi(waiter) + .map_ok(|(send, recv)| { + ( + SendStream::new(send, self.close.clone(), None), + RecvStream::new(recv, self.close.clone()), + ) + }) + .map_err(|e| self.map_error(e)) } } - /// Open a new unidirectional stream, its sending half scheduled by `options`. - /// See [`crate::quic::driver::connection::Connection::open_uni`]. - pub async fn open_uni(&self, options: send::Options<'_>) -> Result { - // Write the stream header ungrouped at the highest priority, before scheduling the stream. - // Otherwise the application could write data with lower priority than the header, resulting - // in queuing. Also the header is very important for determining the session ID - // without reliable reset. - let header = send::Options { - group: None, - priority: i64::MAX, - }; - let mut send = self.conn.open_uni(header).await.map_err(|e| self.map_error(e))?; - Self::write_full(&mut send, &self.header_uni) - .await - .map_err(|e| self.map_error(e))?; - - send.set_priority(options.priority); - if let Some(group) = options.group { - send.set_group(Some(group)); - } - let header = send.offset(); - Ok(SendStream::new(send, self.close.clone(), Some(header))) + /// Poll to open a unidirectional stream and write its WebTransport header. + pub fn poll_open_uni(&self, waiter: &Waiter, options: send::Options<'_>) -> Poll> { + self.opening.poll(waiter, |opening, cx| { + if opening.uni.is_none() { + let send = std::task::ready!(self.conn.poll_open_uni( + waiter, + send::Options { + group: None, + priority: i64::MAX + } + )) + .map_err(|e| self.map_error(e))?; + opening.uni = Some((send, self.header_uni.clone())); + } + let (send, header) = opening.uni.as_mut().unwrap(); + let result = std::task::ready!(poll_header(send, cx, header)); + let (mut send, _) = opening.uni.take().unwrap(); + result.map_err(|e| self.map_error(e))?; + send.set_priority(options.priority); + send.set_group(options.group); + let header = self.session_id.map(|_| send.offset()); + Poll::Ready(Ok(SendStream::new(send, self.close.clone(), header))) + }) } - /// Open a new bidirectional stream, its sending half scheduled by `options`. - /// See [`crate::quic::driver::connection::Connection::open_bi`]. - pub async fn open_bi(&self, options: send::Options<'_>) -> Result<(SendStream, RecvStream), SessionError> { - // Write the stream header ungrouped at the highest priority, before scheduling the stream. - // Otherwise the application could write data with lower priority than the header, resulting - // in queuing. Also the header is very important for determining the session ID - // without reliable reset. - let header = send::Options { - group: None, - priority: i64::MAX, - }; - let (mut send, recv) = self.conn.open_bi(header).await.map_err(|e| self.map_error(e))?; - Self::write_full(&mut send, &self.header_bi) - .await - .map_err(|e| self.map_error(e))?; + /// Poll to open a bidirectional stream and write its WebTransport header. + pub fn poll_open_bi( + &self, + waiter: &Waiter, + options: send::Options<'_>, + ) -> Poll> { + self.poll_open_pair(waiter, options, false) + .map_ok(|pair| pair.expect("ordinary opens wait for credit")) + } - send.set_priority(options.priority); - if let Some(group) = options.group { - send.set_group(Some(group)); - } - let header = send.offset(); - Ok(( - SendStream::new(send, self.close.clone(), Some(header)), - RecvStream::new(recv, self.close.clone()), - )) + /// Open a request without waiting for stream credit; pending only while writing its header. + pub fn poll_open_request( + &self, + waiter: &Waiter, + options: send::Options<'_>, + ) -> Poll, SessionError>> { + self.poll_open_pair(waiter, options, true) } - /// Open a request without waiting for QUIC stream credit, then write its session header. - pub(crate) async fn open_request(&self) -> Result, SessionError> { - let Some((mut send, recv)) = self.conn.open_request().map_err(|err| self.map_error(err))? else { - return Ok(None); - }; - send.set_priority(i64::MAX); - Self::write_full(&mut send, &self.header_bi) - .await - .map_err(|err| self.map_error(err))?; - let header = send.offset(); - Ok(Some(( - SendStream::new(send, self.close.clone(), Some(header)), - RecvStream::new(recv, self.close.clone()), - ))) + fn poll_open_pair( + &self, + waiter: &Waiter, + options: send::Options<'_>, + request: bool, + ) -> Poll, SessionError>> { + self.opening.poll(waiter, |opening, cx| { + let slot = if request { &mut opening.request } else { &mut opening.bi }; + if slot.is_none() { + let (mut send, recv) = if request { + let Some(pair) = self.conn.open_request().map_err(|e| self.map_error(e))? else { + return Poll::Ready(Ok(None)); + }; + pair + } else { + std::task::ready!(self.conn.poll_open_bi( + waiter, + send::Options { + group: None, + priority: i64::MAX + } + )) + .map_err(|e| self.map_error(e))? + }; + send.set_priority(i64::MAX); + *slot = Some((send, recv, self.header_bi.clone())); + } + let (send, _, header) = slot.as_mut().unwrap(); + let result = std::task::ready!(poll_header(send, cx, header)); + let (mut send, recv, _) = slot.take().unwrap(); + result.map_err(|e| self.map_error(e))?; + send.set_priority(options.priority); + send.set_group(options.group); + let header = self.session_id.map(|_| send.offset()); + Poll::Ready(Ok(Some(( + SendStream::new(send, self.close.clone(), header), + RecvStream::new(recv, self.close.clone()), + )))) + }) } - /// Asynchronously receives an application datagram from the remote peer. - /// - /// This method is used to receive an application datagram sent by the remote - /// peer over the connection. - /// It waits for a datagram to become available and returns the received bytes. + /// Receive the next datagram with its WebTransport session header removed. pub async fn read_datagram(&self) -> Result { - let mut datagram = self.conn.read_datagram().await.map_err(|e| self.map_error(e))?; + kio::wait(|waiter| self.poll_recv_datagram(waiter)).await + } + /// Poll for a datagram with its WebTransport session header removed. + pub fn poll_recv_datagram(&self, waiter: &Waiter) -> Poll> { + let mut datagram = std::task::ready!(self.conn.poll_recv_datagram(waiter)).map_err(|e| self.map_error(e))?; let mut cursor = Cursor::new(&datagram); - if let Some(session_id) = self.session_id { - // We have to check and strip the session ID from the datagram. - let actual_id = VarInt::decode(&mut cursor).map_err(|_| WebTransportError::UnknownSession)?; - if actual_id != session_id { - return Err(WebTransportError::UnknownSession.into()); + let actual = VarInt::decode(&mut cursor).map_err(|_| WebTransportError::UnknownSession)?; + if actual != session_id { + return Poll::Ready(Err(WebTransportError::UnknownSession.into())); } } - - // Return the datagram without the session ID. - let datagram = datagram.split_off(cursor.position() as usize); - - Ok(datagram) + Poll::Ready(Ok(datagram.split_off(cursor.position() as usize))) } /// Sends an application datagram to the remote peer, scheduled by `options`. @@ -337,10 +370,10 @@ impl Session { /// The data must be smaller than [`max_datagram_size`](Self::max_datagram_size). /// See [`Connection::send_datagram`](crate::quic::driver::connection::Connection::send_datagram) for how /// `options` schedule it. - pub fn send_datagram(&self, data: Bytes, options: send::Options<'_>) -> Result<(), SessionError> { + pub fn send_datagram(&self, data: impl Buf, options: send::Options<'_>) -> Result<(), SessionError> { // Prepend the header naming the session, which is empty for a raw QUIC session. The // payload is chained rather than copied. - let datagram = (&self.header_datagram[..]).chain(data); + let datagram = self.header_datagram.clone().chain(data); self.conn .send_datagram(datagram, options) .map_err(|e| self.map_error(e))?; @@ -464,7 +497,7 @@ impl Session { // A queued write is not delivery: tearing down QUIC before the DATA and FIN // are acknowledged can discard the capsule during loss recovery. Transport // failure (including endpoint shutdown) also completes this wait. - match send.stopped().await { + match send.await { Ok(None) => {} Ok(Some(code)) => { tracing::debug!(?code, "peer stopped the CONNECT stream during close"); @@ -494,7 +527,7 @@ impl Session { /// connection to shut down, ensuring the `CloseWebTransportSession` capsule has /// been delivered. Use [`close_reason()`](Self::close_reason) for a non-blocking check. pub async fn closed(&self) -> SessionError { - self.map_error(self.conn.closed().await) + kio::wait(|waiter| self.poll_closed(waiter)).await } /// Return why the session was closed, or None if it's not closed. See @@ -508,14 +541,6 @@ impl Session { self.close.map(e.into()) } - async fn write_full(send: &mut crate::quic::driver::send::Stream, buf: &[u8]) -> Result<(), SessionError> { - match send.write_all(buf).await { - Ok(_) => Ok(()), - Err(crate::quic::driver::send::Error::Connection(err)) => Err(err.into()), - Err(err) => Err(WebTransportError::WriteError(err).into()), - } - } - /// Create a new session from a raw QUIC connection. /// /// This is used to pretend like a QUIC connection is a WebTransport session, @@ -532,6 +557,7 @@ impl Session { .map(Arc::from); Self { alpn, + opening: Default::default(), conn, session_id: None, header_uni: Default::default(), @@ -570,18 +596,37 @@ impl Session { } } + /// Poll until the session's connection has closed. + pub fn poll_closed(&self, waiter: &Waiter) -> Poll { + self.conn.poll_closed(waiter).map(|e| self.map_error(e)) + } + + /// The QUIC connection carrying this session. + pub fn connection(&self) -> &crate::quic::driver::connection::Connection { + &self.conn + } + /// Return connection-level statistics. - pub fn stats(&self) -> SessionStats { - SessionStats(self.conn.stats()) + pub fn stats(&self) -> crate::quic::driver::connection::Stats { + self.conn.stats() } } -impl Deref for Session { - type Target = crate::quic::driver::connection::Connection; +#[derive(Default)] +struct Opening { + uni: Option<(send::Stream, Bytes)>, + bi: Option<(send::Stream, crate::quic::driver::recv::Stream, Bytes)>, + request: Option<(send::Stream, crate::quic::driver::recv::Stream, Bytes)>, +} - fn deref(&self) -> &Self::Target { - &self.conn +fn poll_header(send: &mut send::Stream, cx: &mut Context<'_>, header: &mut Bytes) -> Poll> { + while header.has_remaining() { + std::task::ready!(send.poll_write_buf(cx, header)).map_err(|err| match err { + crate::quic::driver::send::Error::Connection(err) => SessionError::from(err), + err => WebTransportError::WriteError(err).into(), + })?; } + Poll::Ready(Ok(())) } impl fmt::Debug for Session { @@ -656,15 +701,6 @@ fn poll_accept_bi_shared( result } -// Type aliases just so clippy doesn't complain about the complexity. -type AcceptUni = - dyn Stream> + Send; -type AcceptBi = dyn Stream< - Item = Result< - (crate::quic::driver::send::Stream, crate::quic::driver::recv::Stream), - crate::quic::driver::connection::Error, - >, - > + Send; /// A unidirectional stream's header: its type, the session a WebTransport stream names, and the /// stream positioned after it. pub(crate) type UniHeader = (StreamUni, Option, crate::quic::driver::recv::Stream); @@ -675,7 +711,7 @@ type PendingBi = dyn Future< > + Send; // Logic just for accepting streams, which is annoying because of the stream header. -pub struct SessionAccept { +struct SessionAccept { session_id: VarInt, // Shared close reason for propagation to accepted streams. @@ -686,10 +722,11 @@ pub struct SessionAccept { qpack_encoder: Option, qpack_decoder: Option, - accept_uni: Pin>, + conn: crate::quic::driver::connection::Connection, + uni_park: kio::Park, + bi_park: kio::Park, // Pending WebTransport headers must be decoded before returning this error. uni_end: Option, - accept_bi: Pin>, // Keep track of work being done to read/write the WebTransport stream header. pending_uni: FuturesUnordered>>, @@ -719,15 +756,6 @@ impl SessionAccept { close: Arc, early: Vec>>, ) -> Self { - // Create a stream that just outputs new streams, so it's easy to call from poll. - let accept_uni = Box::pin(futures::stream::unfold(conn.clone(), |conn| async { - Some((conn.accept_uni().await, conn)) - })); - - let accept_bi = Box::pin(futures::stream::unfold(conn, |conn| async { - Some((conn.accept_bi().await, conn)) - })); - let bi_waiters = Fan::default(); let uni_waiters = Fan::default(); let bi_waker = bi_waiters.waker(); @@ -740,9 +768,10 @@ impl SessionAccept { qpack_decoder: None, qpack_encoder: None, - accept_uni, + conn, + uni_park: Default::default(), + bi_park: Default::default(), uni_end: None, - accept_bi, pending_uni: early.into_iter().collect(), pending_bi: FuturesUnordered::new(), @@ -768,7 +797,7 @@ impl SessionAccept { // Poll-based because we accept and decode streams in parallel. In async land this // would be a `tokio::JoinSet`, but that needs a runtime; `FuturesUnordered` is // runtime-agnostic. - pub fn poll_accept_uni(&mut self, waiter: &Waiter) -> Poll> { + fn poll_accept_uni(&mut self, waiter: &Waiter) -> Poll> { // Register before polling, not on the way out: the shared waker can fire from the // layer below at any point here, and a wake that lands before the caller is on // the list would be lost. @@ -780,7 +809,7 @@ impl SessionAccept { loop { // Accept any new streams. if self.uni_end.is_none() - && let Poll::Ready(Some(res)) = self.accept_uni.poll_next_unpin(cx) + && let Poll::Ready(res) = self.conn.poll_accept_uni(self.uni_park.hold(cx)) { // Start decoding the header and add the future to the list of pending streams. match res { @@ -831,7 +860,7 @@ impl SessionAccept { /// The same contract as [`poll_accept_uni`](Self::poll_accept_uni): the `waiter` /// registration is weak and owned by the caller, and a `Ready` is what the other /// parked accepters need to be woken for. - pub fn poll_accept_bi(&mut self, waiter: &Waiter) -> Poll> { + fn poll_accept_bi(&mut self, waiter: &Waiter) -> Poll> { // Register before polling; see `poll_accept_uni`. self.bi_waiters.register(waiter); @@ -840,7 +869,7 @@ impl SessionAccept { loop { // Accept any new streams. - if let Poll::Ready(Some(res)) = self.accept_bi.poll_next_unpin(cx) { + if let Poll::Ready(res) = self.conn.poll_accept_bi(self.bi_park.hold(cx)) { // Start decoding the header and add the future to the list of pending streams. let (send, recv) = match res { Ok(pair) => pair, @@ -934,15 +963,11 @@ pub(crate) fn log_header_error(err: SessionError, direction: &'static str) { } } -pub struct SessionStats(crate::quic::driver::connection::Stats); +struct Metrics(crate::quic::driver::connection::Stats); -impl web_transport_trait::Stats for SessionStats { - fn bytes_sent(&self) -> Option { - Some(self.0.udp_tx.bytes) - } - - fn bytes_received(&self) -> Option { - Some(self.0.udp_rx.bytes) +impl moq_net::transport::Metrics for Metrics { + fn streams_evicted(&self) -> Option { + Some(self.0.credit.evicted) } fn bytes_lost(&self) -> Option { @@ -971,57 +996,108 @@ impl web_transport_trait::Stats for SessionStats { } } -impl web_transport_trait::Session for Session { +impl moq_net::transport::poll::Session for Session { type SendStream = SendStream; type RecvStream = RecvStream; type Error = SessionError; - - async fn accept_uni(&self) -> Result { - Self::accept_uni(self).await + fn poll_accept_uni(&self, waiter: &Waiter) -> Poll> { + Self::poll_accept_uni(self, waiter) } - - async fn accept_bi(&self) -> Result<(Self::SendStream, Self::RecvStream), Self::Error> { - Self::accept_bi(self).await + fn poll_accept_bi(&self, waiter: &Waiter) -> Poll> { + Self::poll_accept_bi(self, waiter) } - - async fn open_bi(&self) -> Result<(Self::SendStream, Self::RecvStream), Self::Error> { - Self::open_bi(self, send::Options::default()).await + fn poll_open_uni( + &self, + waiter: &Waiter, + options: moq_net::transport::poll::SendOptions<'_, send::Group>, + ) -> Poll> { + Self::poll_open_uni( + self, + waiter, + send::Options { + group: options.group, + priority: options.priority, + }, + ) } - - async fn open_uni(&self) -> Result { - Self::open_uni(self, send::Options::default()).await + fn poll_open_bi( + &self, + waiter: &Waiter, + options: moq_net::transport::poll::SendOptions<'_, send::Group>, + ) -> Poll> { + Self::poll_open_bi( + self, + waiter, + send::Options { + group: options.group, + priority: options.priority, + }, + ) } - - fn close(&self, code: u32, reason: &str) { - Self::close(self, code, reason); + fn poll_open_request( + &self, + waiter: &Waiter, + options: moq_net::transport::poll::SendOptions<'_, send::Group>, + ) -> Poll, SessionError>> { + Self::poll_open_request( + self, + waiter, + send::Options { + group: options.group, + priority: options.priority, + }, + ) } - - async fn closed(&self) -> Self::Error { - Self::closed(self).await + fn send_group(&self, priority: i64) -> Option { + Some(Self::send_group(self, priority)) } - - fn send_datagram(&self, data: Bytes) -> Result<(), Self::Error> { - Self::send_datagram(self, data, send::Options::default()) + fn send_datagram( + &self, + payload: Bytes, + options: moq_net::transport::poll::SendOptions<'_, send::Group>, + ) -> Result<(), SessionError> { + Self::send_datagram( + self, + payload, + send::Options { + group: options.group, + priority: options.priority, + }, + ) } - - async fn recv_datagram(&self) -> Result { - Self::read_datagram(self).await + fn poll_recv_datagram(&self, waiter: &Waiter) -> Poll> { + Self::poll_recv_datagram(self, waiter) } - fn max_datagram_size(&self) -> usize { Self::max_datagram_size(self) } - fn protocol(&self) -> Option<&str> { Self::protocol(self) } - - #[expect( - refining_impl_trait, - reason = "returns the concrete stats type rather than the trait's opaque one" - )] - fn stats(&self) -> SessionStats { - Self::stats(self) + fn address(&self) -> moq_net::transport::Address { + if self.session_id.is_some() { + moq_net::transport::Address::Uri + } else { + moq_net::transport::Address::Setup + } + } + fn close(&self, code: u32, reason: &str) { + Self::close(self, code, reason); + } + fn poll_closed(&self, waiter: &Waiter) -> Poll { + Self::poll_closed(self, waiter) + } + fn metrics(&self) -> impl moq_net::transport::Metrics { + Metrics(self.conn.stats()) + } + fn wire(&self) -> Option { + Some(self.conn.wire()) + } + fn probe(&self) -> Option { + self.conn.probe() + } + fn receive_window(&self) -> Option { + Some(self.conn.receive_window()) } } @@ -1179,16 +1255,46 @@ mod tests { (client_conn, server_conn, [client, server], gate) } + /// An empty FIN needs no read; payload remains readable before the close watch ends. + #[tokio::test] + async fn clean_fin_ends_close_watch() { + for framed in [false, true] { + for payload in [Bytes::new(), Bytes::from(vec![0xa5; 64 * 1024 + 1])] { + let (client, server, _endpoints) = pair().await; + let (client, server) = if framed { + let (client, server) = tokio::join!( + Session::connect(client, url::Url::parse("https://localhost/").unwrap()), + async { crate::quic::web::Request::accept(server).await.unwrap().ok().await }, + ); + (client.unwrap(), server.unwrap()) + } else { + (Session::raw(client), Session::raw(server)) + }; + let mut send = client.open_uni(Default::default()).await.unwrap(); + send.write_all(payload.clone()).await.unwrap(); + let _finished = send.finish(); + let mut recv = server.accept_uni().await.unwrap(); + if !payload.is_empty() { + assert_eq!(recv.read_to_end(payload.len()).await.unwrap(), payload); + } + kio::clock::timeout(Duration::from_secs(2), recv.closed()) + .await + .unwrap_or_else(|err| panic!("FIN watch: framed={framed}, bytes={}, {err}", payload.len())) + .unwrap(); + assert!(recv.read_chunk(1).await.unwrap().is_none()); + } + } + } + #[tokio::test] async fn request_credit_refusal_does_not_leave_an_open() { - use moq_net::transport::poll::{SendStream as _, Session as _}; let mut transport = crate::quic::driver::transport::Config::default(); transport.limits.max_streams = std::num::NonZeroU64::new(1); let (client, server, _endpoints, _) = gated_pair_with_transport(transport, Default::default()).await; - let client = crate::transport::Session::new(Session::raw(client), moq_net::transport::Address::Setup); - let server = crate::transport::Session::new(Session::raw(server), moq_net::transport::Address::Setup); + let client = Session::raw(client); + let server = Session::raw(server); let waiter = &moq_net::kio::Waiter::noop(); - let Poll::Ready(Ok(Some((mut send, mut recv)))) = client.poll_open_request(waiter, Default::default()) else { + let Poll::Ready(Ok(Some((send, mut recv)))) = client.poll_open_request(waiter, Default::default()) else { panic!("initial request has credit"); }; for _ in 0..2 { @@ -1198,7 +1304,7 @@ mod tests { )); } let send = send.finish(); - let (mut peer_send, mut peer_recv) = server.accept_bi().await.unwrap(); + let (peer_send, mut peer_recv) = server.accept_bi().await.unwrap(); let peer_send = peer_send.finish(); assert!(peer_recv.read_chunk(1).await.unwrap().is_none()); assert!(recv.read_chunk(1).await.unwrap().is_none()); @@ -1212,7 +1318,6 @@ mod tests { #[tokio::test] async fn request_header_send_pressure_keeps_its_admitted_stream() { - use moq_net::transport::poll::Session as _; let mut server_transport = crate::quic::driver::transport::Config::default(); server_transport.limits.max_streams = std::num::NonZeroU64::new(1); let mut client_transport = crate::quic::driver::transport::Config::default(); @@ -1220,8 +1325,8 @@ mod tests { client_transport.limits.send_window = std::num::NonZeroU64::new(1); let (client, _server, _endpoints, _) = gated_pair_with_transport(server_transport, client_transport).await; let mut web = Session::raw(client.clone()); - web.header_bi = vec![0x41, 0]; - let session = crate::transport::Session::new(web, moq_net::transport::Address::Setup); + web.header_bi = Bytes::from_static(&[0x41, 0]); + let session = web; let waiter = &moq_net::kio::Waiter::noop(); // The only stream credit is consumed, but the header cannot fully enter the send window. assert!(session.poll_open_request(waiter, Default::default()).is_pending()); @@ -1396,7 +1501,7 @@ mod tests { let (client, server) = (client.unwrap(), server.unwrap()); client.open_uni(Default::default()).await.unwrap().reset(7); - let (mut send, _recv) = client.open_bi(Default::default()).await.unwrap(); + let (send, _recv) = client.open_bi(Default::default()).await.unwrap(); send.reset(8); let read = async { @@ -1409,8 +1514,8 @@ mod tests { }; // Without the header the server never surfaces the streams, so bound the wait. let (uni, bi) = kio::clock::timeout(Duration::from_secs(10), read).await.unwrap(); - assert!(matches!(uni, crate::quic::web::ReadError::Reset(7)), "{uni:?}"); - assert!(matches!(bi, crate::quic::web::ReadError::Reset(8)), "{bi:?}"); + assert!(matches!(uni, crate::quic::web::recv::Error::Reset(7)), "{uni:?}"); + assert!(matches!(bi, crate::quic::web::recv::Error::Reset(8)), "{bi:?}"); } /// A stream dropped before `finish` is reset with WebTransport code 0, still after its @@ -1429,7 +1534,7 @@ mod tests { drop(uni); let (mut send, mut client_bi) = client.open_bi(Default::default()).await.unwrap(); send.write_all(&b"request"[..]).await.unwrap(); - send.finish().unwrap(); + drop(send.finish()); let read = async { let mut uni = server.accept_uni().await.unwrap(); @@ -1447,8 +1552,8 @@ mod tests { }; // Without the header the server never surfaces the stream, so bound the wait. let (uni, bi) = kio::clock::timeout(Duration::from_secs(10), read).await.unwrap(); - assert!(matches!(uni, Err(crate::quic::web::ReadError::Reset(0))), "{uni:?}"); - assert!(matches!(bi, Err(crate::quic::web::ReadError::Reset(0))), "{bi:?}"); + assert!(matches!(uni, Err(crate::quic::web::recv::Error::Reset(0))), "{uni:?}"); + assert!(matches!(bi, Err(crate::quic::web::recv::Error::Reset(0))), "{bi:?}"); } /// A stream dropped before its end is stopped with WebTransport code 0, not the driver's raw @@ -1486,8 +1591,8 @@ mod tests { }) .await .unwrap(); - assert!(matches!(uni, crate::quic::web::WriteError::Stopped(0)), "{uni:?}"); - assert!(matches!(bi, crate::quic::web::WriteError::Stopped(0)), "{bi:?}"); + assert!(matches!(uni, crate::quic::web::send::Error::Stopped(0)), "{uni:?}"); + assert!(matches!(bi, crate::quic::web::send::Error::Stopped(0)), "{bi:?}"); } /// A stream that names another session is dropped, and the session's own stream after it @@ -1545,7 +1650,7 @@ mod tests { let mut send = client.open_uni(Default::default()).await.unwrap(); send.write_all(&b"after"[..]).await.unwrap(); - send.finish().unwrap(); + drop(send.finish()); let mut recv = server.accept_uni().await.unwrap(); assert_eq!(recv.read_to_end(64).await.unwrap(), b"after"); diff --git a/rs/moq-tokio/src/quic/web/settings.rs b/rs/moq-tokio/src/quic/web/settings.rs index 70f5d7d6f9b..4a084201dec 100644 --- a/rs/moq-tokio/src/quic/web/settings.rs +++ b/rs/moq-tokio/src/quic/web/settings.rs @@ -71,15 +71,12 @@ impl Settings { async fn accept( conn: &crate::quic::driver::connection::Connection, ) -> Result<(crate::quic::driver::recv::Stream, Vec>>), SettingsError> { - let mut accept = std::pin::pin!(futures::stream::unfold(conn.clone(), |conn| async { - Some((conn.accept_uni().await, conn)) - })); let mut pending = FuturesUnordered::>>::new(); let mut early = Vec::>>::new(); let mut recv = loop { tokio::select! { - Some(recv) = accept.next() => pending.push(Box::pin(read_uni_header(recv?))), + recv = kio::wait(|waiter| conn.poll_accept_uni(waiter)) => pending.push(Box::pin(read_uni_header(recv?))), Some(header) = pending.next() => match header { Ok((StreamUni::CONTROL, _, recv)) => break recv, Ok(header) => early.push(Box::pin(std::future::ready(Ok(header)))), diff --git a/rs/moq-tokio/src/server.rs b/rs/moq-tokio/src/server.rs index 452a91b8c68..d9a523ba828 100644 --- a/rs/moq-tokio/src/server.rs +++ b/rs/moq-tokio/src/server.rs @@ -723,9 +723,7 @@ impl Server { // (like the stream bindings). let Accepted { session, url, identity, authority, mut link } = within(deadline, crate::quic::accept(_conn, alpns)).await?; link.local = local; - let address = match url { Some(_) => moq_net::transport::Address::Uri, None => moq_net::transport::Address::Setup }; - let quic = Some((*session).clone()); - let session = crate::transport::Session::new(session, address); + let quic = Some(session.connection().clone()); let (request, deadline) = accept_request(&setup.server, session, deadline).await?; // Only WebTransport carries a request URL; raw QUIC puts the path in the SETUP. let transport = match url { Some(_) => Transport::WebTransport, None => Transport::Quic }; @@ -2402,144 +2400,144 @@ mod tests { struct ScriptedError; #[cfg(feature = "websocket")] - impl web_transport_trait::Error for ScriptedError { + impl moq_net::transport::Error for ScriptedError { fn session_error(&self) -> Option<(u32, String)> { None } } - /// Records every write, or accepts none at all when `stalled`. + /// Records every write, or accepts none when stalled. #[cfg(feature = "websocket")] struct ScriptedSend { written: std::sync::Arc>>, stalled: bool, } - #[cfg(feature = "websocket")] - impl web_transport_trait::SendStream for ScriptedSend { + impl moq_net::transport::poll::SendStream for ScriptedSend { type Error = ScriptedError; - - async fn write(&mut self, buf: &[u8]) -> Result { + type Group = moq_net::transport::poll::Ungrouped; + fn poll_write(&mut self, _: &kio::Waiter, buf: &[u8]) -> std::task::Poll> { if self.stalled { - std::future::pending::<()>().await; + return std::task::Poll::Pending; } self.written.lock().unwrap().extend_from_slice(buf); - Ok(buf.len()) - } - - fn set_priority(&mut self, _order: i32) {} - - fn finish(&mut self) -> Result<(), Self::Error> { - Ok(()) - } - - fn reset(&mut self, _code: u32) {} - - async fn closed(&mut self) -> Result<(), Self::Error> { - std::future::pending().await + std::task::Poll::Ready(Ok(buf.len())) } - } - - #[cfg(feature = "websocket")] - impl crate::transport::GroupedSend for ScriptedSend { - type Group = moq_net::transport::poll::Ungrouped; - + fn set_priority(&mut self, _: i64) {} fn set_group(&mut self, group: &Self::Group) { match *group {} } - } - - #[cfg(feature = "websocket")] - impl crate::transport::Grouped for Scripted { - type Recv = moq_net::transport::poll::Buffered>; - - fn recv(stream: crate::transport::RecvStream) -> Self::Recv { - moq_net::transport::poll::Buffered::new(stream) + fn finish(&mut self) -> Result<(), ScriptedError> { + Ok(()) + } + fn reset(&mut self, _: u32) {} + fn poll_closed(&self, _: &kio::Waiter) -> std::task::Poll> { + std::task::Poll::Pending } } - /// Delivers `data` once, then nothing more. + /// Delivers data once, then parks. #[cfg(feature = "websocket")] struct ScriptedRecv { data: Option, } - #[cfg(feature = "websocket")] - impl web_transport_trait::RecvStream for ScriptedRecv { + impl moq_net::transport::poll::Chunks for ScriptedRecv { type Error = ScriptedError; - - async fn read(&mut self, dst: &mut [u8]) -> Result, Self::Error> { - let Some(mut data) = self.data.take() else { - return std::future::pending().await; + fn poll_read_chunk( + &mut self, + _: &kio::Waiter, + max: usize, + ) -> std::task::Poll, ScriptedError>> { + let Some(data) = &mut self.data else { + return std::task::Poll::Pending; }; - let n = dst.len().min(data.len()); - dst[..n].copy_from_slice(&data.split_to(n)); - self.data = (!data.is_empty()).then_some(data); - Ok(Some(n)) - } - - fn stop(&mut self, _code: u32) {} - - async fn closed(&mut self) -> Result<(), Self::Error> { - std::future::pending().await + let chunk = data.split_to(max.min(data.len())); + if data.is_empty() { + self.data = None; + } + std::task::Poll::Ready(Ok(Some(chunk))) } + fn stop(&mut self, _: u32) {} } #[cfg(feature = "websocket")] - impl web_transport_trait::Session for Scripted { + impl moq_net::transport::poll::Session for Scripted { type SendStream = ScriptedSend; - type RecvStream = ScriptedRecv; + type RecvStream = moq_net::transport::poll::Buffered; type Error = ScriptedError; - - async fn accept_uni(&self) -> Result { - std::future::pending().await + fn poll_accept_uni(&self, _: &kio::Waiter) -> std::task::Poll> { + std::task::Poll::Pending } - - async fn accept_bi(&self) -> Result<(Self::SendStream, Self::RecvStream), Self::Error> { + fn poll_accept_bi( + &self, + _: &kio::Waiter, + ) -> std::task::Poll, ScriptedError>> { let Some(setup) = self.setup.clone() else { - return std::future::pending().await; - }; - let send = ScriptedSend { - written: Default::default(), - stalled: true, + return std::task::Poll::Pending; }; - Ok((send, ScriptedRecv { data: Some(setup) })) + std::task::Poll::Ready(Ok(( + ScriptedSend { + written: Default::default(), + stalled: true, + }, + moq_net::transport::poll::Buffered::new(ScriptedRecv { data: Some(setup) }), + ))) + } + fn poll_open_uni( + &self, + _: &kio::Waiter, + _: moq_net::transport::poll::SendOptions<'_, moq_net::transport::poll::Ungrouped>, + ) -> std::task::Poll> { + std::task::Poll::Pending + } + fn poll_open_bi( + &self, + _: &kio::Waiter, + _: moq_net::transport::poll::SendOptions<'_, moq_net::transport::poll::Ungrouped>, + ) -> std::task::Poll, ScriptedError>> { + std::task::Poll::Ready(Ok(( + ScriptedSend { + written: self.written.clone(), + stalled: false, + }, + moq_net::transport::poll::Buffered::new(ScriptedRecv { data: None }), + ))) } - - async fn open_bi(&self) -> Result<(Self::SendStream, Self::RecvStream), Self::Error> { - let send = ScriptedSend { - written: self.written.clone(), - stalled: false, - }; - Ok((send, ScriptedRecv { data: None })) + fn poll_open_request( + &self, + waiter: &kio::Waiter, + options: moq_net::transport::poll::SendOptions<'_, moq_net::transport::poll::Ungrouped>, + ) -> std::task::Poll>, ScriptedError>> { + self.poll_open_bi(waiter, options).map_ok(Some) } - - async fn open_uni(&self) -> Result { - std::future::pending().await + fn send_group(&self, _: i64) -> Option { + None } - - fn send_datagram(&self, _payload: bytes::Bytes) -> Result<(), Self::Error> { + fn send_datagram( + &self, + _: bytes::Bytes, + _: moq_net::transport::poll::SendOptions<'_, moq_net::transport::poll::Ungrouped>, + ) -> Result<(), ScriptedError> { Ok(()) } - - async fn recv_datagram(&self) -> Result { - std::future::pending().await + fn poll_recv_datagram(&self, _: &kio::Waiter) -> std::task::Poll> { + std::task::Poll::Pending } - fn max_datagram_size(&self) -> usize { 0 } - fn protocol(&self) -> Option<&str> { Some(self.alpn) } - - fn close(&self, code: u32, _reason: &str) { + fn address(&self) -> moq_net::transport::Address { + moq_net::transport::Address::Uri + } + fn close(&self, code: u32, _: &str) { self.closed.lock().unwrap().get_or_insert(code); } - - async fn closed(&self) -> Self::Error { - std::future::pending().await + fn poll_closed(&self, _: &kio::Waiter) -> std::task::Poll { + std::task::Poll::Pending } } @@ -2567,13 +2565,7 @@ mod tests { timeout: Some(timeout), }; let start = kio::clock::now(); - let Err(err) = accept_request( - &setup.server, - crate::transport::Session::new(peer.clone(), moq_net::transport::Address::Uri), - setup.deadline(), - ) - .await - else { + let Err(err) = accept_request(&setup.server, peer.clone(), setup.deadline()).await else { panic!("a peer that never sent SETUP was accepted"); }; assert!(matches!(err, Error::MoqNet(moq_net::Error::Timeout)), "{err}"); @@ -2595,7 +2587,7 @@ mod tests { // then waits on a reply that never comes. let recorder = Scripted::new("moql"); let client = tokio::spawn({ - let recorder = crate::transport::Session::new(recorder.clone(), moq_net::transport::Address::Uri); + let recorder = recorder.clone(); async move { let _ = moq_net::Client::new() .with_versions("moq-lite-02".parse::().unwrap().into()) @@ -2617,13 +2609,7 @@ mod tests { timeout: Some(timeout), }; let start = kio::clock::now(); - let Ok((request, deadline)) = accept_request( - &setup.server, - crate::transport::Session::new(peer.clone(), moq_net::transport::Address::Uri), - setup.deadline(), - ) - .await - else { + let Ok((request, deadline)) = accept_request(&setup.server, peer.clone(), setup.deadline()).await else { panic!("a readable SETUP was refused"); }; assert_eq!(start.elapsed(), std::time::Duration::ZERO); diff --git a/rs/moq-tokio/src/transport.rs b/rs/moq-tokio/src/transport.rs index b2c712afe5c..4387b0c2260 100644 --- a/rs/moq-tokio/src/transport.rs +++ b/rs/moq-tokio/src/transport.rs @@ -1,25 +1,4 @@ -//! Adapts the async transport interface to the poll one moq-net requires. -//! -//! moq-net accepts its own `moq_net::transport::poll` traits. -//! Native QUIC, which still offers only the async interface, is wrapped -//! in [`Session`] here, at the edge where async already lives. The wrapping -//! costs one allocation per operation and one copy per write, and its -//! closed-watch emulation is weaker than a native implementation (see -//! [`Session`]), so a backend that implements the poll interface itself is -//! adapted to moq-net at this boundary. -//! -//! It also names the [`Transport`] a session runs on, for the accept and dial sides alike. - -use std::task::{Context, Poll, ready}; - -use bytes::Bytes; -use futures::FutureExt; -use web_transport_trait::poll as wt_poll; - -mod grouped; -mod owned; -pub(crate) use grouped::{Grouped, GroupedSend}; -pub use owned::Error; +//! The network transport carrying a MoQ session. /// The network transport carrying a MoQ session, on either side of it. #[non_exhaustive] @@ -58,1562 +37,3 @@ impl std::fmt::Display for Transport { f.write_str(self.as_str()) } } - -/// A stored in-flight operation future. Native transports are Send, so the -/// plain boxed flavor suffices. -pub(crate) type OpBox = futures::future::BoxFuture<'static, T>; - -/// A stream op that hands the stream back with its result, or `None` if interrupted. -type StreamOp = OpBox<(S, Option>)>; - -/// Adapts this crate's async QUIC backend to the poll interface moq-net requires. -/// -/// Only that backend fits: the bound goes through a sealed trait, so another -/// [`web_transport_trait::Session`] cannot be wrapped. A custom transport implements -/// moq-net's `transport::poll` traits instead. -/// -/// Each pending session operation is stored as a boxed future built from a clone -/// of the underlying session; stream operations temporarily move the stream into -/// the future and take it back when the operation settles. Two costs follow: one -/// allocation per operation, and one copy per write (a poll write borrows its -/// buffer, an async write future cannot). Prefer a transport's native poll -/// implementation when it has one. -/// -/// A few contract loosenings, all inherent to wrapping futures: -/// -/// - A write must be retried with the same bytes until it resolves. The adapter -/// copies the buffer it was first given; a caller that swaps buffers while the -/// write is pending would corrupt the stream. Every caller in this crate -/// retries with the same buffer. -/// - `stop`, `reset`, and `finish` during an in-flight operation are deferred -/// until that operation settles. If the stream is dropped before then, the -/// pending operation is finished on the tokio runtime and the deferred code -/// applied after it, so the peer still sees the real reason; only without a -/// runtime does the underlying transport's drop behavior (a reset or stop -/// with code 0) stand in. -/// - Stream `poll_closed` watches are emulated rather than delegated: the -/// underlying async `closed()` future would own the stream for its whole -/// lifetime, deadlocking any later read or write (the async trait's -/// `closed(&mut self)` cannot be stored as a `'static` watch without taking -/// the stream with it). A receive stream reports closed through its reads -/// (FIN drained, or a read error), buffering bounded read-ahead so a FIN -/// behind undelivered payload still resolves the watch. At the cap it reads -/// no further: it watches the underlying `closed()` for a reset or -/// connection close, interruptibly so any read reclaims the stream, and -/// resumes reading ahead once the caller's reads drain below the cap. -/// Send-side watches are interruptible, so an idle watch can observe peer -/// cancellation while later writes reclaim the stream. -pub struct Session { - session: S, - address: moq_net::transport::Address, - accept_uni: OpSlot>, - accept_bi: OpSlot>, - open_uni: OpSlot>, - open_bi: OpSlot>, - request_bi: OpSlot>, - recv_datagram: OpSlot>, - closed: OpSlot, - // Wire counters for a backend that has none and reports bytes only in its stats. - polled_wire: moq_net::transport::Wire, -} - -/// The stream pair an async-interface accept or open resolves to. -type BiResult = Result< - ( - ::SendStream, - ::RecvStream, - ), - ::Error, ->; - -/// A request's stream pair, or a refusal before consuming stream credit. -type RequestResult = Result< - Option<( - ::SendStream, - ::RecvStream, - )>, - ::Error, ->; - -/// A stored in-flight operation, `None` when idle, which every task polling it shares. -pub(crate) type OpSlot = moq_net::kio::Bridge>>; - -/// Lazily start and poll the operation in `slot`, clearing it when it resolves. -/// -/// Whichever caller polls it once it resolves takes the output; the rest start the next. -pub(crate) fn poll_op(slot: &OpSlot, waiter: &moq_net::kio::Waiter, start: impl FnOnce() -> OpBox) -> Poll { - slot.poll(waiter, |slot, cx| { - let fut = slot.get_or_insert_with(start); - let output = ready!(fut.as_mut().poll(cx)); - *slot = None; - Poll::Ready(output) - }) -} - -impl Session { - /// Wrap an async-interface transport session. - pub fn new(session: S, address: moq_net::transport::Address) -> Self { - Self { - session, - address, - accept_uni: OpSlot::default(), - accept_bi: OpSlot::default(), - open_uni: OpSlot::default(), - open_bi: OpSlot::default(), - request_bi: OpSlot::default(), - recv_datagram: OpSlot::default(), - closed: OpSlot::default(), - polled_wire: moq_net::transport::Wire::new(), - } - } -} - -// Manual impl: a clone starts with no in-flight operations, since each handle -// owns its own progress under the poll contract. -impl Clone for Session { - fn clone(&self) -> Self { - Self { - polled_wire: self.polled_wire.clone(), - ..Self::new(self.session.clone(), self.address) - } - } -} - -/// The send half produced by [`Session`]: an async-interface stream adapted to the -/// poll interface. See [`Session`] for the deferred `finish`/`reset` behavior. -pub struct SendStream { - // Same never-locked Mutex as [`Session`]'s slots: `get_mut` is free on an - // exclusive borrow, and the wrapper keeps the boxed future from stripping - // `Sync` off the containing session types. - state: std::sync::Mutex>>, - // Deferred actions, applied when the in-flight operation settles. - group: Option, - priority: Option, - finish: bool, - reset: Option, - /// Bytes already transmitted but not yet reported to the caller: a write the - /// stored future finished (possibly inside - /// [`poll_closed`](wt_poll::SendStream::poll_closed)) while the caller held a - /// `Pending`. Later `poll_write` calls reconcile against the buffer actually - /// presented (the poll contract allows a shorter retry), reporting and - /// consuming a prefix per call, never writing these bytes a second time. - completed: Bytes, - /// Cancels the in-flight write so a reset applies immediately instead of - /// waiting behind blocked I/O (whose partial progress a reset discards - /// anyway). Fired by [`reset`](wt_poll::SendStream::reset) and by [`Drop`], - /// and by [`set_priority`](wt_poll::SendStream::set_priority) during the - /// closed() watch, which holds the stream but is safe to restart. - interrupt: Option>, -} - -enum SendState { - Idle(S), - /// A write in flight; `chunk` is the copied bytes, kept (refcounted, no - /// extra copy) so a completion absorbed by `poll_closed` can verify the - /// retrying caller supplied the same bytes. A `None` result means the write - /// was interrupted by a reset (see [`SendStream::interrupt`]). - Writing { - fut: StreamOp, - chunk: Bytes, - }, - /// The closed() acknowledgement watch; a `None` result means it was - /// interrupted by a late reset or priority change (see [`SendStream::interrupt`]). - Closing(StreamOp), -} - -impl SendStream { - /// Wrap an async-interface send stream. - pub fn new(stream: S) -> Self { - Self { - state: std::sync::Mutex::new(Some(SendState::Idle(stream))), - group: None, - priority: None, - finish: false, - reset: None, - completed: Bytes::new(), - interrupt: None, - } - } - - /// Apply actions deferred while an operation was in flight. - fn settle(&mut self, stream: &mut S) { - if let Some(group) = self.group.take() { - stream.set_group(&group); - } - if let Some(priority) = self.priority.take() { - GroupedSend::set_priority(stream, priority); - } - if let Some(code) = self.reset.take() { - self.finish = false; - stream.reset(code); - } - if std::mem::take(&mut self.finish) { - // A deferred finish has nowhere to report to; its failure surfaces - // through closed() like any other terminal state. - let _ = stream.finish(); - } - } -} - -impl SendStream { - /// Set the full-width priority, deferred while an operation holds the stream. - fn prioritize(&mut self, priority: i64) { - match self.state.get_mut().unwrap().as_mut() { - Some(SendState::Idle(stream)) => GroupedSend::set_priority(stream, priority), - // A finished stream still retransmits under its priority until the FIN is - // acknowledged, so reclaim it from the watch rather than wait that out. - Some(SendState::Closing(_)) => { - self.priority = Some(priority); - self.reclaim(); - } - _ => self.priority = Some(priority), - } - } - - /// Schedule a newly opened stream by its options. - fn schedule(&mut self, options: moq_net::transport::poll::SendOptions<'_, S::Group>) { - self.prioritize(options.priority); - if let Some(group) = options.group { - self.join(group); - } - } - - /// Join `group`, deferred like [`prioritize`](Self::prioritize). - fn join(&mut self, group: &S::Group) { - match self.state.get_mut().unwrap().as_mut() { - Some(SendState::Idle(stream)) => stream.set_group(group), - Some(SendState::Closing(_)) => { - self.group = Some(group.clone()); - self.reclaim(); - } - _ => self.group = Some(group.clone()), - } - } - - /// Interrupt the closed() watch so a deferred action applies now. - fn reclaim(&mut self) { - if let Some(tx) = self.interrupt.take() { - let _ = tx.send(()); - } - } -} - -impl wt_poll::SendStream for SendStream { - type Error = S::Error; - - fn poll_write(&mut self, cx: &mut Context<'_>, buf: &[u8]) -> Poll> { - loop { - // Transmitted-but-unreported bytes from a completed write are - // reconciled against the buffer presented NOW: the poll contract - // allows a retry with a shorter buffer, so report (and consume) at - // most its length and keep the rest for the next call. - if !self.completed.is_empty() && !buf.is_empty() { - let n = buf.len().min(self.completed.len()); - let reported = self.completed.split_to(n); - // A hard assert, not a debug one: these bytes are already on the - // wire and the bridge cannot un-send them, so a caller continuing - // with different data would silently corrupt the stream. Failing - // loudly is the only honest option left. - assert_eq!( - &buf[..n], - &reported[..], - "poll_write must continue with the bytes the write bridge already transmitted" - ); - return Poll::Ready(Ok(n)); - } - match self.state.get_mut().unwrap().take().expect("in-flight") { - SendState::Idle(mut stream) => { - if buf.is_empty() { - *self.state.get_mut().unwrap() = Some(SendState::Idle(stream)); - return Poll::Ready(Ok(0)); - } - // write_chunk delivers the whole chunk, so the reported - // count is exact. The caller retries with the same bytes - // until this resolves (see [`Session`]). - let chunk = Bytes::copy_from_slice(buf); - let retained = chunk.clone(); - let (tx, rx) = futures::channel::oneshot::channel::<()>(); - self.interrupt = Some(tx); - let fut = async move { - let res = { - let mut op = std::pin::pin!(stream.write_chunk(chunk).fuse()); - let mut rx = rx.fuse(); - loop { - futures::select_biased! { - res = op => break Some(res), - cancel = rx => match cancel { - Ok(()) => break None, - // A dropped (unfired) sender must not interrupt; - // only an explicit reset does. The fused arm goes - // quiet and the write keeps driving. - Err(_) => continue, - }, - } - } - }; - (stream, res) - } - .boxed(); - *self.state.get_mut().unwrap() = Some(SendState::Writing { fut, chunk: retained }); - } - SendState::Writing { mut fut, chunk } => match fut.as_mut().poll(cx) { - Poll::Pending => { - *self.state.get_mut().unwrap() = Some(SendState::Writing { fut, chunk }); - return Poll::Pending; - } - Poll::Ready((mut stream, res)) => { - self.interrupt = None; - self.settle(&mut stream); - *self.state.get_mut().unwrap() = Some(SendState::Idle(stream)); - // An interrupted write was abandoned for a reset, which - // settle just applied; the next iteration's write reports - // the stream state. - if let Some(res) = res { - res?; - // Reported through the reconciliation at the top of the - // loop, which caps it at the presented buffer's length. - self.completed = chunk; - } - } - }, - SendState::Closing(mut fut) => { - if let Some(tx) = self.interrupt.take() { - let _ = tx.send(()); - } - match fut.as_mut().poll(cx) { - Poll::Pending => { - *self.state.get_mut().unwrap() = Some(SendState::Closing(fut)); - return Poll::Pending; - } - Poll::Ready((mut stream, _res)) => { - self.interrupt = None; - self.settle(&mut stream); - *self.state.get_mut().unwrap() = Some(SendState::Idle(stream)); - } - } - } - } - } - } - - fn set_priority(&mut self, order: i32) { - self.prioritize(order.into()); - } - - fn finish(&mut self) -> Result<(), Self::Error> { - match self.state.get_mut().unwrap().as_mut() { - Some(SendState::Idle(stream)) => stream.finish(), - _ => { - self.finish = true; - if matches!(self.state.get_mut().unwrap(), Some(SendState::Closing(_))) - && let Some(tx) = self.interrupt.take() - { - let _ = tx.send(()); - } - Ok(()) - } - } - } - - fn reset(&mut self, code: u32) { - match self.state.get_mut().unwrap().as_mut() { - Some(SendState::Idle(stream)) => stream.reset(code), - _ => { - self.reset = Some(code); - // A reset discards the in-flight write's progress anyway, so - // cancel it rather than letting blocked I/O delay the code. - if let Some(tx) = self.interrupt.take() { - let _ = tx.send(()); - } - } - } - } - - fn poll_closed(&mut self, cx: &mut Context<'_>) -> Poll> { - loop { - match self.state.get_mut().unwrap().take().expect("in-flight") { - SendState::Idle(mut stream) => { - self.settle(&mut stream); - // Interruptible like a write: a late reset (the group machines - // cancel a finished stream this way) must not wait for a FIN - // acknowledgement that may never come. - let (tx, rx) = futures::channel::oneshot::channel::<()>(); - self.interrupt = Some(tx); - let fut = async move { - let res = { - let mut op = std::pin::pin!(stream.closed().fuse()); - let mut rx = rx.fuse(); - loop { - futures::select_biased! { - res = op => break Some(res), - cancel = rx => match cancel { - Ok(()) => break None, - Err(_) => continue, - }, - } - } - }; - (stream, res) - } - .boxed(); - *self.state.get_mut().unwrap() = Some(SendState::Closing(fut)); - } - SendState::Writing { mut fut, chunk } => match fut.as_mut().poll(cx) { - Poll::Pending => { - *self.state.get_mut().unwrap() = Some(SendState::Writing { fut, chunk }); - return Poll::Pending; - } - Poll::Ready((mut stream, res)) => { - self.interrupt = None; - self.settle(&mut stream); - *self.state.get_mut().unwrap() = Some(SendState::Idle(stream)); - match res { - // A failed write is a closed stream; report it here. - Some(Err(err)) => return Poll::Ready(Err(err)), - // The caller of poll_write was told Pending and will - // retry; poll_write's reconciliation reports these bytes - // instead of writing them a second time. - Some(Ok(())) => self.completed = chunk, - // Interrupted for a reset, applied by settle above; the - // next iteration starts the real closed watch. - None => {} - } - } - }, - SendState::Closing(mut fut) => match fut.as_mut().poll(cx) { - Poll::Pending => { - *self.state.get_mut().unwrap() = Some(SendState::Closing(fut)); - return Poll::Pending; - } - Poll::Ready((mut stream, res)) => { - self.interrupt = None; - self.settle(&mut stream); - *self.state.get_mut().unwrap() = Some(SendState::Idle(stream)); - // An interrupted watch was abandoned for a late reset or - // priority change, which settle just applied; the next - // iteration watches again. - if let Some(res) = res { - return Poll::Ready(res); - } - } - }, - } - } - } -} - -impl Drop for SendStream { - fn drop(&mut self) { - match self.state.get_mut().unwrap().take() { - // Apply a deferred reset if the stream is idle. - Some(SendState::Idle(mut stream)) => { - if let Some(code) = self.reset.take() { - stream.reset(code); - } - } - // The stream lives inside the in-flight future; dropping it here - // would fire the backend's default drop behavior (a code-0 reset) - // and lose the deferred reset code or FIN the caller asked for. The - // peer uses that code to classify the abort (Old vs Cancel vs - // Evicted), so finish the operation on the runtime and apply the - // terminal action then. A deferred reset fired the interrupt when it - // was recorded, so the salvage task never waits on blocked I/O; a - // deferred finish lets the write complete first (the FIN follows the - // data). - Some(SendState::Writing { fut, .. }) => { - let reset = self.reset.take(); - let finish = std::mem::take(&mut self.finish); - if reset.is_some() || finish { - crate::runtime::detach(crate::panic::isolate(async move { - let (mut stream, _) = fut.await; - match reset { - Some(code) => stream.reset(code), - None => { - let _ = stream.finish(); - } - } - })); - } - } - // Both deferred actions fired the watch's interrupt when recorded, so - // the salvage task reclaims the stream without waiting on the peer. - Some(SendState::Closing(fut)) => { - let reset = self.reset.take(); - let finish = std::mem::take(&mut self.finish); - if reset.is_some() || finish { - crate::runtime::detach(crate::panic::isolate(async move { - let (mut stream, _) = fut.await; - match reset { - Some(code) => stream.reset(code), - None => { - let _ = stream.finish(); - } - } - })); - } - } - None => {} - } - } -} - -/// The receive half produced by [`Session`]: an async-interface stream adapted to -/// the poll interface. See [`Session`] for the deferred `stop` behavior. -pub struct RecvStream { - // See [`SendStream::state`] for why this Mutex exists; it is never locked. - state: std::sync::Mutex>>, - /// Bytes already read from the transport but not yet handed to the caller. - buffer: Bytes, - /// Read-ahead pulled in by the closed-watch while payload sat undelivered, so - /// a FIN right behind buffered data still resolves the watch. Drained by the - /// read methods before the transport is polled again; bounded by - /// [`READ_AHEAD_CAP`]. - queued: std::collections::VecDeque, - /// Total bytes across `queued`, so the cap check is O(1). - queued_len: usize, - /// The peer finished the stream. - fin: bool, - /// A deferred STOP_SENDING, applied when the in-flight operation settles. - stop: Option, - /// Cancels the in-flight read so a stop applies immediately instead of - /// waiting behind blocked I/O. Fired by [`stop`](wt_poll::RecvStream::stop) - /// and by [`Drop`], and by a read that needs the stream back from the - /// closed-watch. - interrupt: Option>, - /// The in-flight operation is the closed-watch's `closed()` at the cap, - /// which any read interrupts rather than waits on. - watching: bool, - /// The closed-watch parked at the cap, woken once reads drain below it. - parked: Option, -} - -/// How much the closed-watch may read ahead of the caller before it parks and -/// waits for the caller to drain. Bounds memory against a peer that floods a -/// stream nobody is reading. -const READ_AHEAD_CAP: usize = 64 * 1024; - -enum RecvState { - Idle(S), - /// A read in flight, or the closed-watch's `closed()` reporting its end - /// as `Ok(None)`; a `None` result means it was interrupted (see - /// [`RecvStream::interrupt`]). - Reading(StreamOp, S::Error>), -} - -impl RecvStream { - /// Wrap an async-interface receive stream. - pub fn new(stream: S) -> Self { - Self { - state: std::sync::Mutex::new(Some(RecvState::Idle(stream))), - buffer: Bytes::new(), - queued: std::collections::VecDeque::new(), - queued_len: 0, - fin: false, - stop: None, - interrupt: None, - watching: false, - parked: None, - } - } - - /// The wrapped stream, before any read: the backend leases it natively instead. - #[cfg(feature = "quic")] - fn into_inner(mut self) -> S { - match self.state.get_mut().unwrap().take() { - Some(RecvState::Idle(stream)) => stream, - _ => unreachable!("unwrapped before any read"), - } - } - - /// Pop read-ahead into `self.buffer` if the head is empty. - fn unqueue(&mut self) { - if self.buffer.is_empty() - && let Some(next) = self.queued.pop_front() - { - self.queued_len -= next.len(); - self.buffer = next; - } - } - - /// Wake a closed-watch parked at the cap once reads drain below it. - fn drained(&mut self) { - if self.buffer.len() + self.queued_len < READ_AHEAD_CAP - && let Some(parked) = self.parked.take() - { - parked.wake(); - } - } - - /// Start an interruptible operation on the idle `stream`: a read of up to - /// `max` bytes, or with `None` the closed-watch's `closed()`, whose end - /// resolves as `Ok(None)`. - fn start(&mut self, mut stream: S, max: Option) { - let (tx, rx) = futures::channel::oneshot::channel::<()>(); - self.interrupt = Some(tx); - self.watching = max.is_none(); - let fut = async move { - let res = { - let op = async { - match max { - Some(max) => stream.read_chunk(max).await, - None => stream.closed().await.map(|()| None), - } - }; - let mut op = std::pin::pin!(op.fuse()); - let mut rx = rx.fuse(); - loop { - futures::select_biased! { - res = op => break Some(res), - cancel = rx => match cancel { - Ok(()) => break None, - // A dropped (unfired) sender must not interrupt; - // only an explicit stop or a read does. The fused - // arm goes quiet and the operation keeps driving. - Err(_) => continue, - }, - } - } - }; - (stream, res) - } - .boxed(); - *self.state.get_mut().unwrap() = Some(RecvState::Reading(fut)); - } - - /// Read the next chunk from the transport, setting `self.fin` on the FIN. - fn poll_fill(&mut self, cx: &mut Context<'_>, max: usize) -> Poll, S::Error>> { - loop { - match self.state.get_mut().unwrap().take().expect("in-flight") { - RecvState::Idle(mut stream) => { - if let Some(code) = self.stop.take() { - stream.stop(code); - } - self.start(stream, Some(max)); - } - RecvState::Reading(fut) => { - // A read wants the stream back from the closed-watch, which - // would otherwise hold it until the stream ends. - let watching = self.watching; - if watching && let Some(tx) = self.interrupt.take() { - let _ = tx.send(()); - } - match ready!(self.poll_op(cx, fut)) { - // Interrupted, or the watch saw the stream end: the - // read that follows reports how. - None => continue, - Some(Ok(_)) if watching => continue, - Some(res) => return Poll::Ready(res), - } - } - } - } - } - - /// Poll the in-flight operation, settling it once it resolves; `None` means - /// it was interrupted, and the next operation reports the stream state. - fn poll_op( - &mut self, - cx: &mut Context<'_>, - mut fut: StreamOp, S::Error>, - ) -> Poll, S::Error>>> { - let Poll::Ready((mut stream, res)) = fut.as_mut().poll(cx) else { - *self.state.get_mut().unwrap() = Some(RecvState::Reading(fut)); - return Poll::Pending; - }; - self.interrupt = None; - let watched = std::mem::take(&mut self.watching); - // Apply a stop requested while the operation was in flight now, not on - // the next read: a caller that stops and never reads again must still - // send STOP_SENDING. - if let Some(code) = self.stop.take() { - stream.stop(code); - } - *self.state.get_mut().unwrap() = Some(RecvState::Idle(stream)); - // Only a read's end is the FIN: closed() also ends at a reset. - if !watched && let Some(Ok(None)) = &res { - self.fin = true; - } - Poll::Ready(res) - } -} - -impl wt_poll::RecvStream for RecvStream { - type Error = S::Error; - - fn poll_read(&mut self, cx: &mut Context<'_>, dst: &mut [u8]) -> Poll, Self::Error>> { - if dst.is_empty() { - return Poll::Ready(Ok(Some(0))); - } - - loop { - self.unqueue(); - if !self.buffer.is_empty() { - let n = dst.len().min(self.buffer.len()); - dst[..n].copy_from_slice(&self.buffer.split_to(n)); - self.drained(); - return Poll::Ready(Ok(Some(n))); - } - if self.fin { - return Poll::Ready(Ok(None)); - } - if let Some(bytes) = ready!(self.poll_fill(cx, dst.len()))? { - self.buffer = bytes; - } - } - } - - fn poll_read_chunk(&mut self, cx: &mut Context<'_>, max: usize) -> Poll, Self::Error>> { - if max == 0 { - return Poll::Ready(Ok(Some(Bytes::new()))); - } - - loop { - self.unqueue(); - if !self.buffer.is_empty() { - let n = max.min(self.buffer.len()); - let chunk = self.buffer.split_to(n); - self.drained(); - return Poll::Ready(Ok(Some(chunk))); - } - if self.fin { - return Poll::Ready(Ok(None)); - } - if let Some(bytes) = ready!(self.poll_fill(cx, max))? { - self.buffer = bytes; - } - } - } - - fn stop(&mut self, code: u32) { - match self.state.get_mut().unwrap().as_mut() { - Some(RecvState::Idle(stream)) => stream.stop(code), - _ => { - self.stop = Some(code); - // The caller is discarding the stream, so cancel the in-flight - // read rather than letting blocked I/O delay the stop. - if let Some(tx) = self.interrupt.take() { - let _ = tx.send(()); - } - } - } - } - - fn poll_closed(&mut self, cx: &mut Context<'_>) -> Poll> { - // Emulated with reads rather than held on the underlying closed(): that - // future owns the stream, so it is only watched at the read-ahead cap, and - // interruptibly, so any read reclaims the stream. Below the cap a FIN or - // reset surfaces through reads; payload arriving before it is queued as - // read-ahead (up to READ_AHEAD_CAP) so a FIN right behind undelivered data - // still resolves the watch without an external read. - loop { - if self.fin { - return Poll::Ready(Ok(())); - } - if self.buffer.len() + self.queued_len >= READ_AHEAD_CAP { - // Read no further, but still see a reset or connection close - // behind the backlog, and resume reading ahead once the caller's - // reads drain below the cap. - self.parked = Some(cx.waker().clone()); - match self.state.get_mut().unwrap().take().expect("in-flight") { - RecvState::Idle(stream) => { - self.start(stream, None); - continue; - } - RecvState::Reading(fut) if self.watching => { - match ready!(self.poll_op(cx, fut)) { - Some(Err(err)) => return Poll::Ready(Err(err)), - // A reset ends closed() as well as the FIN, and after - // either no payload is left to wait on, so one read - // says which without growing the backlog. - Some(Ok(_)) => { - let Some(RecvState::Idle(stream)) = self.state.get_mut().unwrap().take() else { - unreachable!("a settled watch leaves the stream idle"); - }; - self.start(stream, Some(8 * 1024)); - } - // Interrupted by a stop; watch again. - None => {} - } - continue; - } - // A read in flight, ours or the caller's: settle it below. - reading => *self.state.get_mut().unwrap() = Some(reading), - } - } - match ready!(self.poll_fill(cx, 8 * 1024)) { - Ok(Some(bytes)) => { - self.queued_len += bytes.len(); - self.queued.push_back(bytes); - } - Ok(None) => {} - // A read error is also a closed stream. - Err(err) => return Poll::Ready(Err(err)), - } - } - } -} - -impl Drop for RecvStream { - fn drop(&mut self) { - match self.state.get_mut().unwrap().take() { - // Apply a deferred stop if the stream is idle. - Some(RecvState::Idle(mut stream)) => { - if let Some(code) = self.stop.take() { - stream.stop(code); - } - } - // The stream lives inside the in-flight read; dropping it here would - // fire the backend's default drop behavior (a code-0 stop) and lose - // the deferred code the peer classifies the abort by. Finish the - // read on the runtime and stop with the real code then; a deferred - // stop fired the interrupt when it was recorded, so the task never - // waits on blocked I/O. - Some(RecvState::Reading(fut)) => { - if let Some(code) = self.stop.take() { - crate::runtime::detach(crate::panic::isolate(async move { - let (mut stream, _) = fut.await; - stream.stop(code); - })); - } - } - None => {} - } - } -} - -#[cfg(feature = "quic")] -impl Grouped for crate::quic::web::Session { - fn open_request(&self) -> impl std::future::Future> + Send { - Self::open_request(self) - } - // Leased in the QUIC stack, so the receive window bounds what an incomplete message holds. - type Recv = crate::quic::web::RecvStream; - - fn recv(stream: RecvStream) -> Self::Recv { - stream.into_inner() - } - - fn wire(&self) -> Option { - Some(crate::quic::driver::connection::Connection::wire(self)) - } - - fn probe(&self) -> Option { - crate::quic::driver::connection::Connection::probe(self) - } - - fn streams_evicted(&self) -> Option { - Some(crate::quic::driver::connection::Connection::stats(self).credit.evicted) - } - - fn receive_window(&self) -> Option { - Some(crate::quic::driver::connection::Connection::receive_window(self)) - } - - fn send_group(&self, priority: i64) -> Option { - Some(Self::send_group(self, priority)) - } - - fn send_datagram( - &self, - payload: Bytes, - options: moq_net::transport::poll::SendOptions<'_, crate::quic::driver::send::Group>, - ) -> Result<(), Self::Error> { - let options = crate::quic::driver::send::Options { - group: options.group, - priority: options.priority, - }; - Self::send_datagram(self, payload, options) - } -} - -#[cfg(feature = "quic")] -impl GroupedSend for crate::quic::web::SendStream { - type Group = crate::quic::driver::send::Group; - - fn set_group(&mut self, group: &Self::Group) { - Self::set_group(self, Some(group)); - } - - fn set_priority(&mut self, priority: i64) { - crate::quic::web::SendStream::set_priority(self, priority); - } -} - -#[cfg(feature = "quic")] -impl moq_net::transport::poll::SendGroup for crate::quic::driver::send::Group { - fn set_priority(&mut self, priority: i64) { - Self::set_priority(self, priority) - } -} - -#[cfg(test)] -mod tests { - use std::sync::{ - Arc, Mutex, - atomic::{AtomicBool, Ordering}, - }; - - use super::*; - use web_transport_trait::poll::{RecvStream as _, SendStream as _}; - - #[derive(Debug, Clone, Default, PartialEq)] - struct FakeError; - - impl std::fmt::Display for FakeError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "fake error") - } - } - - impl std::error::Error for FakeError {} - - impl web_transport_trait::Error for FakeError { - fn session_error(&self) -> Option<(u32, String)> { - None - } - } - - /// An async-interface send stream whose writes can be blocked, so a test can - /// hold an operation in flight inside the adapter. - #[derive(Clone, Default)] - struct FakeSend { - writes: Arc>>, - blocked: Arc, - priorities: Arc>>, - groups: Arc>>, - finished: Arc, - resets: Arc>>, - /// The peer never acknowledges the FIN: closed() stays pending forever. - never_ack: Arc, - stopped: Arc, - } - - impl web_transport_trait::SendStream for FakeSend { - type Error = FakeError; - - async fn write(&mut self, buf: &[u8]) -> Result { - std::future::poll_fn(|cx| { - if self.blocked.load(Ordering::SeqCst) { - cx.waker().wake_by_ref(); - return Poll::Pending; - } - Poll::Ready(()) - }) - .await; - self.writes.lock().unwrap().extend_from_slice(buf); - Ok(buf.len()) - } - - fn set_priority(&mut self, order: i32) { - self.priorities.lock().unwrap().push(order); - } - - fn finish(&mut self) -> Result<(), Self::Error> { - self.finished.store(true, Ordering::SeqCst); - Ok(()) - } - - fn reset(&mut self, code: u32) { - self.resets.lock().unwrap().push(code); - } - - async fn closed(&mut self) -> Result<(), Self::Error> { - std::future::poll_fn(|_| { - if self.stopped.load(Ordering::SeqCst) { - return Poll::Ready(Err(FakeError)); - } - match self.finished.load(Ordering::SeqCst) && !self.never_ack.load(Ordering::SeqCst) { - true => Poll::Ready(Ok(())), - false => Poll::Pending, - } - }) - .await - } - } - - /// A send group that only names itself, so a test can see which one a stream joined. - #[derive(Clone)] - struct FakeGroup(u32); - - impl moq_net::transport::poll::SendGroup for FakeGroup { - fn set_priority(&mut self, _: i64) {} - } - - impl GroupedSend for FakeSend { - type Group = FakeGroup; - - fn set_group(&mut self, group: &FakeGroup) { - self.groups.lock().unwrap().push(group.0); - } - } - - /// An async-interface receive stream replaying canned chunks, then EOF. - #[derive(Default)] - struct FakeRecv { - chunks: std::collections::VecDeque, - stops: Arc>>, - /// Reads park while set, so a test can hold one in flight in the adapter. - blocked: Arc, - /// The peer reset the stream, discarding whatever was not yet read. - reset: Arc, - } - - impl web_transport_trait::RecvStream for FakeRecv { - type Error = FakeError; - - async fn read(&mut self, dst: &mut [u8]) -> Result, Self::Error> { - std::future::poll_fn(|cx| { - if self.blocked.load(Ordering::SeqCst) { - cx.waker().wake_by_ref(); - return Poll::Pending; - } - Poll::Ready(()) - }) - .await; - if self.reset.load(Ordering::SeqCst) { - return Err(FakeError); - } - let Some(chunk) = self.chunks.front_mut() else { - return Ok(None); - }; - let n = dst.len().min(chunk.len()); - dst[..n].copy_from_slice(&chunk.split_to(n)); - if chunk.is_empty() { - self.chunks.pop_front(); - } - Ok(Some(n)) - } - - fn stop(&mut self, code: u32) { - self.stops.lock().unwrap().push(code); - } - - /// Ends at a reset or at the FIN once every chunk was read, and like - /// moq-tokio's own streams does not say which. - async fn closed(&mut self) -> Result<(), Self::Error> { - std::future::poll_fn(|_| match self.reset.load(Ordering::SeqCst) || self.chunks.is_empty() { - true => Poll::Ready(Ok(())), - false => Poll::Pending, - }) - .await - } - } - - fn cx() -> Context<'static> { - Context::from_waker(std::task::Waker::noop()) - } - - // The regression behind goaway_gates_new_subscribes_moq_lite_04: a dropped - // closed() watch must not hold the stream hostage, so a later write still - // goes through. - #[test] - fn send_closed_watch_does_not_block_writes() { - let fake = FakeSend::default(); - let mut send = SendStream::new(fake.clone()); - let mut cx = cx(); - - // An idle closed watch stays pending until the peer cancels. - assert!(send.poll_closed(&mut cx).is_pending()); - - // The write proceeds; the old ownership-transfer watch deadlocked here. - assert_eq!(send.poll_write(&mut cx, b"hello"), Poll::Ready(Ok(5))); - assert_eq!(fake.writes.lock().unwrap().as_slice(), b"hello"); - } - - #[test] - fn send_closed_observes_stop_while_idle() { - let fake = FakeSend::default(); - let mut send = SendStream::new(fake.clone()); - let mut cx = cx(); - assert!(send.poll_closed(&mut cx).is_pending()); - fake.stopped.store(true, Ordering::SeqCst); - assert!(matches!(send.poll_closed(&mut cx), Poll::Ready(Err(_)))); - } - - // After finish() the real closed() watch runs and resolves. - #[test] - fn send_closed_resolves_after_finish() { - let fake = FakeSend::default(); - let mut send = SendStream::new(fake.clone()); - let mut cx = cx(); - - assert_eq!(send.poll_write(&mut cx, b"bye"), Poll::Ready(Ok(3))); - send.finish().unwrap(); - assert_eq!(send.poll_closed(&mut cx), Poll::Ready(Ok(()))); - } - - // Actions issued while a write is in flight apply when it settles, and the - // settled write reports its original length. - #[test] - fn send_defers_actions_across_an_inflight_write() { - let fake = FakeSend::default(); - fake.blocked.store(true, Ordering::SeqCst); - let mut send = SendStream::new(fake.clone()); - let mut cx = cx(); - - assert!(send.poll_write(&mut cx, b"queued").is_pending()); - send.set_priority(7); - assert!(fake.priorities.lock().unwrap().is_empty(), "deferred while in flight"); - - fake.blocked.store(false, Ordering::SeqCst); - assert_eq!(send.poll_write(&mut cx, b"queued"), Poll::Ready(Ok(6))); - assert_eq!(fake.priorities.lock().unwrap().as_slice(), &[7]); - } - - // A group joined while a write holds the stream applies when it settles, and one - // joined during the FIN watch reclaims the stream, which still retransmits. - #[test] - fn send_defers_a_group_across_an_inflight_write() { - let fake = FakeSend::default(); - fake.blocked.store(true, Ordering::SeqCst); - let mut send = SendStream::new(fake.clone()); - let mut cx = cx(); - - assert!(send.poll_write(&mut cx, b"queued").is_pending()); - send.join(&FakeGroup(1)); - send.prioritize(i64::MAX); - assert!(fake.groups.lock().unwrap().is_empty(), "deferred while in flight"); - - fake.blocked.store(false, Ordering::SeqCst); - assert_eq!(send.poll_write(&mut cx, b"queued"), Poll::Ready(Ok(6))); - assert_eq!(fake.groups.lock().unwrap().as_slice(), &[1]); - assert_eq!(fake.priorities.lock().unwrap().as_slice(), &[i32::MAX], "saturated"); - - fake.never_ack.store(true, Ordering::SeqCst); - send.finish().unwrap(); - assert!(send.poll_closed(&mut cx).is_pending()); - send.join(&FakeGroup(2)); - assert!(send.poll_closed(&mut cx).is_pending()); - assert_eq!(fake.groups.lock().unwrap().as_slice(), &[1, 2]); - } - - // The cluster-failover corruption: a write held Pending by backpressure is - // driven to completion by the peer-close check (poll_closed). Its result must - // reach the retrying poll_write; discarding it made the retry put the same - // bytes on the wire twice, which a peer decodes as a phantom frame. - #[test] - fn a_write_completed_by_poll_closed_is_not_duplicated() { - let fake = FakeSend::default(); - fake.blocked.store(true, Ordering::SeqCst); - let mut send = SendStream::new(fake.clone()); - let mut cx = cx(); - - // Backpressure: the write parks inside the adapter. - assert!(send.poll_write(&mut cx, b"header").is_pending()); - - // The pressure lifts; the driver's next pass checks for a peer close - // first, which drives the stored write to completion. - fake.blocked.store(false, Ordering::SeqCst); - assert!(send.poll_closed(&mut cx).is_pending(), "not terminal yet"); - - // The retry reports the completed write instead of writing again. - assert_eq!(send.poll_write(&mut cx, b"header"), Poll::Ready(Ok(6))); - assert_eq!( - fake.writes.lock().unwrap().as_slice(), - b"header", - "the bytes must reach the stream exactly once" - ); - } - - // A late reset must not wait behind a FIN acknowledgement that never comes: - // the group machines finish a stream and keep watching precisely so a late - // cancel can still reset it. - #[test] - fn a_late_reset_interrupts_the_ack_watch() { - let fake = FakeSend::default(); - fake.never_ack.store(true, Ordering::SeqCst); - let mut send = SendStream::new(fake.clone()); - let mut cx = cx(); - - assert_eq!(send.poll_write(&mut cx, b"bye"), Poll::Ready(Ok(3))); - send.finish().unwrap(); - // The ack never arrives; the watch parks. - assert!(send.poll_closed(&mut cx).is_pending()); - - // The late cancel: the reset applies without waiting for the ack, and - // the watch then resolves against the reset stream. - send.reset(9); - let closed = send.poll_closed(&mut cx); - assert_eq!(fake.resets.lock().unwrap().as_slice(), &[9]); - // The re-armed watch on the reset stream is allowed to stay pending in - // this fake (it never acks); what matters is the reset went out. - let _ = closed; - } - - // A finished stream still retransmits under its priority, so a reorder must - // reach it during the ack watch, and the watch must still resolve afterwards. - #[test] - fn a_priority_change_reaches_the_ack_watch() { - let fake = FakeSend::default(); - fake.never_ack.store(true, Ordering::SeqCst); - let mut send = SendStream::new(fake.clone()); - let mut cx = cx(); - - assert_eq!(send.poll_write(&mut cx, b"queued"), Poll::Ready(Ok(6))); - send.finish().unwrap(); - assert!(send.poll_closed(&mut cx).is_pending()); - - send.set_priority(7); - assert!(send.poll_closed(&mut cx).is_pending()); - assert_eq!(fake.priorities.lock().unwrap().as_slice(), &[7]); - - // The ack arrives for the restarted watch. - fake.never_ack.store(false, Ordering::SeqCst); - send.set_priority(8); - assert_eq!(send.poll_closed(&mut cx), Poll::Ready(Ok(()))); - assert_eq!(fake.priorities.lock().unwrap().as_slice(), &[7, 8]); - assert!(fake.resets.lock().unwrap().is_empty()); - assert_eq!(fake.writes.lock().unwrap().as_slice(), b"queued"); - } - - // The poll contract allows retrying a pending write with a SHORTER buffer; - // transmitted-but-unreported bytes reconcile against the buffer presented, - // never reporting more than its length and never re-transmitting. - #[test] - fn a_completed_write_reconciles_against_a_shorter_retry() { - let fake = FakeSend::default(); - fake.blocked.store(true, Ordering::SeqCst); - let mut send = SendStream::new(fake.clone()); - let mut cx = cx(); - - assert!(send.poll_write(&mut cx, b"header").is_pending()); - fake.blocked.store(false, Ordering::SeqCst); - assert!(send.poll_closed(&mut cx).is_pending()); - - // The caller shrank its buffer: only that much is reported per call. - assert_eq!(send.poll_write(&mut cx, b"hea"), Poll::Ready(Ok(3))); - assert_eq!(send.poll_write(&mut cx, b"der"), Poll::Ready(Ok(3))); - assert_eq!( - fake.writes.lock().unwrap().as_slice(), - b"header", - "the bytes must reach the stream exactly once" - ); - - // Fully reconciled: the next write is a fresh operation. - assert_eq!(send.poll_write(&mut cx, b"!"), Poll::Ready(Ok(1))); - assert_eq!(fake.writes.lock().unwrap().as_slice(), b"header!"); - } - - // A larger chunk than the destination is buffered and served across reads. - #[test] - fn recv_buffers_the_excess() { - let stops = Arc::new(Mutex::new(Vec::new())); - let fake = FakeRecv { - chunks: [Bytes::from_static(b"abcdef")].into_iter().collect(), - stops: stops.clone(), - ..Default::default() - }; - let mut recv = RecvStream::new(fake); - let mut cx = cx(); - - let mut dst = [0u8; 4]; - assert_eq!(recv.poll_read(&mut cx, &mut dst), Poll::Ready(Ok(Some(4)))); - assert_eq!(&dst, b"abcd"); - - // The retry may shrink; the buffered tail serves it. - let mut dst = [0u8; 2]; - assert_eq!(recv.poll_read(&mut cx, &mut dst), Poll::Ready(Ok(Some(2)))); - assert_eq!(&dst, b"ef"); - - // EOF, and the closed watch resolves once drained. - assert_eq!(recv.poll_read(&mut cx, &mut dst), Poll::Ready(Ok(None))); - assert_eq!(recv.poll_closed(&mut cx), Poll::Ready(Ok(()))); - } - - // stop() while idle applies immediately. - #[test] - fn recv_stop_applies_when_idle() { - let stops = Arc::new(Mutex::new(Vec::new())); - let fake = FakeRecv { - chunks: Default::default(), - stops: stops.clone(), - ..Default::default() - }; - let mut recv = RecvStream::new(fake); - - recv.stop(42); - assert_eq!(stops.lock().unwrap().as_slice(), &[42]); - } - - // A stop issued while a read is in flight goes out when that read settles, - // not on a later read that may never come. - #[test] - fn recv_stop_applies_when_the_read_settles() { - let stops = Arc::new(Mutex::new(Vec::new())); - let blocked = Arc::new(AtomicBool::new(true)); - let fake = FakeRecv { - chunks: [Bytes::from_static(b"data")].into_iter().collect(), - stops: stops.clone(), - blocked: blocked.clone(), - ..Default::default() - }; - let mut recv = RecvStream::new(fake); - let mut cx = cx(); - - let mut dst = [0u8; 4]; - assert!(recv.poll_read(&mut cx, &mut dst).is_pending()); - recv.stop(7); - assert!(stops.lock().unwrap().is_empty(), "deferred while in flight"); - - // The read settles (here via a retry); the stop goes out with it. - blocked.store(false, Ordering::SeqCst); - assert_eq!(recv.poll_read(&mut cx, &mut dst), Poll::Ready(Ok(Some(4)))); - assert_eq!(stops.lock().unwrap().as_slice(), &[7]); - } - - // A writer dropped while its write is still in flight must not lose the - // deferred reset code: the peer classifies the abort by it (Old vs Cancel), - // so the salvage task finishes the write and applies the real code. - #[tokio::test] - async fn a_deferred_reset_code_survives_a_drop_mid_write() { - let fake = FakeSend::default(); - fake.blocked.store(true, Ordering::SeqCst); - let mut send = SendStream::new(fake.clone()); - let mut cx = cx(); - - assert!(send.poll_write(&mut cx, b"payload").is_pending()); - send.reset(9); - drop(send); - - // The write stays blocked forever: the reset must not wait behind it. - kio::clock::timeout(std::time::Duration::from_secs(1), async { - while fake.resets.lock().unwrap().is_empty() { - tokio::task::yield_now().await; - } - }) - .await - .expect("the salvage task never applied the reset"); - assert_eq!(fake.resets.lock().unwrap().as_slice(), &[9]); - } - - // An idle closed watch owns the stream, so a FIN recorded against it and then - // dropped must still go out instead of the backend's drop reset. - #[tokio::test(start_paused = true)] - async fn a_deferred_finish_survives_a_drop_mid_watch() { - let fake = FakeSend::default(); - let mut send = SendStream::new(fake.clone()); - let mut cx = cx(); - - assert!(send.poll_closed(&mut cx).is_pending()); - send.finish().unwrap(); - drop(send); - - for _ in 0..10 { - tokio::task::yield_now().await; - } - assert!(fake.finished.load(Ordering::SeqCst), "the deferred FIN was lost"); - assert!(fake.resets.lock().unwrap().is_empty()); - } - - // The receive-side twin: a reader dropped mid-read still sends the deferred - // STOP_SENDING with the real code. - #[tokio::test] - async fn a_deferred_stop_code_survives_a_drop_mid_read() { - let stops = Arc::new(Mutex::new(Vec::new())); - let blocked = Arc::new(AtomicBool::new(true)); - let fake = FakeRecv { - chunks: [Bytes::from_static(b"data")].into_iter().collect(), - stops: stops.clone(), - blocked: blocked.clone(), - ..Default::default() - }; - let mut recv = RecvStream::new(fake); - let mut cx = cx(); - - let mut dst = [0u8; 4]; - assert!(recv.poll_read(&mut cx, &mut dst).is_pending()); - wt_poll::RecvStream::stop(&mut recv, 11); - drop(recv); - - // The read stays blocked forever: the stop must not wait behind it. - kio::clock::timeout(std::time::Duration::from_secs(1), async { - while stops.lock().unwrap().is_empty() { - tokio::task::yield_now().await; - } - }) - .await - .expect("the salvage task never applied the stop"); - assert_eq!(stops.lock().unwrap().as_slice(), &[11]); - } - - // A FIN sitting behind undelivered payload resolves the closed watch without - // an external read; the payload is still delivered afterward, in order. - #[test] - fn recv_closed_resolves_behind_buffered_data() { - let stops = Arc::new(Mutex::new(Vec::new())); - let fake = FakeRecv { - chunks: [Bytes::from_static(b"tail")].into_iter().collect(), - stops: stops.clone(), - ..Default::default() - }; - let mut recv = RecvStream::new(fake); - let mut cx = cx(); - - // The watch reads through the payload to the FIN. - assert_eq!(recv.poll_closed(&mut cx), Poll::Ready(Ok(()))); - - // Nothing was lost: the read-ahead serves the payload, then EOF. - let mut dst = [0u8; 4]; - assert_eq!(recv.poll_read(&mut cx, &mut dst), Poll::Ready(Ok(Some(4)))); - assert_eq!(&dst, b"tail"); - assert_eq!(recv.poll_read(&mut cx, &mut dst), Poll::Ready(Ok(None))); - } - - /// A waker counting its wakes, standing in for a task. - #[derive(Default)] - struct Wakes(std::sync::atomic::AtomicUsize); - - impl std::task::Wake for Wakes { - fn wake(self: Arc) { - self.0.fetch_add(1, Ordering::SeqCst); - } - } - - /// More payload than the read-ahead cap, in watch-sized chunks. - fn flood() -> std::collections::VecDeque { - (0..READ_AHEAD_CAP / (8 * 1024) + 1) - .map(|_| Bytes::from(vec![7u8; 8 * 1024])) - .collect() - } - - // A reset behind a full read-ahead still closes the watch, and the payload - // read before it is still delivered. - #[test] - fn recv_closed_sees_a_reset_at_the_cap() { - let reset = Arc::new(AtomicBool::new(false)); - let mut recv = RecvStream::new(FakeRecv { - chunks: flood(), - reset: reset.clone(), - ..Default::default() - }); - let mut cx = cx(); - - // The watch fills the read-ahead, then waits on the stream's end. - assert_eq!(recv.poll_closed(&mut cx), Poll::Pending); - reset.store(true, Ordering::SeqCst); - assert_eq!(recv.poll_closed(&mut cx), Poll::Ready(Err(FakeError))); - - let mut read = 0; - while let Poll::Ready(Ok(Some(chunk))) = recv.poll_read_chunk(&mut cx, usize::MAX) { - read += chunk.len(); - } - assert_eq!(read, READ_AHEAD_CAP); - assert_eq!( - recv.poll_read_chunk(&mut cx, 1), - Poll::Ready(Err(FakeError)), - "a reset, not the FIN" - ); - } - - // A watch parked at the cap is woken by a read in another task draining - // below it, and then reads ahead to the FIN. - #[test] - fn recv_closed_resumes_when_a_read_drains_the_cap() { - let mut recv = RecvStream::new(FakeRecv { - chunks: flood(), - ..Default::default() - }); - let wakes = Arc::new(Wakes::default()); - let watcher = std::task::Waker::from(wakes.clone()); - let mut watch = Context::from_waker(&watcher); - let mut cx = cx(); - - assert_eq!(recv.poll_closed(&mut watch), Poll::Pending); - let woken = wakes.0.load(Ordering::SeqCst); - - // The reader drains one chunk, which wakes the watch. - let Poll::Ready(Ok(Some(chunk))) = recv.poll_read_chunk(&mut cx, 8 * 1024) else { - panic!("the read-ahead serves the reader"); - }; - assert_eq!(chunk.len(), 8 * 1024); - assert!( - wakes.0.load(Ordering::SeqCst) > woken, - "the draining read never woke the watch" - ); - - // The watch reclaims the stream and reads through to the FIN. - assert_eq!(recv.poll_closed(&mut watch), Poll::Ready(Ok(()))); - let mut read = chunk.len(); - while let Poll::Ready(Ok(Some(chunk))) = recv.poll_read_chunk(&mut cx, usize::MAX) { - read += chunk.len(); - } - assert_eq!(read, READ_AHEAD_CAP + 8 * 1024); - assert_eq!(recv.poll_read_chunk(&mut cx, 1), Poll::Ready(Ok(None))); - } - - /// An async-interface session that never accepts, opens, or closes. - #[derive(Clone)] - struct FakeSession; - - impl Grouped for FakeSession { - type Recv = moq_net::transport::poll::Buffered>; - - fn recv(stream: RecvStream) -> Self::Recv { - moq_net::transport::poll::Buffered::new(stream) - } - } - - impl web_transport_trait::Session for FakeSession { - type SendStream = FakeSend; - type RecvStream = FakeRecv; - type Error = FakeError; - - async fn accept_uni(&self) -> Result { - std::future::pending().await - } - - async fn accept_bi(&self) -> Result<(Self::SendStream, Self::RecvStream), Self::Error> { - std::future::pending().await - } - - async fn open_bi(&self) -> Result<(Self::SendStream, Self::RecvStream), Self::Error> { - std::future::pending().await - } - - async fn open_uni(&self) -> Result { - std::future::pending().await - } - - fn send_datagram(&self, _payload: Bytes) -> Result<(), Self::Error> { - Ok(()) - } - - async fn recv_datagram(&self) -> Result { - std::future::pending().await - } - - fn max_datagram_size(&self) -> usize { - 0 - } - - fn close(&self, _code: u32, _reason: &str) {} - - async fn closed(&self) -> Self::Error { - std::future::pending().await - } - } - - #[test] - fn owned_poll_adapter_forwards_writes_and_resets() { - use moq_net::transport::poll; - let fake = FakeSend::default(); - let mut send = owned::BridgedSend::new(fake.clone()); - let waiter = &moq_net::kio::Waiter::noop(); - assert!(matches!( - poll::SendStream::poll_write(&mut send, waiter, b"hello"), - Poll::Ready(Ok(5)) - )); - poll::SendStream::set_priority(&mut send, 7); - poll::SendStream::reset(&mut send, 0x33); - assert_eq!(fake.writes.lock().unwrap().as_slice(), b"hello"); - assert_eq!(fake.priorities.lock().unwrap().as_slice(), &[7]); - assert_eq!(fake.resets.lock().unwrap().as_slice(), &[0x33]); - for address in [moq_net::transport::Address::Uri, moq_net::transport::Address::Setup] { - let session = Session::new(FakeSession, address); - let cloned = session.clone(); - assert_eq!(poll::Session::address(&session), address); - assert_eq!(poll::Session::address(&cloned), address); - assert!(poll::Session::poll_accept_uni(&cloned, waiter).is_pending()); - } - } -} diff --git a/rs/moq-tokio/src/transport/grouped.rs b/rs/moq-tokio/src/transport/grouped.rs deleted file mode 100644 index 89cc0c54089..00000000000 --- a/rs/moq-tokio/src/transport/grouped.rs +++ /dev/null @@ -1,82 +0,0 @@ -//! Send-group, wire-counter, and probe hooks for the async backends. -//! -//! The traits are `pub` in this private module, so other crates can neither name nor -//! implement them: `pub(crate)` is refused (E0446) because the public adapter's -//! `type Group` projects through [`GroupedSend`]. - -use moq_net::transport::poll::SendOptions; - -/// A backend session whose send streams may join send groups, as native QUIC's do. -/// -/// The async interface has no groups, wire counters, or probe target, so moq-net's -/// [`send_group`](moq_net::transport::poll::Session::send_group), -/// [`wire`](moq_net::transport::poll::Session::wire), and -/// [`probe`](moq_net::transport::poll::Session::probe) reach a backend through this. A -/// backend without them implements it empty. -pub trait Grouped: web_transport_trait::Session { - /// The receive stream moq-net reads: the backend's own lease where it has one, or - /// [`Buffered`](moq_net::transport::poll::Buffered) reads otherwise. - type Recv: moq_net::transport::poll::RecvStream; - - /// Adapt one of the backend's receive streams, freshly accepted or opened. - fn recv(stream: super::RecvStream) -> Self::Recv; - - /// Open a request, waiting for stream credit like any open unless the backend can inspect it. - fn open_request(&self) -> impl std::future::Future> + Send { - async { self.open_bi().await.map(Some) } - } - - /// The wire counters the backend updates as it sends and receives, or `None` when it has none. - fn wire(&self) -> Option { - None - } - - /// The target the backend's sender probes up to, or `None` when it cannot probe. - fn probe(&self) -> Option { - None - } - - /// Receive streams evicted to free a stuck receive window, or `None` when the backend never - /// evicts. - fn streams_evicted(&self) -> Option { - None - } - - /// The receive windows now in force, or `None` when the backend cannot say; see - /// [`poll::Session::receive_window`](moq_net::transport::poll::Session::receive_window). - fn receive_window(&self) -> Option { - None - } - - /// Open a send group at `priority`, or `None` when the backend has none. - fn send_group(&self, priority: i64) -> Option<::Group> { - let _ = priority; - None - } - - /// Send a datagram scheduled by `options`; a backend without groups ignores them. - fn send_datagram( - &self, - payload: bytes::Bytes, - options: SendOptions<'_, ::Group>, - ) -> Result<(), Self::Error> { - let _ = options; - web_transport_trait::Session::send_datagram(self, payload) - } -} - -/// A backend send stream that may join its [`Grouped`] session's send groups. -pub trait GroupedSend: web_transport_trait::SendStream { - /// The backend's send group, [`Ungrouped`](moq_net::transport::poll::Ungrouped) when it has none. - type Group: moq_net::transport::poll::SendGroup + Send + Sync; - - /// Move the stream into `group`. - fn set_group(&mut self, group: &Self::Group); - - /// Set the priority within the stream's send group, or session-wide when ungrouped. - /// - /// Saturated into the async interface's narrower priority by default. - fn set_priority(&mut self, priority: i64) { - web_transport_trait::SendStream::set_priority(self, priority.clamp(i32::MIN.into(), i32::MAX.into()) as i32); - } -} diff --git a/rs/moq-tokio/src/transport/owned.rs b/rs/moq-tokio/src/transport/owned.rs deleted file mode 100644 index f5cd027d453..00000000000 --- a/rs/moq-tokio/src/transport/owned.rs +++ /dev/null @@ -1,326 +0,0 @@ -use bytes::{Buf, Bytes}; -use moq_net::transport::{self as moq, poll}; -use std::task::Poll; -use web_transport_trait::{self as wt, poll as backend}; - -/// A backend failure preserving the session and stream code registries. -#[derive(Debug, thiserror::Error)] -#[error(transparent)] -pub struct Error(E); - -impl moq::Error for Error { - fn close_kind(&self) -> moq::CloseKind { - close_kind(&self.0) - } - fn session_error(&self) -> Option<(u32, String)> { - self.0.session_error() - } - fn stream_error(&self) -> Option { - self.0.stream_error() - } -} - -struct Metrics { - inner: S, - streams_evicted: Option, -} -impl moq::Metrics for Metrics { - fn bytes_lost(&self) -> Option { - self.inner.bytes_lost() - } - fn packets_sent(&self) -> Option { - self.inner.packets_sent() - } - fn packets_received(&self) -> Option { - self.inner.packets_received() - } - fn packets_lost(&self) -> Option { - self.inner.packets_lost() - } - fn rtt(&self) -> Option { - self.inner.rtt() - } - fn estimated_send_rate(&self) -> Option { - self.inner.estimated_send_rate() - } - fn streams_evicted(&self) -> Option { - self.streams_evicted - } -} -/// The send half moq-net writes: [`SendStream`](super::SendStream), bridged so its close can be -/// watched through a shared borrow. -/// -/// `pub` only because its session's `type SendStream` names it; the module is private. -pub struct BridgedSend(moq_net::kio::Bridge>); - -impl BridgedSend { - pub(super) fn new(stream: S) -> Self { - Self(moq_net::kio::Bridge::new(super::SendStream::new(stream))) - } - - /// A newly opened stream, scheduled by its options. - fn scheduled(stream: S, options: poll::SendOptions<'_, S::Group>) -> Self { - let mut stream = Self::new(stream); - stream.0.get_mut().schedule(options); - stream - } -} - -impl> poll::Session for super::Session { - type SendStream = BridgedSend; - type RecvStream = S::Recv; - type Error = Error; - fn poll_accept_uni(&self, waiter: &moq_net::kio::Waiter) -> Poll> { - let session = &self.session; - super::poll_op(&self.accept_uni, waiter, || { - let session = session.clone(); - Box::pin(async move { session.accept_uni().await }) - }) - .map_ok(|recv| S::recv(super::RecvStream::new(recv))) - .map_err(Error) - } - fn poll_accept_bi(&self, waiter: &moq_net::kio::Waiter) -> Poll, Self::Error>> { - let session = &self.session; - super::poll_op(&self.accept_bi, waiter, || { - let session = session.clone(); - Box::pin(async move { session.accept_bi().await }) - }) - .map_ok(|(send, recv)| (BridgedSend::new(send), S::recv(super::RecvStream::new(recv)))) - .map_err(Error) - } - fn poll_open_uni( - &self, - waiter: &moq_net::kio::Waiter, - options: poll::SendOptions<'_, ::Group>, - ) -> Poll> { - let session = &self.session; - super::poll_op(&self.open_uni, waiter, || { - let session = session.clone(); - Box::pin(async move { session.open_uni().await }) - }) - .map_ok(|send| BridgedSend::scheduled(send, options)) - .map_err(Error) - } - fn poll_open_bi( - &self, - waiter: &moq_net::kio::Waiter, - options: poll::SendOptions<'_, ::Group>, - ) -> Poll, Self::Error>> { - let session = &self.session; - super::poll_op(&self.open_bi, waiter, || { - let session = session.clone(); - Box::pin(async move { session.open_bi().await }) - }) - .map_ok(|(send, recv)| { - ( - BridgedSend::scheduled(send, options), - S::recv(super::RecvStream::new(recv)), - ) - }) - .map_err(Error) - } - - fn poll_open_request( - &self, - waiter: &moq_net::kio::Waiter, - options: poll::SendOptions<'_, ::Group>, - ) -> Poll>, Self::Error>> { - let session = &self.session; - super::poll_op(&self.request_bi, waiter, || { - let session = session.clone(); - Box::pin(async move { session.open_request().await }) - }) - .map_ok(|streams| { - streams.map(|(send, recv)| { - ( - BridgedSend::scheduled(send, options), - S::recv(super::RecvStream::new(recv)), - ) - }) - }) - .map_err(Error) - } - fn send_group(&self, priority: i64) -> Option<::Group> { - super::Grouped::send_group(&self.session, priority) - } - fn send_datagram( - &self, - payload: Bytes, - options: poll::SendOptions<'_, ::Group>, - ) -> Result<(), Self::Error> { - super::Grouped::send_datagram(&self.session, payload, options).map_err(Error) - } - fn poll_recv_datagram(&self, waiter: &moq_net::kio::Waiter) -> Poll> { - let session = &self.session; - super::poll_op(&self.recv_datagram, waiter, || { - let session = session.clone(); - Box::pin(async move { session.recv_datagram().await }) - }) - .map_err(Error) - } - fn max_datagram_size(&self) -> usize { - wt::Session::max_datagram_size(&self.session) - } - fn address(&self) -> moq::Address { - self.address - } - fn protocol(&self) -> Option<&str> { - wt::Session::protocol(&self.session) - } - fn close(&self, code: u32, reason: &str) { - wt::Session::close(&self.session, code, reason); - } - fn poll_closed(&self, waiter: &moq_net::kio::Waiter) -> Poll { - let session = &self.session; - super::poll_op(&self.closed, waiter, || { - let session = session.clone(); - Box::pin(async move { session.closed().await }) - }) - .map(Error) - } - fn metrics(&self) -> impl moq::Metrics { - let inner = wt::Session::stats(&self.session); - // Reading the metrics refreshes the polled wire, which moq-net reads right after. - if super::Grouped::wire(&self.session).is_none() { - if let Some(sent) = wt::Stats::bytes_sent(&inner) { - self.polled_wire.set_sent_bytes(sent); - } - if let Some(received) = wt::Stats::bytes_received(&inner) { - self.polled_wire.set_received_bytes(received); - } - } - Metrics { - inner, - streams_evicted: super::Grouped::streams_evicted(&self.session), - } - } - fn wire(&self) -> Option { - super::Grouped::wire(&self.session).or_else(|| Some(self.polled_wire.clone())) - } - fn probe(&self) -> Option { - super::Grouped::probe(&self.session) - } - fn receive_window(&self) -> Option { - super::Grouped::receive_window(&self.session) - } -} -impl poll::SendStream for BridgedSend { - type Error = Error; - type Group = S::Group; - fn poll_write(&mut self, waiter: &moq_net::kio::Waiter, buf: &[u8]) -> Poll> { - backend::SendStream::poll_write(self.0.get_mut(), &mut waiter.context(), buf).map_err(Error) - } - fn poll_write_buf( - &mut self, - waiter: &moq_net::kio::Waiter, - buf: &mut B, - ) -> Poll> { - backend::SendStream::poll_write_buf(self.0.get_mut(), &mut waiter.context(), buf).map_err(Error) - } - fn set_priority(&mut self, priority: i64) { - self.0.get_mut().prioritize(priority) - } - fn set_group(&mut self, group: &Self::Group) { - self.0.get_mut().join(group) - } - fn finish(&mut self) -> Result<(), Self::Error> { - backend::SendStream::finish(self.0.get_mut()).map_err(Error) - } - fn reset(&mut self, code: u32) { - backend::SendStream::reset(self.0.get_mut(), code) - } - fn poll_closed(&self, waiter: &moq_net::kio::Waiter) -> Poll> { - self.0.poll(waiter, backend::SendStream::poll_closed).map_err(Error) - } -} -impl poll::Chunks for super::RecvStream { - type Error = Error; - fn poll_read_chunk( - &mut self, - waiter: &moq_net::kio::Waiter, - max: usize, - ) -> Poll, Self::Error>> { - backend::RecvStream::poll_read_chunk(self, &mut waiter.context(), max).map_err(Error) - } - fn stop(&mut self, code: u32) { - backend::RecvStream::stop(self, code) - } -} - -#[cfg(feature = "quic")] -impl poll::RecvStream for crate::quic::web::RecvStream { - type Error = Error; - type Lease<'a> = crate::quic::driver::recv::Lease<'a>; - - fn poll_lease(&mut self, waiter: &moq_net::kio::Waiter, want: usize) -> Poll, Self::Error>> { - crate::quic::web::RecvStream::poll_lease(self, &mut waiter.context(), want).map_err(Error) - } - fn set_priority(&mut self, priority: i64) { - crate::quic::web::RecvStream::set_priority(self, priority) - } - fn stop(&mut self, code: u32) { - // A stream already closed has nobody left to tell. - crate::quic::web::RecvStream::stop(self, code); - } - fn poll_reset(&mut self, waiter: &moq_net::kio::Waiter) -> Poll { - crate::quic::web::RecvStream::poll_reset(self, &mut waiter.context()).map(Error) - } -} - -#[cfg(feature = "quic")] -impl poll::Lease for crate::quic::driver::recv::Lease<'_> { - fn is_fin(&self) -> bool { - crate::quic::driver::recv::Lease::is_fin(self) - } - fn chunks(&self) -> impl Iterator { - crate::quic::driver::recv::Lease::chunks(self) - } - fn release(self) { - crate::quic::driver::recv::Lease::release(self) - } -} - -fn close_kind(error: &(dyn std::error::Error + 'static)) -> moq::CloseKind { - #[cfg(feature = "quic")] - if let Some(crate::quic::web::ReadError::Evicted) = error.downcast_ref::() { - return moq::CloseKind::Evicted; - } - #[cfg(feature = "quic")] - if let Some(error) = error.downcast_ref::() { - use crate::quic::{driver::connection::Error as ConnectionError, web::SessionError}; - return match error { - SessionError::ConnectionError(ConnectionError::TimedOut) => moq::CloseKind::IdleTimeout, - SessionError::ConnectionError(ConnectionError::LocallyClosed { .. }) => moq::CloseKind::Closed, - _ => moq::CloseKind::Transport, - }; - } - error.source().map(close_kind).unwrap_or_default() -} - -#[cfg(all(test, feature = "quic"))] -mod outcome_tests { - use super::*; - use crate::quic::{driver::connection::Error as ConnectionError, web::SessionError}; - use moq_net::transport::Error as _; - - #[test] - fn typed_idle_and_local_closes_survive_session_and_stream_wrappers() { - let idle = Error(SessionError::ConnectionError(ConnectionError::TimedOut)); - assert!(matches!( - moq_net::Error::from_transport(idle), - moq_net::Error::IdleTimeout - )); - let idle = Error(crate::quic::web::ReadError::SessionError( - SessionError::ConnectionError(ConnectionError::TimedOut), - )); - assert_eq!(idle.close_kind(), moq::CloseKind::IdleTimeout); - let local = Error(SessionError::ConnectionError(ConnectionError::LocallyClosed { - code: 0u32.into(), - reason: String::new(), - })); - assert!(matches!( - moq_net::Error::from_transport(local), - moq_net::Error::Session(moq_net::SessionError::Cancel) - )); - } -} diff --git a/rs/moq-tokio/tests/integration/datagram_delivery.rs b/rs/moq-tokio/tests/integration/datagram_delivery.rs index a0035758069..8221e4ac2f2 100644 --- a/rs/moq-tokio/tests/integration/datagram_delivery.rs +++ b/rs/moq-tokio/tests/integration/datagram_delivery.rs @@ -90,8 +90,7 @@ impl Viewer { let (endpoint, conn) = dial(version, addr, Default::default()).await; let origin = moq_tokio::origin::spawn(); - let transport = - moq_tokio::transport::Session::new(web::Session::raw(conn.clone()), moq_net::transport::Address::Setup); + let transport = web::Session::raw(conn.clone()); let (session, driver) = moq_net::Client::new() .with_versions(version.into()) .with_subscriber(origin.clone()) diff --git a/rs/moq-tokio/tests/integration/receive_credit.rs b/rs/moq-tokio/tests/integration/receive_credit.rs index 4c1c52ae3c5..66b6391bf93 100644 --- a/rs/moq-tokio/tests/integration/receive_credit.rs +++ b/rs/moq-tokio/tests/integration/receive_credit.rs @@ -119,8 +119,7 @@ async fn setup_through( let (endpoint, conn) = dial(version, addr, transport).await; let subscriber = moq_tokio::origin::spawn(); - let session = - moq_tokio::transport::Session::new(web::Session::raw(conn.clone()), moq_net::transport::Address::Setup); + let session = web::Session::raw(conn.clone()); let (client, driver) = moq_net::Client::new() .with_versions(version.into()) .with_subscriber(subscriber.clone()) @@ -292,10 +291,7 @@ async fn the_less_urgent_partial_frame_gives_way(version: &str, profile: Option< group.finish().unwrap(); assert_eq!(next_frame(&mut low_sub).await, &b"after"[..], "{version}"); assert_eq!(setup.conn.stats().credit.evicted, 1, "{version}"); - let session = moq_tokio::transport::Session::new( - web::Session::raw(setup.conn.clone()), - moq_net::transport::Address::Setup, - ); + let session = web::Session::raw(setup.conn.clone()); assert_eq!( moq_net::transport::Metrics::streams_evicted(&moq_net::transport::poll::Session::metrics(&session)), Some(1), @@ -471,9 +467,7 @@ async fn reversed(version: &str) -> Reversed { let broadcast = publisher.create_broadcast("room").expect("broadcast"); broadcast.announce(Default::default()).expect("announce"); let subscriber = moq_tokio::origin::spawn(); - let session = |conn: driver::connection::Connection| { - moq_tokio::transport::Session::new(web::Session::raw(conn), moq_net::transport::Address::Setup) - }; + let session = |conn: driver::connection::Connection| web::Session::raw(conn); // A moq-lite client's driver sends its half of the handshake, so it runs meanwhile. let client_session = tokio::spawn({ let session = session(conn.clone()); @@ -814,7 +808,6 @@ async fn javascript_partial_frame_gives_way(version: &str) { ) .expect("endpoint"); let port = endpoint.local_addr().port(); - let mut server = web::Server::new(endpoint); let script = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../test/receive-credit/sender.ts"); let child = tokio::process::Command::new("bun") @@ -830,10 +823,14 @@ async fn javascript_partial_frame_gives_way(version: &str) { .spawn() .expect("spawn bun"); - let request = kio::clock::timeout(TIMEOUT, server.accept()) - .await - .expect("connect timeout") - .expect("request"); + let request = kio::clock::timeout(TIMEOUT, async { + let incoming = endpoint.accept().await.expect("incoming connection"); + let connection = incoming.accept().expect("accept QUIC").await.expect("QUIC handshake"); + web::Request::accept(connection).await + }) + .await + .expect("connect timeout") + .expect("request"); let session = request .respond(web::proto::ConnectResponse::OK.with_protocol(version.alpn())) .await @@ -842,10 +839,7 @@ async fn javascript_partial_frame_gives_way(version: &str) { let (session, driver) = moq_net::Server::new() .with_versions(version.into()) .with_subscriber(origin.clone()) - .accept( - moq_net::time::Instant::now(), - moq_tokio::transport::Session::new(session, moq_net::transport::Address::Uri), - ) + .accept(moq_net::time::Instant::now(), session) .await .expect("server handshake"); tokio::spawn(moq_net::time::run(driver)); diff --git a/rs/moq-tokio/tests/integration/sim.rs b/rs/moq-tokio/tests/integration/sim.rs index 33e96759315..180169eb1e2 100644 --- a/rs/moq-tokio/tests/integration/sim.rs +++ b/rs/moq-tokio/tests/integration/sim.rs @@ -150,7 +150,7 @@ async fn serve(network: &Network, version: Version, seed: u64) -> Arrivals { let accept = async { let conn = server.accept().await.unwrap().accept().unwrap().await.unwrap(); - let session = moq_tokio::transport::Session::new(web::Session::raw(conn), moq_net::transport::Address::Setup); + let session = web::Session::raw(conn); let (session, driver) = moq_net::Server::new() .with_versions(version.into()) .with_publisher(publisher.consume()) @@ -166,7 +166,7 @@ async fn serve(network: &Network, version: Version, seed: u64) -> Arrivals { .unwrap() .await .unwrap(); - let session = moq_tokio::transport::Session::new(web::Session::raw(conn), moq_net::transport::Address::Setup); + let session = web::Session::raw(conn); let (session, driver) = moq_net::Client::new() .with_versions(version.into()) .with_subscriber(viewer.clone()) diff --git a/rs/moq-uring/Cargo.toml b/rs/moq-uring/Cargo.toml index ceb9934dbd9..e33552224f3 100644 --- a/rs/moq-uring/Cargo.toml +++ b/rs/moq-uring/Cargo.toml @@ -26,6 +26,7 @@ _manual = ["kio/manual"] [target.'cfg(target_os = "linux")'.dependencies] bytes = "1" +futures = "0.3" http = "1" io-uring = { workspace = true } # The worker is a kio clock backend: its turn's instant is the ambient clock. @@ -47,7 +48,6 @@ thiserror = "2" tracing = "0.1" url = "2" web-transport-proto = { workspace = true } -web-transport-trait = { workspace = true } [dev-dependencies] anyhow = "1" @@ -56,7 +56,6 @@ criterion = { package = "bench-harness", path = "../bench-harness" } moq-net = { path = "../moq-net" } thiserror = { workspace = true } tokio = { workspace = true, features = ["macros", "net", "rt", "sync", "time"] } -web-transport-trait = { workspace = true } workspace-hack = { path = "../workspace-hack" } [target.'cfg(target_os = "linux")'.dev-dependencies] @@ -106,5 +105,10 @@ required-features = ["quic"] name = "timers" harness = false +[[bench]] +name = "web_headers" +harness = false +required-features = ["quic"] + [lints] workspace = true diff --git a/rs/moq-uring/MIGRATE.md b/rs/moq-uring/MIGRATE.md index 5759bd5e8de..65583a3f7d8 100644 --- a/rs/moq-uring/MIGRATE.md +++ b/rs/moq-uring/MIGRATE.md @@ -4,6 +4,29 @@ How to update your code for each breaking change, newest first. ## Unreleased +### Native WebTransport implements moq-net's poll traits ([#7771](https://github.com/moq-dev/moq/pull/7771)) + +`quic::web` is supported as a WebTransport library without running MoQ. +Pass its `Session` directly to moq-net through `transport::Local::new(session)`. +Remove the generic `transport::Session::new(session, address)` wrapper; a WebTransport session reports `Address::Uri` and `Session::raw(connection)` reports `Address::Setup`. +The generic send/receive adapters, their error wrapper, and the sealed `Grouped`/`GroupedSend` traits are removed, along with every `web_transport_trait` implementation and dependency. +Custom transports implement `moq_net::transport::poll` directly. + +Use `quic::web::send::Stream` and `recv::Stream` instead of `SendStream` and `RecvStream`. +Their errors are `send::Error` and `recv::Error`. +`finish(self)` returns a `send::Finished`, which can be awaited for acknowledgements or a peer stop; `reset(self, code)` and `stop(self, code)` consume their stream. +Code using moq-net's poll traits keeps their mutable terminal methods. +Session polls take `&self` and a caller-owned `&kio::Waiter`; opens also take the driver's `send::Options`, preserving full `i64` priorities and real send groups. + +`Request` dereferences to `proto::ConnectRequest`; read its `url` and `protocols` fields there. +`Request::connection()` names the underlying QUIC connection. +`Request::respond` takes `impl Into` and requires status OK; `reject` takes an `http::StatusCode`. +Replace `web::Response::default()` with `web::proto::ConnectResponse::OK` and `web::Rejected` variants with HTTP status codes. +`send::Stream`, `send::Finished`, and `recv::Stream` expose their stream ID as `id()`. + +A receive stream's `closed()` observes an empty FIN without a read. When there is payload, read it before waiting for close. `poll_reset` still watches only failures, so it remains pending after a clean end. +Invalid stream error codes fail as `InvalidReset` or `InvalidStopped`, rather than looking like a clean end. + ### QUIC waits have caller-owned poll forms ([#7707](https://github.com/moq-dev/moq/pull/7707)) The QUIC driver connection's stream opens, accepts, datagram receive and close waits, @@ -43,7 +66,7 @@ Pass a received `Packet::dst_ip()` as `src_ip` to answer from the address the pe ### Wrap sessions in `transport::Local` (#6176) moq-net dropped `connect_lite`, `accept_lite`, and `accept_request_lite`, and every session now needs a `Send` transport. -Wrap the worker's session as `transport::Local::new(transport::Session::new(conn, address))` and call `connect`, `accept`, or `accept_request`. +Wrap the worker's `quic::web::Session` as `transport::Local::new(session)` and call `connect`, `accept`, or `accept_request`. ### Timers run on `kio::clock` @@ -74,12 +97,3 @@ A qlog directory gets one file per endpoint and one per dial instead of one per `quic::server::Config` drops `identity`, `alpn`, and `client_auth` for one `tls: Arc`, and `Config::new` takes it. Build it from an identity with `identity.server(&alpns, client_auth)?`, or share a resolver with other listeners. - -### Sessions are wrapped for moq-net - -moq-net no longer takes a `web_transport_trait` session. -Wrap a moq-uring session with `transport::Session::new`, naming how the request target travels. - -```rust -let session = moq_uring::transport::Session::new(connection, moq_net::transport::Address::Uri); -``` diff --git a/rs/moq-uring/README.md b/rs/moq-uring/README.md index bedb06d4eba..f76be803dae 100644 --- a/rs/moq-uring/README.md +++ b/rs/moq-uring/README.md @@ -35,12 +35,11 @@ local (`!Send`) task set, and the UDP sockets bound through it. the application protocol. - **WebTransport**: browsers negotiate `h3` and `quic::web::Request` runs the HTTP/3 CONNECT handshake (SETTINGS, subprotocol selection, capsule close) - over the same adapter via `web-transport-proto`. `quic::web::Session` is - a raw or web transport (`Session::raw`). Wrap it with - `transport::Session::new(session, address)`, using `moq_net::transport::Address::Uri` - for WebTransport or `Setup` for raw QUIC. moq-net boxes the transport with a - native `Send` bound, so wrap the session once more in `transport::Local`, which - checks the thread on every use, before `connect`/`accept`. They return the + over the same driver via `web-transport-proto`. `quic::web::Session` is + a raw or web transport (`Session::raw`) implementing moq-net's poll traits directly. + moq-net boxes the transport with a native `Send` bound, so wrap the session in + `transport::Local::new(session)`, which checks the thread on every use, before + `connect`/`accept`. They return the session and its driver; poll the driver or await it inside a `Handle::spawn` task to run it on the worker. Web mode maps stream and close codes through the HTTP/3 error space. diff --git a/rs/moq-uring/benches/echo_quic.rs b/rs/moq-uring/benches/echo_quic.rs index 8098d47efdd..654615b0709 100644 --- a/rs/moq-uring/benches/echo_quic.rs +++ b/rs/moq-uring/benches/echo_quic.rs @@ -17,18 +17,18 @@ mod linux { use std::time::Instant; use criterion::{BenchmarkId, Criterion, Throughput}; + use moq_net::transport::poll::SendStream as _; use moq_uring::{Config, Error, Worker, quic, udp}; - use web_transport_trait::poll::{RecvStream as _, SendStream as _, Session as _}; use super::support; const PAYLOAD: usize = 1024 * 1024; - async fn drain(recv: &mut quic::web::RecvStream) -> Vec { + async fn drain(recv: &mut quic::web::recv::Stream) -> Vec { let mut out = Vec::new(); let mut buf = [0u8; 64 * 1024]; loop { - match std::future::poll_fn(|cx| recv.poll_read(cx, &mut buf)) + match kio::wait(|waiter| recv.poll_read(waiter, &mut buf)) .await .expect("read") { @@ -38,14 +38,12 @@ mod linux { } } - async fn write_finish(send: &mut quic::web::SendStream, mut buf: &[u8]) { + async fn write_finish(mut send: quic::web::send::Stream, mut buf: &[u8]) { while !buf.is_empty() { - let n = std::future::poll_fn(|cx| send.poll_write(cx, buf)) - .await - .expect("write"); + let n = kio::wait(|waiter| send.poll_write(waiter, buf)).await.expect("write"); buf = &buf[n..]; } - send.finish().expect("finish"); + drop(send.finish()); } fn measure(udp_config: udp::Config, iterations: u64) -> std::time::Duration { @@ -87,14 +85,14 @@ mod linux { while let Ok(done) = start_rx.recv() { let (mut send, mut recv) = session.open_bi(Default::default()).await.expect("open stream"); send.write_all(&payload[..]).await.expect("write"); - send.finish().expect("finish"); + drop(send.finish()); assert_eq!(recv.read_to_end(PAYLOAD + 1).await.expect("read").len(), PAYLOAD); done.send(()).expect("notify server"); } }); }); - let mut session = worker + let session = worker .block_on(async { let conn = endpoint.accept().await.expect("accept"); quic::web::Request::accept(conn) @@ -112,11 +110,11 @@ mod linux { start_tx.send(done_tx).expect("start"); worker .block_on(async { - let (mut send, mut recv) = std::future::poll_fn(|cx| session.poll_accept_bi(cx)) + let (send, mut recv) = kio::wait(|waiter| session.poll_accept_bi(waiter)) .await .expect("accept stream"); let payload = drain(&mut recv).await; - write_finish(&mut send, &payload).await; + write_finish(send, &payload).await; done_rx.await.expect("peer received echo"); }) .expect("worker"); diff --git a/rs/moq-uring/benches/session_lite.rs b/rs/moq-uring/benches/session_lite.rs index bb7384f70d0..68759dbf23b 100644 --- a/rs/moq-uring/benches/session_lite.rs +++ b/rs/moq-uring/benches/session_lite.rs @@ -145,10 +145,7 @@ mod linux { .with_publisher(&pub_origin) .accept( moq_net::time::Instant::now(), - moq_uring::transport::Local::new(moq_uring::transport::Session::new( - quic::web::Session::raw(conn), - moq_net::transport::Address::Setup, - )), + moq_uring::transport::Local::new(quic::web::Session::raw(conn)), ) .await .expect("accept"); @@ -166,10 +163,7 @@ mod linux { .with_subscriber(sub_origin.clone()) .connect( moq_net::time::Instant::now(), - moq_uring::transport::Local::new(moq_uring::transport::Session::new( - quic::web::Session::raw(conn), - moq_net::transport::Address::Setup, - )), + moq_uring::transport::Local::new(quic::web::Session::raw(conn)), ) .await .expect("connect"); diff --git a/rs/moq-uring/benches/web_headers.rs b/rs/moq-uring/benches/web_headers.rs new file mode 100644 index 00000000000..99046771523 --- /dev/null +++ b/rs/moq-uring/benches/web_headers.rs @@ -0,0 +1,39 @@ +//! The cost of ready stream headers against independently stalled headers. + +use criterion::{Criterion, criterion_group, criterion_main}; + +#[cfg(target_os = "linux")] +#[path = "../tests/integration/web_headers.rs"] +mod headers; +#[cfg(target_os = "linux")] +#[path = "../tests/integration/support.rs"] +mod support; + +fn benchmark(c: &mut Criterion) { + #[cfg(target_os = "linux")] + { + match moq_uring::Worker::new(Default::default()) { + Ok(worker) => drop(worker), + Err(moq_uring::Error::Unsupported(reason)) if std::env::var_os("CI_STRICT").is_none() => { + eprintln!("skipping io_uring header benchmark: {reason}"); + return; + } + Err(err) => panic!("worker: {err}"), + } + let mut group = c.benchmark_group("web_headers"); + for pending in [0, 64, 256] { + for ready in [1, 16] { + group.throughput(criterion::Throughput::Elements(2 * ready as u64)); + group.bench_function(format!("pending-{pending}/ready-{ready}"), |b| { + b.iter_custom(|iterations| headers::run(pending, ready, iterations)); + }); + } + } + group.finish(); + } + #[cfg(not(target_os = "linux"))] + let _ = c; +} + +criterion_group!(benches, benchmark); +criterion_main!(benches); diff --git a/rs/moq-uring/src/lib.rs b/rs/moq-uring/src/lib.rs index 122dc073ac7..b13d7550aa7 100644 --- a/rs/moq-uring/src/lib.rs +++ b/rs/moq-uring/src/lib.rs @@ -14,7 +14,7 @@ //! //! [`quic`] stacks a sans-IO QUIC stack on that path: a [`quic::Endpoint`] //! serves many connections on one socket (demuxed by connection id, dials -//! included), each a [`quic::Connection`] wrapped by [`transport::Session`] +//! included), each a [`quic::Connection`] wrapped by [`quic::web::Session`] //! and [`transport::Local`], which meets moq-net's `Send` bound by checking the //! thread on every use, so `moq_net::Client::connect` and `Server::accept` run //! real MoQ sessions on the worker. The socket is the identity: whatever is diff --git a/rs/moq-uring/src/quic/client.rs b/rs/moq-uring/src/quic/client.rs index 39b64d53b6b..ff2a6677926 100644 --- a/rs/moq-uring/src/quic/client.rs +++ b/rs/moq-uring/src/quic/client.rs @@ -50,8 +50,7 @@ impl Default for Config { /// Shorthand for a dial-only [`Endpoint`](super::Endpoint) and one /// [`connect`](super::Endpoint::connect) through it. The connection's driver /// runs as a task on the worker that adopted `socket`. Wrap the returned -/// [`Connection`] with [`crate::transport::Session::new`] and -/// [`moq_net::transport::Address::Setup`], then [`crate::transport::Local`], to +/// [`Connection`] with [`super::web::Session::raw`], then [`crate::transport::Local`], to /// hand it to `moq_net::Client::connect`, or use the stream API directly. pub async fn connect( socket: udp::Socket, diff --git a/rs/moq-uring/src/quic/driver/connection.rs b/rs/moq-uring/src/quic/driver/connection.rs index 78d69be03c3..72cabafa40e 100644 --- a/rs/moq-uring/src/quic/driver/connection.rs +++ b/rs/moq-uring/src/quic/driver/connection.rs @@ -164,7 +164,7 @@ impl Inner { /// Close the connection with a full-width application code. /// - /// The [`web_transport_trait::poll::Session::close`] surface narrows codes + /// The [`moq_net::transport::poll::Session::close`] surface narrows codes /// to `u32`; the WebTransport layer maps its codes into HTTP/3's error /// space, which needs the whole varint range. pub(crate) fn close_code(&self, code: u64, reason: &str) { @@ -209,7 +209,7 @@ pub struct Connection { impl Connection { /// Close the connection with a full-width application code. /// - /// The [`web_transport_trait::poll::Session::close`] surface narrows codes + /// The [`moq_net::transport::poll::Session::close`] surface narrows codes /// to `u32`; the WebTransport layer maps its codes into HTTP/3's error /// space, which needs the whole varint range. pub(crate) fn close_code(&self, code: u64, reason: &str) { @@ -329,11 +329,6 @@ impl Connection { self.shared.probe.clone() } - /// Receive streams evicted to free a stuck receive window. - pub(crate) fn streams_evicted(&self) -> u64 { - self.shared.conn.borrow().stats().credit.evicted - } - /// The receive windows now in force: the connection's, and the most one stream holds. pub(crate) fn receive_window(&self) -> moq_net::transport::ReceiveWindow { let conn = self.shared.conn.borrow(); @@ -560,13 +555,12 @@ impl Connection { poll_closed(&self.shared, waiter) } - /// The connection's counters in [`web_transport_trait::Stats`] shape. - pub(crate) fn transport_stats(&self) -> impl web_transport_trait::Stats + use<> { + /// The connection's metrics for moq-net. + pub(crate) fn transport_stats(&self) -> impl moq_net::transport::Metrics + use<> { let stats = self.stats(); Stats { - bytes_sent: stats.udp_tx.bytes, - bytes_received: stats.udp_rx.bytes, bytes_lost: stats.path.lost_bytes, + streams_evicted: stats.credit.evicted, packets_sent: stats.udp_tx.datagrams, packets_received: stats.udp_rx.datagrams, packets_lost: stats.path.lost_packets, @@ -646,12 +640,10 @@ impl std::fmt::Debug for Connection { } } -/// A snapshot of moq-quic's counters in [`web_transport_trait::Stats`] -/// shape. +/// A snapshot of moq-quic's metrics for moq-net. struct Stats { - bytes_sent: u64, - bytes_received: u64, bytes_lost: u64, + streams_evicted: u64, packets_sent: u64, packets_received: u64, packets_lost: u64, @@ -660,15 +652,10 @@ struct Stats { bandwidth: Option, } -impl web_transport_trait::Stats for Stats { - fn bytes_sent(&self) -> Option { - Some(self.bytes_sent) +impl moq_net::transport::Metrics for Stats { + fn streams_evicted(&self) -> Option { + Some(self.streams_evicted) } - - fn bytes_received(&self) -> Option { - Some(self.bytes_received) - } - fn bytes_lost(&self) -> Option { Some(self.bytes_lost) } diff --git a/rs/moq-uring/src/quic/driver/recv.rs b/rs/moq-uring/src/quic/driver/recv.rs index 0997089e980..9f37d191cc8 100644 --- a/rs/moq-uring/src/quic/driver/recv.rs +++ b/rs/moq-uring/src/quic/driver/recv.rs @@ -504,7 +504,7 @@ impl From for std::io::Error { } } -/// What the `web_transport_trait` impls report, which have one error type per session. +/// Preserve a stream failure when a connection-level operation, such as framing, reports it. impl From for ConnectionError { fn from(err: Error) -> Self { match err { diff --git a/rs/moq-uring/src/quic/driver/send.rs b/rs/moq-uring/src/quic/driver/send.rs index 22a0ce0c8e8..f6c4976132d 100644 --- a/rs/moq-uring/src/quic/driver/send.rs +++ b/rs/moq-uring/src/quic/driver/send.rs @@ -136,14 +136,6 @@ impl Stream { self.0.state = HalfState::Open(offset, code); } - /// The peer's STOP_SENDING code, if it already stopped the stream. - pub(crate) fn stop_code(&self) -> Option { - match self.0.shared.conn.borrow_mut().send_stream(self.0.id).end() { - Ok(Some(End::Stopped(code))) => Some(code), - _ => None, - } - } - /// Set the priority within the stream's send group, or session-wide when ungrouped. /// /// Higher goes first. @@ -447,7 +439,7 @@ impl From for std::io::Error { } } -/// What the `web_transport_trait` impls report, which have one error type per session. +/// Preserve a stream failure when a connection-level operation, such as framing, reports it. impl From for ConnectionError { fn from(err: Error) -> Self { match err { diff --git a/rs/moq-uring/src/quic/mod.rs b/rs/moq-uring/src/quic/mod.rs index 039392f13bf..24bf10e1c45 100644 --- a/rs/moq-uring/src/quic/mod.rs +++ b/rs/moq-uring/src/quic/mod.rs @@ -5,9 +5,8 @@ //! shuttles packets between the socket and the connection (GSO trains out, //! GRO coalesces in), arms the worker's userspace timers from the //! connection's timeout, and wakes stream waiters. The returned -//! [`Connection`] implements [`web_transport_trait::poll`], so -//! wrapping it with `crate::transport::Session::new` and -//! [`moq_net::transport::Address::Setup`], then `crate::transport::Local`, lets +//! [`web::Session`] implements [`moq_net::transport::poll`], so wrapping +//! [`web::Session::raw`] in [`crate::transport::Local`] lets //! `moq_net::Client::connect` / `Server::accept` run real MoQ sessions on the //! worker; everything is `Rc`-shared and `!Send` by design. //! @@ -127,7 +126,7 @@ pub enum Congestion { /// Why a connection could not be set up or has ended. /// /// One error type for the whole module: it is also what every -/// [`web_transport_trait::poll`] operation on a [`Connection`] reports, which +/// [`moq_net::transport::poll`] session operation reports, which /// is how the close reason reaches `moq_net::Error::from_transport`. #[derive(Clone, Debug, thiserror::Error)] #[non_exhaustive] @@ -209,18 +208,48 @@ pub enum Error { Config(#[from] moq_quic::ConfigError), } -impl web_transport_trait::Error for Error { +impl moq_net::transport::Error for Error { + fn close_kind(&self) -> moq_net::transport::CloseKind { + match self { + Self::TimedOut => moq_net::transport::CloseKind::IdleTimeout, + Self::Closed => moq_net::transport::CloseKind::Closed, + Self::Evicted => moq_net::transport::CloseKind::Evicted, + _ => moq_net::transport::CloseKind::Transport, + } + } fn session_error(&self) -> Option<(u32, String)> { match self { - Self::App { code, reason } => Some((u32::try_from(*code).unwrap_or(u32::MAX), reason.clone())), + Self::App { code, reason } => u32::try_from(*code).ok().map(|code| (code, reason.clone())), _ => None, } } fn stream_error(&self) -> Option { match self { - Self::Reset(code) | Self::Stop(code) => Some(u32::try_from(*code).unwrap_or(u32::MAX)), + Self::Reset(code) | Self::Stop(code) => u32::try_from(*code).ok(), _ => None, } } } + +#[cfg(test)] +mod outcome_tests { + use super::*; + use moq_net::transport::{CloseKind, Error as _}; + #[test] + fn transport_kinds_and_out_of_registry_codes_stay_typed() { + assert_eq!(Error::TimedOut.close_kind(), CloseKind::IdleTimeout); + assert_eq!(Error::Closed.close_kind(), CloseKind::Closed); + assert_eq!(web::recv::Error::Evicted.close_kind(), CloseKind::Evicted); + assert_eq!( + web::send::Error::SessionError(Error::TimedOut).close_kind(), + CloseKind::IdleTimeout + ); + let outside = Error::App { + code: u64::MAX, + reason: "unknown registry".into(), + }; + assert_eq!(outside.session_error(), None); + assert_eq!(Error::Reset(u64::MAX).stream_error(), None); + } +} diff --git a/rs/moq-uring/src/quic/web.rs b/rs/moq-uring/src/quic/web.rs index 134e8efc8c5..919a1c74e9c 100644 --- a/rs/moq-uring/src/quic/web.rs +++ b/rs/moq-uring/src/quic/web.rs @@ -15,15 +15,31 @@ //! through untouched in raw mode, except that a received stream code in the //! HTTP/3 range is unmapped, since older raw peers send that form. +use futures::{StreamExt, stream::FuturesUnordered}; use std::cell::RefCell; use std::collections::VecDeque; +use std::pin::Pin; use std::rc::Rc; use std::task::{Context, Poll, ready}; use bytes::{Buf, Bytes, BytesMut}; -use web_transport_proto as proto; +/// HTTP types used when accepting or refusing a session. +pub use http; +/// The HTTP/3 and WebTransport wire types used by this API. +pub use web_transport_proto as proto; +/// The HTTP/3 ALPN used by WebTransport. +pub const ALPN: &str = "h3"; -use super::{Connection, Error, VarInt, recv, send}; +use super::{Connection, Error, VarInt, recv as driver_recv, send as driver_send}; + +/// Incoming WebTransport streams and their errors. +pub mod recv; +/// Outgoing WebTransport streams and their errors. +pub mod send; +use self::{recv::Stream as RecvStream, send::Stream as SendStream}; + +/// An error establishing or using a WebTransport session on this worker. +pub use super::Error as SessionError; /// The frame type WebTransport bidirectional streams lead with. const FRAME_WEBTRANSPORT: u64 = 0x41; @@ -47,19 +63,6 @@ const CLOSE_GRACE: std::time::Duration = std::time::Duration::from_secs(1); /// frame and dribble it until the process runs out of memory. Everything /// parsed here is a few hundred bytes; no real peer meets this. const MESSAGE_LIMIT: usize = 64 * 1024; -/// How many unidirectional streams a peer may open before its control stream -/// arrives. -/// -/// The handshake holds each arrival (QPACK, the control stream) or parks it as -/// an early WebTransport stream, so without a cap a peer that never sends a -/// control stream could open them without end. A real client opens three -/// before its CONNECT, plus whatever it pipelines. -const HANDSHAKE_STREAMS: usize = 64; -/// How many of a peer's critical streams to hold open once the session is -/// running. HTTP/3 defines one control stream and two QPACK streams; the -/// slack is for a peer that greases, and anything past it is dropped rather -/// than retained. -const HELD_STREAMS: usize = 8; /// `H3_GENERAL_PROTOCOL_ERROR`, for closing a connection whose HTTP/3 /// handshake never produced a session. const H3_GENERAL_PROTOCOL_ERROR: u64 = 0x0101; @@ -77,19 +80,19 @@ pub struct Request { /// Closes the connection on every way out of here that is not an answer. guard: Guard, request: proto::ConnectRequest, - send: send::Stream, - recv: recv::Stream, + send: driver_send::Stream, + recv: driver_recv::Stream, /// The peer's control and QPACK streams, held open for the session's /// life: closing them reads as tearing the H3 connection down. - held: Vec, + held: [Option; 3], /// Our control stream, same deal. - control: send::Stream, + control: driver_send::Stream, /// WebTransport streams a pipelining peer opened before its CONNECT was /// answered, headers consumed, keyed by the session id they claimed. - early: Vec<(u64, recv::Stream)>, + early: Vec<(u64, driver_recv::Stream)>, /// Streams that arrived during the handshake and are still mid-header; /// the session goes on classifying them. - pending: Vec, + pending: FuturesUnordered, } /// Closes a connection whose handshake was abandoned, disarmed once the peer @@ -172,66 +175,34 @@ impl Request { // Taking them one at a time would let a stream that sends its type // byte and then stalls hold off a control stream that has already // fully arrived, for as long as the peer keeps the connection alive. - let mut pending: Vec = Vec::new(); - let mut held = Vec::new(); + let mut pending: FuturesUnordered = FuturesUnordered::new(); + let mut held = [None, None, None]; let mut early = Vec::new(); - // Every arrival counts, not just the ones kept: a stream of unknown - // type is dropped here, and a dropped stream returns its credit, so - // counting what we retain would let a peer loop this forever while - // never sending a control stream. - let mut arrivals = 0usize; let mut peer_control = None; kio::wait(|waiter| { let mut cx = waiter.context(); loop { - // Stop adopting past the cap, but do not give up on what is - // already here: the control stream may be sitting at the head - // of a queue a pipelining peer filled behind it, and refusing - // that peer is the bug the cap is not for. The one that tips - // it over is kept rather than dropped, since dropping it would - // cancel a legitimate stream on a handshake that then succeeds. - let mut over = false; - while !over { - match conn.poll_accept_uni(waiter) { - Poll::Ready(Ok(recv)) => { - arrivals += 1; - pending.push(PendingUni::new(recv)); - over = arrivals > HANDSHAKE_STREAMS; - } - Poll::Ready(Err(err)) => return Poll::Ready(Err(err)), - Poll::Pending => break, - } + while let Poll::Ready(recv) = conn.poll_accept_uni(waiter) { + pending.push(PendingUni::new(recv?)); } // Each mid-header stream parks the caller in its own stream's // waiters, so progress on any of them re-polls us. let mut progressed = false; - let mut index = 0; - while index < pending.len() { - let Poll::Ready(class) = pending[index].poll_classify(&mut cx) else { - index += 1; - continue; - }; - // Whatever was last takes this slot, so it is polled by - // this same pass rather than waited for. - let stream = pending.swap_remove(index); + while let Poll::Ready(Some((class, recv))) = pending.poll_next_unpin(&mut cx) { progressed = true; match class { UniClass::Control => { - peer_control = Some(stream.recv); - return Poll::Ready(Ok(())); + peer_control = Some(recv); + return Poll::Ready(Ok::<(), Error>(())); } - UniClass::Qpack => held.push(stream.recv), - UniClass::Web(session) => early.push((session, stream.recv)), + UniClass::QpackEncoder => held[1] = Some(recv), + UniClass::QpackDecoder => held[2] = Some(recv), + UniClass::Web(session) => early.push((session, recv)), UniClass::Unknown => {} } } - // Only now, with everything adopted classified: the peer sent - // this many streams and none of them was a control stream. - if over { - return Poll::Ready(Err(Error::Web("too many streams before the control stream".into()))); - } if !progressed { return Poll::Pending; } @@ -244,7 +215,7 @@ impl Request { if settings.supports_webtransport() == 0 { return Err(Error::Web("peer does not support WebTransport".into())); } - held.push(peer_control); + held[0] = Some(peer_control); // The CONNECT request rides the client's first bidirectional stream, // which arrives first: QUIC creates lower-numbered streams @@ -265,42 +236,32 @@ impl Request { }) } - /// The URL the peer connected to; the path and query are what a server - /// routes and authenticates on. The `url` crate is - /// [re-exported](crate::url) so naming this type needs no dependency of - /// your own. - pub fn url(&self) -> &url::Url { - &self.request.url - } - - /// The subprotocols the peer offered, the WebTransport equivalent of ALPN. - /// Pick one and name it in [`respond`](Self::respond). - pub fn protocols(&self) -> &[String] { - &self.request.protocols - } - - /// Accept with a `200`, answering as `response` describes. - pub async fn respond(mut self, response: Response) -> Result { - let Response { protocol } = response; - - let mut encoded = proto::ConnectResponse::OK; - if let Some(protocol) = &protocol { - if !self.request.protocols.iter().any(|offered| offered == protocol) { - return Err(Error::Web(format!("subprotocol {protocol:?} was not offered"))); - } - encoded = encoded.with_protocol(protocol); + /// Reply to the CONNECT request with a successful WebTransport response. + pub async fn respond(mut self, response: impl Into) -> Result { + let response = response.into(); + if response.status != http::StatusCode::OK { + return Err(Error::Web("respond requires OK; use reject for other statuses".into())); + } + if let Some(protocol) = &response.protocol + && !self.request.protocols.contains(protocol) + { + return Err(Error::Web(format!("subprotocol {protocol:?} was not offered"))); } let mut buf = Vec::new(); - encoded.encode(&mut buf).map_err(|err| Error::Web(err.to_string()))?; + response.encode(&mut buf).map_err(|err| Error::Web(err.to_string()))?; write_all(&mut self.send, &buf).await?; self.guard.disarm(); + Ok(Session::establish(self, response)) + } - Ok(Session::establish(self, protocol)) + /// The QUIC connection carrying this handshake. + pub fn connection(&self) -> &Connection { + &self.conn } /// Accept with a `200` and no subprotocol. pub async fn ok(self) -> Result { - self.respond(Response::default()).await + self.respond(proto::ConnectResponse::OK).await } /// Refuse with `reason`, ending the handshake. @@ -311,8 +272,8 @@ impl Request { /// ours) stay open until then: RFC 9114 makes closing one a connection /// error, so tearing them down here would show the peer an H3 failure /// instead of the status it was sent. - pub async fn reject(mut self, reason: Rejected) -> Result<(), Error> { - let response = proto::ConnectResponse::new(reason.status()); + pub async fn reject(mut self, status: http::StatusCode) -> Result<(), Error> { + let response = proto::ConnectResponse::new(status); let mut buf = Vec::new(); response.encode(&mut buf).map_err(|err| Error::Web(err.to_string()))?; write_all(&mut self.send, &buf).await?; @@ -338,54 +299,10 @@ impl Request { } } -/// How to answer a CONNECT the server is accepting. -/// -/// Built with [`default`](Self::default) and the setters below, so a knob -/// added later stays additive. -#[derive(Clone, Debug, Default)] -pub struct Response { - protocol: Option, -} - -impl Response { - /// Select a subprotocol from the ones the peer - /// [offered](Request::protocols), the WebTransport equivalent of ALPN. - /// Answering with one the peer did not offer is an error. - pub fn with_protocol(mut self, protocol: impl Into) -> Self { - self.protocol = Some(protocol.into()); - self - } -} - -/// Why a CONNECT is being refused. -/// -/// Named rather than numeric so callers need no HTTP crate of their own, and -/// so this stays a contract of moq-uring's rather than of whichever `http` -/// version it happens to build against. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -#[non_exhaustive] -pub enum Rejected { - /// The peer is not allowed here (`401`). - Unauthorized, - /// The peer is known and still not allowed here (`403`). - Forbidden, - /// Nothing is served at the requested path (`404`). - NotFound, - /// The request was malformed (`400`). - BadRequest, - /// The server cannot take it right now (`503`). - Unavailable, -} - -impl Rejected { - fn status(self) -> http::StatusCode { - match self { - Self::Unauthorized => http::StatusCode::UNAUTHORIZED, - Self::Forbidden => http::StatusCode::FORBIDDEN, - Self::NotFound => http::StatusCode::NOT_FOUND, - Self::BadRequest => http::StatusCode::BAD_REQUEST, - Self::Unavailable => http::StatusCode::SERVICE_UNAVAILABLE, - } +impl std::ops::Deref for Request { + type Target = proto::ConnectRequest; + fn deref(&self) -> &Self::Target { + &self.request } } @@ -406,7 +323,7 @@ struct Web { header_uni: Bytes, header_bi: Bytes, header_datagram: Bytes, - state: RefCell, + state: kio::Bridge, /// The session's terminal error, in WebTransport code space: a received /// `CloseWebTransportSession` capsule, or the local [`Session::close`]. /// First writer wins. @@ -415,16 +332,16 @@ struct Web { struct State { /// Incoming streams whose headers are still being read. - pending_uni: Vec, - pending_bi: Vec, + pending_uni: FuturesUnordered, + pending_bi: FuturesUnordered, /// Classified application streams nobody has accepted yet. - ready_uni: VecDeque, - ready_bi: VecDeque<(send::Stream, recv::Stream)>, + ready_uni: VecDeque, + ready_bi: VecDeque<(driver_send::Stream, driver_recv::Stream)>, /// Streams held open, never read: control and QPACK, ours and theirs. - held_recv: Vec, - _control_send: Option, + held_recv: [Option; 3], + _control_send: Option, /// The CONNECT stream's send half; taken exactly once by the close path. - connect_send: Option, + connect_send: Option, } /// A MoQ transport over the worker: raw QUIC, or a WebTransport session. @@ -434,17 +351,15 @@ struct State { /// disabled. Clones share the session, which closes with code 0 once its last /// clone and stream are gone. pub struct Session { - park: kio::Park, + opening: Rc>, handles: Rc, conn: Connection, web: Option>, - /// What [`protocol`](web_transport_trait::poll::Session::protocol) + /// What [`protocol`](Self::protocol) /// reports: the selected subprotocol in web mode, the ALPN in raw mode. - protocol: Option, - /// Streams opened but still writing their WebTransport header, handed out - /// only once it is all written, so a reset always delivers it. - opening_uni: Option<(SendStream, Bytes)>, - opening_bi: Option<(SendStream, RecvStream, Bytes)>, + alpn: Option, + request: Option, + response: Option, } impl Session { @@ -458,11 +373,6 @@ impl Session { self.conn.probe() } - /// Receive streams evicted to free a stuck receive window. - pub(crate) fn streams_evicted(&self) -> u64 { - self.conn.streams_evicted() - } - /// The receive windows now in force: the connection's, and the most one stream holds. pub(crate) fn receive_window(&self) -> moq_net::transport::ReceiveWindow { self.conn.receive_window() @@ -471,14 +381,14 @@ impl Session { /// Open a send group at `priority`, for streams to join with /// [`SendStream::set_group`]. The HTTP/3 streams stay outside every /// group, so they go first. - pub fn send_group(&self, priority: i64) -> send::Group { + pub fn send_group(&self, priority: i64) -> driver_send::Group { self.conn.send_group(priority) } /// Send a datagram scheduled by `options`, like one of its group's streams. /// /// See [`Connection::send_datagram`]. - pub fn send_datagram(&self, payload: Bytes, options: send::Options<'_>) -> Result<(), Error> { + pub fn send_datagram(&self, payload: impl Buf, options: driver_send::Options<'_>) -> Result<(), Error> { let Some(web) = &self.web else { return Ok(self.conn.send_datagram(payload, options)?); }; @@ -492,15 +402,15 @@ impl Session { /// Wrap a raw-QUIC connection in a session, with no /// WebTransport layering: streams and datagrams pass through untouched. pub fn raw(conn: Connection) -> Self { - let protocol = conn.protocol().map(str::to_owned); + let alpn = conn.protocol().map(str::to_owned); Self { - park: kio::Park::default(), + opening: Default::default(), handles: Handles::new(&conn, None), conn, web: None, - protocol, - opening_uni: None, - opening_bi: None, + alpn, + request: None, + response: None, } } @@ -511,11 +421,11 @@ impl Session { } /// Assemble the web flavor and spawn its capsule reader. - fn establish(request: Request, protocol: Option) -> Self { + fn establish(request: Request, response: proto::ConnectResponse) -> Self { let Request { conn, guard: _, - request: _, + request, send, recv, held, @@ -546,12 +456,12 @@ impl Session { header_uni: header_uni.into(), header_bi: header_bi.into(), header_datagram: header_datagram.into(), - state: RefCell::new(State { + state: kio::Bridge::new(State { // Still mid-header when the control stream turned up; the // session finishes classifying them, so a pipelined // WebTransport stream is not lost to the handshake ending. pending_uni: pending, - pending_bi: Vec::new(), + pending_bi: FuturesUnordered::new(), ready_uni, ready_bi: VecDeque::new(), held_recv: held, @@ -569,13 +479,13 @@ impl Session { .spawn(async move { read_capsules(capsules, capsule_conn, recv).await }); Self { - park: kio::Park::default(), + opening: Default::default(), handles: Handles::new(&conn, Some(&web)), conn, web: Some(web), - protocol, - opening_uni: None, - opening_bi: None, + alpn: None, + request: Some(request), + response: Some(response), } } @@ -591,13 +501,13 @@ impl Session { impl Clone for Session { fn clone(&self) -> Self { Self { - park: kio::Park::default(), + opening: self.opening.clone(), handles: self.handles.clone(), conn: self.conn.clone(), web: self.web.clone(), - protocol: self.protocol.clone(), - opening_uni: None, - opening_bi: None, + alpn: self.alpn.clone(), + request: self.request.clone(), + response: self.response.clone(), } } } @@ -606,196 +516,364 @@ impl std::fmt::Debug for Session { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Session") .field("web", &self.web.is_some()) - .field("protocol", &self.protocol) + .field("protocol", &self.protocol()) .finish() } } -impl Session { - pub(crate) fn poll_open_request( - &mut self, - cx: &mut Context<'_>, - ) -> Poll, Error>> { - if self.opening_bi.is_none() { - let Some((send, recv)) = self.conn.open_request().map_err(|err| self.map_err(err))? else { - return Poll::Ready(Ok(None)); - }; - let header = self.web.as_ref().map(|web| web.header_bi.clone()).unwrap_or_default(); - self.opening_bi = Some(( - SendStream::new(send, &self.handles), - RecvStream::new(recv, &self.handles), - header, - )); - } - web_transport_trait::poll::Session::poll_open_bi(self, cx).map_ok(Some) - } +#[derive(Default)] +struct Opening { + uni: Option<(SendStream, Bytes)>, + bi: Option<(SendStream, RecvStream, Bytes)>, + request: Option<(SendStream, RecvStream, Bytes)>, } -impl web_transport_trait::poll::Session for Session { - type SendStream = SendStream; - type RecvStream = RecvStream; - type Error = Error; - - fn poll_accept_uni(&mut self, cx: &mut Context<'_>) -> Poll> { - let Some(web) = self.web.clone() else { - let inner = ready!(self.conn.poll_accept_uni(self.park.hold(cx)))?; - return Poll::Ready(Ok(RecvStream::new(inner, &self.handles))); +impl Session { + /// Poll for a unidirectional stream after its session header. + pub fn poll_accept_uni(&self, waiter: &kio::Waiter) -> Poll> { + let Some(web) = &self.web else { + return self + .conn + .poll_accept_uni(waiter) + .map_ok(|recv| RecvStream::new(recv, &self.handles)); }; - - loop { - if let Some(inner) = web.state.borrow_mut().ready_uni.pop_front() { - return Poll::Ready(Ok(RecvStream::new(inner, &self.handles))); - } - - // Adopt every new arrival, then try to classify the pending set. - // Each mid-header stream parks the caller in its own stream's - // waiters, so progress on any of them re-polls us. - let ended = loop { - match self.conn.poll_accept_uni(self.park.hold(cx)) { - Poll::Ready(Ok(recv)) => web.state.borrow_mut().pending_uni.push(PendingUni::new(recv)), - Poll::Ready(Err(err)) => break Some(err), - Poll::Pending => break None, + web.state.poll(waiter, |state, cx| { + loop { + if let Some(recv) = state.ready_uni.pop_front() { + return Poll::Ready(Ok(RecvStream::new(recv, &self.handles))); } - }; - - if !classify_uni(&web, cx) { - // Buffered headers can finish after the connection error. Hand those - // streams out before reporting that no more can arrive. - if web.state.borrow().pending_uni.is_empty() - && let Some(err) = ended - { - return Poll::Ready(Err(unmap_err(err))); + let ended = loop { + match self.conn.poll_accept_uni(waiter) { + Poll::Ready(Ok(recv)) => state.pending_uni.push(PendingUni::new(recv)), + Poll::Ready(Err(err)) => break Some(err), + Poll::Pending => break None, + } + }; + if !classify_uni(web.session_id, state, cx) { + if state.pending_uni.is_empty() + && let Some(err) = ended + { + return Poll::Ready(Err(unmap_err(err))); + } + return Poll::Pending; } - return Poll::Pending; } - } + }) } - fn poll_accept_bi( - &mut self, - cx: &mut Context<'_>, - ) -> Poll> { - let Some(web) = self.web.clone() else { - let (send, recv) = ready!(self.conn.poll_accept_bi(self.park.hold(cx)))?; - return Poll::Ready(Ok(( - SendStream::new(send, &self.handles), - RecvStream::new(recv, &self.handles), - ))); - }; - - loop { - if let Some((send, recv)) = web.state.borrow_mut().ready_bi.pop_front() { - return Poll::Ready(Ok(( - SendStream::new(send, &self.handles), + /// Poll for a bidirectional stream after its session header. + pub fn poll_accept_bi(&self, waiter: &kio::Waiter) -> Poll> { + let Some(web) = &self.web else { + return self.conn.poll_accept_bi(waiter).map_ok(|(send, recv)| { + ( + SendStream::new(send, &self.handles, None), RecvStream::new(recv, &self.handles), - ))); - } - + ) + }); + }; + web.state.poll(waiter, |state, cx| { loop { - match self.conn.poll_accept_bi(self.park.hold(cx)) { - Poll::Ready(Ok((send, recv))) => web.state.borrow_mut().pending_bi.push(PendingBi::new(send, recv)), - Poll::Ready(Err(err)) => return Poll::Ready(Err(unmap_err(err))), - Poll::Pending => break, + if let Some((send, recv)) = state.ready_bi.pop_front() { + return Poll::Ready(Ok(( + SendStream::new(send, &self.handles, None), + RecvStream::new(recv, &self.handles), + ))); + } + let ended = loop { + match self.conn.poll_accept_bi(waiter) { + Poll::Ready(Ok((send, recv))) => state.pending_bi.push(PendingBi::new(send, recv)), + Poll::Ready(Err(err)) => break Some(err), + Poll::Pending => break None, + } + }; + if !classify_bi(web.session_id, state, cx) { + if state.pending_bi.is_empty() + && let Some(err) = ended + { + return Poll::Ready(Err(unmap_err(err))); + } + return Poll::Pending; } } - - if !classify_bi(&web, cx) { - return Poll::Pending; - } - } + }) } - fn poll_open_uni(&mut self, cx: &mut Context<'_>) -> Poll> { - if self.opening_uni.is_none() { - let inner = ready!(self.conn.poll_open_uni(self.park.hold(cx), Default::default())) + /// Poll to open a unidirectional stream scheduled by `options`. + pub fn poll_open_uni( + &self, + waiter: &kio::Waiter, + options: driver_send::Options<'_>, + ) -> Poll> { + self.opening.poll(waiter, |opening, cx| { + if opening.uni.is_none() { + let inner = ready!(self.conn.poll_open_uni( + waiter, + driver_send::Options { + group: None, + priority: i64::MAX + } + )) .map_err(|err| self.map_err(err))?; - let header = self.web.as_ref().map(|web| web.header_uni.clone()).unwrap_or_default(); - self.opening_uni = Some((SendStream::new(inner, &self.handles), header)); - } - let (send, header) = self.opening_uni.as_mut().expect("opening"); - let result = ready!(send.poll_header(cx, header)); - let (send, _) = self.opening_uni.take().expect("opening"); - Poll::Ready(result.map(|()| send)) + let header = self.web.as_ref().map(|web| web.header_uni.clone()).unwrap_or_default(); + opening.uni = Some((SendStream::new(inner, &self.handles, None), header)); + } + let (send, header) = opening.uni.as_mut().unwrap(); + let result = ready!(send.poll_header(cx, header)).map_err(Error::from); + let (mut send, _) = opening.uni.take().unwrap(); + result?; + send.set_priority(options.priority); + send.set_group(options.group); + Poll::Ready(Ok(send)) + }) } - fn poll_open_bi( - &mut self, - cx: &mut Context<'_>, - ) -> Poll> { - if self.opening_bi.is_none() { - let (send, recv) = ready!(self.conn.poll_open_bi(self.park.hold(cx), Default::default())) - .map_err(|err| self.map_err(err))?; - let header = self.web.as_ref().map(|web| web.header_bi.clone()).unwrap_or_default(); - self.opening_bi = Some(( - SendStream::new(send, &self.handles), - RecvStream::new(recv, &self.handles), - header, - )); - } - let (send, _, header) = self.opening_bi.as_mut().expect("opening"); - let result = ready!(send.poll_header(cx, header)); - let (send, recv, _) = self.opening_bi.take().expect("opening"); - Poll::Ready(result.map(|()| (send, recv))) + /// Poll to open a bidirectional stream scheduled by `options`. + pub fn poll_open_bi( + &self, + waiter: &kio::Waiter, + options: driver_send::Options<'_>, + ) -> Poll> { + self.poll_open_pair(waiter, options, false) + .map_ok(|pair| pair.expect("ordinary opens wait for credit")) } - fn poll_send_datagram(&mut self, _: &mut Context<'_>, payload: &[u8]) -> Poll> { - Poll::Ready(Self::send_datagram( - self, - Bytes::copy_from_slice(payload), - Default::default(), - )) + /// Open a request without waiting for stream credit; pending only while writing its header. + pub fn poll_open_request( + &self, + waiter: &kio::Waiter, + options: driver_send::Options<'_>, + ) -> Poll, Error>> { + self.poll_open_pair(waiter, options, true) } - fn poll_recv_datagram(&mut self, cx: &mut Context<'_>) -> Poll> { - let Some(web) = self.web.clone() else { - return self.conn.poll_recv_datagram(self.park.hold(cx)); - }; - loop { - let datagram = ready!(self.conn.poll_recv_datagram(self.park.hold(cx))).map_err(unmap_err)?; - // The prefix names the session; anything else is not ours. - let mut peek: &[u8] = &datagram; - match decode_varint(&mut peek) { - Some(id) if id == web.session_id => { - let start = datagram.len() - peek.len(); - return Poll::Ready(Ok(datagram.slice(start..))); - } - _ => tracing::debug!("dropping a datagram for an unknown session"), + fn poll_open_pair( + &self, + waiter: &kio::Waiter, + options: driver_send::Options<'_>, + request: bool, + ) -> Poll, Error>> { + self.opening.poll(waiter, |opening, cx| { + let slot = if request { &mut opening.request } else { &mut opening.bi }; + if slot.is_none() { + let (mut send, recv) = if request { + let Some(pair) = self.conn.open_request().map_err(|e| self.map_err(e))? else { + return Poll::Ready(Ok(None)); + }; + pair + } else { + ready!(self.conn.poll_open_bi( + waiter, + driver_send::Options { + group: None, + priority: i64::MAX + } + )) + .map_err(|e| self.map_err(e))? + }; + send.set_priority(i64::MAX); + let header = self.web.as_ref().map(|web| web.header_bi.clone()).unwrap_or_default(); + *slot = Some(( + SendStream::new(send, &self.handles, None), + RecvStream::new(recv, &self.handles), + header, + )); } - } + let (send, _, header) = slot.as_mut().unwrap(); + let result = ready!(send.poll_header(cx, header)).map_err(Error::from); + let (mut send, recv, _) = slot.take().unwrap(); + result?; + send.set_priority(options.priority); + send.set_group(options.group); + Poll::Ready(Ok(Some((send, recv)))) + }) } - fn max_datagram_size(&self) -> usize { - let inner = self.conn.max_datagram_size().unwrap_or(0); - match &self.web { - Some(web) => inner.saturating_sub(web.header_datagram.len()), - None => inner, + /// Poll for the next datagram after its session header. + pub fn poll_recv_datagram(&self, waiter: &kio::Waiter) -> Poll> { + let datagram = ready!(self.conn.poll_recv_datagram(waiter)).map_err(|e| self.map_err(e))?; + let Some(web) = &self.web else { + return Poll::Ready(Ok(datagram)); + }; + let mut peek: &[u8] = &datagram; + match decode_varint(&mut peek) { + Some(id) if id == web.session_id => Poll::Ready(Ok(datagram.slice(datagram.len() - peek.len()..))), + _ => Poll::Ready(Err(Error::Web("unknown datagram session".into()))), } } - fn protocol(&self) -> Option<&str> { - self.protocol.as_deref() - } - - fn close(&mut self, code: u32, reason: &str) { + /// The maximum datagram payload, excluding its session header. + pub fn max_datagram_size(&self) -> usize { + self.conn + .max_datagram_size() + .unwrap_or(0) + .saturating_sub(self.web.as_ref().map_or(0, |web| web.header_datagram.len())) + } + /// The CONNECT request, or `None` for raw QUIC. + pub fn request(&self) -> Option<&proto::ConnectRequest> { + self.request.as_ref() + } + /// The CONNECT response, or `None` for raw QUIC. + pub fn response(&self) -> Option<&proto::ConnectResponse> { + self.response.as_ref() + } + /// The negotiated WebTransport subprotocol, or raw QUIC ALPN. + pub fn protocol(&self) -> Option<&str> { + self.response + .as_ref() + .and_then(|response| response.protocol.as_deref()) + .or(self.alpn.as_deref()) + } + /// Close the session with an application code and reason. + pub fn close(&self, code: u32, reason: &str) { close(&self.conn, self.web.as_ref(), code, reason); } + /// Poll until the session's connection has closed. + pub fn poll_closed(&self, waiter: &kio::Waiter) -> Poll { + let err = ready!(self.conn.poll_closed(waiter)); + Poll::Ready( + self.web + .as_ref() + .and_then(|web| web.closed.borrow().clone()) + .unwrap_or_else(|| self.map_err(err)), + ) + } + /// Return connection-level QUIC statistics. + pub fn stats(&self) -> moq_quic::connection::Stats { + self.conn.stats() + } + /// The QUIC connection carrying this session. + pub fn connection(&self) -> &Connection { + &self.conn + } + /// Accept the next unidirectional stream after its session header. + pub async fn accept_uni(&self) -> Result { + kio::wait(|w| self.poll_accept_uni(w)).await + } + /// Accept the next bidirectional stream after its session header. + pub async fn accept_bi(&self) -> Result<(SendStream, RecvStream), Error> { + kio::wait(|w| self.poll_accept_bi(w)).await + } + /// Open a unidirectional stream scheduled by `options`. + pub async fn open_uni(&self, options: driver_send::Options<'_>) -> Result { + kio::wait(|w| self.poll_open_uni(w, options)).await + } + /// Open a bidirectional stream scheduled by `options`. + pub async fn open_bi(&self, options: driver_send::Options<'_>) -> Result<(SendStream, RecvStream), Error> { + kio::wait(|w| self.poll_open_bi(w, options)).await + } + /// Receive a datagram with its session header removed. + pub async fn read_datagram(&self) -> Result { + kio::wait(|w| self.poll_recv_datagram(w)).await + } + /// Wait until the session's connection has closed. + pub async fn closed(&self) -> Error { + kio::wait(|w| self.poll_closed(w)).await + } +} - fn poll_closed(&mut self, cx: &mut Context<'_>) -> Poll { - let err = ready!(self.conn.poll_closed(self.park.hold(cx))); - let Some(web) = &self.web else { - return Poll::Ready(err); - }; - // A recorded close (capsule or local) is the truth; the connection - // error is just its H3-mangled echo. - if let Some(recorded) = web.closed.borrow().clone() { - return Poll::Ready(recorded); +impl moq_net::transport::poll::Session for Session { + type SendStream = SendStream; + type RecvStream = RecvStream; + type Error = Error; + fn poll_accept_uni(&self, waiter: &kio::Waiter) -> Poll> { + Self::poll_accept_uni(self, waiter) + } + fn poll_accept_bi(&self, waiter: &kio::Waiter) -> Poll> { + Self::poll_accept_bi(self, waiter) + } + fn poll_open_uni( + &self, + waiter: &kio::Waiter, + options: moq_net::transport::poll::SendOptions<'_, driver_send::Group>, + ) -> Poll> { + Self::poll_open_uni( + self, + waiter, + driver_send::Options { + group: options.group, + priority: options.priority, + }, + ) + } + fn poll_open_bi( + &self, + waiter: &kio::Waiter, + options: moq_net::transport::poll::SendOptions<'_, driver_send::Group>, + ) -> Poll> { + Self::poll_open_bi( + self, + waiter, + driver_send::Options { + group: options.group, + priority: options.priority, + }, + ) + } + fn poll_open_request( + &self, + waiter: &kio::Waiter, + options: moq_net::transport::poll::SendOptions<'_, driver_send::Group>, + ) -> Poll, Error>> { + Self::poll_open_request( + self, + waiter, + driver_send::Options { + group: options.group, + priority: options.priority, + }, + ) + } + fn send_group(&self, priority: i64) -> Option { + Some(Self::send_group(self, priority)) + } + fn send_datagram( + &self, + payload: Bytes, + options: moq_net::transport::poll::SendOptions<'_, driver_send::Group>, + ) -> Result<(), Error> { + Self::send_datagram( + self, + payload, + driver_send::Options { + group: options.group, + priority: options.priority, + }, + ) + } + fn poll_recv_datagram(&self, waiter: &kio::Waiter) -> Poll> { + Self::poll_recv_datagram(self, waiter) + } + fn max_datagram_size(&self) -> usize { + Self::max_datagram_size(self) + } + fn protocol(&self) -> Option<&str> { + Self::protocol(self) + } + fn address(&self) -> moq_net::transport::Address { + if self.web.is_some() { + moq_net::transport::Address::Uri + } else { + moq_net::transport::Address::Setup } - Poll::Ready(unmap_err(err)) } - - fn stats(&self) -> impl web_transport_trait::Stats { + fn close(&self, code: u32, reason: &str) { + Self::close(self, code, reason); + } + fn poll_closed(&self, waiter: &kio::Waiter) -> Poll { + Self::poll_closed(self, waiter) + } + fn metrics(&self) -> impl moq_net::transport::Metrics { self.conn.transport_stats() } + fn wire(&self) -> Option { + Some(Self::wire(self)) + } + fn probe(&self) -> Option { + Self::probe(self) + } + fn receive_window(&self) -> Option { + Some(Self::receive_window(self)) + } } /// Close the session: in web mode with a `CloseWebTransportSession` capsule @@ -817,7 +895,7 @@ fn close(conn: &Connection, web: Option<&Rc>, code: u32, reason: &str) { }); } - let connect_send = web.state.borrow_mut().connect_send.take(); + let connect_send = web.state.lock().connect_send.take(); let http3 = proto::error_to_http3(code); let Some(send) = connect_send else { conn.close_code(http3, reason); @@ -875,7 +953,7 @@ fn close(conn: &Connection, web: Option<&Rc>, code: u32, reason: &str) { } } if !frame.has_remaining() { - finished = send.take().map(send::Stream::finish); + finished = send.take().map(driver_send::Stream::finish); } } @@ -908,326 +986,49 @@ impl Handles { } } -impl Drop for Handles { - fn drop(&mut self) { - close(&self.conn, self.web.as_ref(), 0, "dropped"); - } -} - -/// An outgoing stream, WebTransport-framed when its session is. -/// -/// A web stream we opened is handed out with its header already written, so a -/// reset still delivers the header and the peer can tell which session the -/// reset belongs to. -pub struct SendStream { - park: kio::Park, - state: SendState, - /// The end of the WebTransport header on a stream we opened, for a reset - /// to deliver. - header: Option, - web: bool, - /// Last, so the stream's own reset goes out before a close it triggers. - _session: Rc, -} - -/// Where the stream is, since `web_transport_trait` finishes and resets through `&mut self`. -enum SendState { - Open(send::Stream), - Finished(send::Finished), - /// Reset, by us or by echoing the peer's stop. - Reset, -} - -impl SendStream { - fn new(mut inner: send::Stream, session: &Rc) -> Self { - let web = session.web.is_some(); - // The driver's raw 0 reads to a browser as an HTTP/3 stream error rather than the - // WebTransport cancellation that dropping a stream means. moq cancels subscriptions by - // dropping, so this is the ordinary path. - if web { - inner.set_drop_reset(None, http3_code(proto::error_to_http3(0))); - } - Self { - park: kio::Park::default(), - state: SendState::Open(inner), - header: None, - web, - _session: session.clone(), - } - } - - fn map(&self, err: impl Into) -> Error { - match self.web { - true => unmap_err(err.into()), - false => unmap_legacy(err.into()), - } - } - - /// Set the priority within the stream's send group, or session-wide when ungrouped. - pub fn set_priority(&mut self, priority: i64) { - match &mut self.state { - SendState::Open(stream) => stream.set_priority(priority), - SendState::Finished(stream) => stream.set_priority(priority), - SendState::Reset => {} - } - } - - /// Move the stream into a send group, or with `None` back out. See - /// [`send::Stream::set_group`]. - pub fn set_group(&mut self, group: Option<&send::Group>) { - match &mut self.state { - SendState::Open(stream) => stream.set_group(group), - SendState::Finished(stream) => stream.set_group(group), - SendState::Reset => {} - } - } - - /// Write the WebTransport header of a stream we opened, noting where it - /// ends once it is all written. - fn poll_header(&mut self, cx: &mut Context<'_>, header: &mut Bytes) -> Poll> { - if !self.web { - return Poll::Ready(Ok(())); - } - let SendState::Open(stream) = &mut self.state else { - unreachable!("the header goes first"); - }; - while header.has_remaining() { - if let Err(err) = ready!(stream.poll_write(cx, header)) { - return Poll::Ready(Err(self.map(err))); - } - } - let offset = stream.offset(); - stream.set_drop_reset(Some(offset), http3_code(proto::error_to_http3(0))); - self.header = Some(offset); - Poll::Ready(Ok(())) - } - - fn poll_write_any(&mut self, cx: &mut Context<'_>, buf: &mut impl Buf) -> Poll> { - let SendState::Open(stream) = &mut self.state else { - return Poll::Ready(Err(Error::Quic("stream already finished".to_string()))); - }; - match ready!(stream.poll_write(cx, buf)) { - Ok(n) => Poll::Ready(Ok(n)), - Err(err) => Poll::Ready(Err(self.map(err))), - } - } -} - -impl web_transport_trait::poll::SendStream for SendStream { - type Error = Error; - - fn poll_write(&mut self, cx: &mut Context<'_>, mut buf: &[u8]) -> Poll> { - self.poll_write_any(cx, &mut buf) - } - - fn poll_write_buf(&mut self, cx: &mut Context<'_>, buf: &mut B) -> Poll> { - self.poll_write_any(cx, buf) - } - - fn set_priority(&mut self, order: i32) { - SendStream::set_priority(self, order.into()); - } - - fn finish(&mut self) -> Result<(), Self::Error> { - let stream = match std::mem::replace(&mut self.state, SendState::Reset) { - SendState::Open(stream) => stream, - // Already finished or reset, so the FIN it wanted is out. A finished stream keeps - // its handle, which `poll_closed` and `reset` still need. - done => { - self.state = done; - return Ok(()); - } - }; - // A STOP_SENDING beat us here. Carry the code like `poll_write` does, or - // `moq_net::Error::from_transport` cannot decode a routine cancellation. Dropping the - // stream echoes the code. - if let Some(code) = stream.stop_code() { - return Err(self.map(send::Error::Stopped(code))); +impl Handles { + fn encode_stream_code(&self, code: u32) -> VarInt { + if self.web.is_some() { + http3_code(proto::error_to_http3(code)) + } else { + code.into() } - self.state = SendState::Finished(stream.finish()); - Ok(()) } - - fn reset(&mut self, code: u32) { - let code = match self.web { - true => http3_code(proto::error_to_http3(code)), - false => VarInt::from_u32(code), - }; - match std::mem::replace(&mut self.state, SendState::Reset) { - SendState::Open(stream) => match self.header { - Some(header) => stream.reset_at(header, code), - None => stream.reset(code), - }, - SendState::Finished(stream) => match self.header { - Some(header) => stream.reset_at(header, code), - None => stream.reset(code), - }, - SendState::Reset => {} + fn decode_stream_code(&self, code: VarInt) -> Option { + decode_stream_code(self.web.is_none(), code) + } + + fn session_gone(&self, code: VarInt) -> Option { + self.web + .as_ref() + .filter(|_| code.into_inner() == 0x170d7b68) + .map(|web| { + web.closed + .borrow() + .clone() + .unwrap_or_else(|| Error::Web("session gone".into())) + }) + } + fn map(&self, err: Error) -> Error { + if let Some(web) = &self.web { + web.closed.borrow().clone().unwrap_or_else(|| unmap_err(err)) + } else { + err } } - - fn poll_closed(&mut self, cx: &mut Context<'_>) -> Poll> { - let stopped = match &mut self.state { - SendState::Open(stream) => ready!(stream.poll_closed(self.park.hold(cx))), - SendState::Finished(stream) => ready!(stream.poll_closed(self.park.hold(cx))), - SendState::Reset => return Poll::Ready(Ok(())), - }; - Poll::Ready(match stopped { - Ok(None) => Ok(()), - Ok(Some(code)) => Err(self.map(send::Error::Stopped(code))), - Err(err) => Err(self.map(err)), - }) - } } -impl std::fmt::Debug for SendStream { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match &self.state { - SendState::Open(stream) => stream.fmt(f), - SendState::Finished(stream) => stream.fmt(f), - SendState::Reset => f.write_str("Reset"), - } +impl Drop for Handles { + fn drop(&mut self) { + close(&self.conn, self.web.as_ref(), 0, "dropped"); } } -/// An HTTP/3 error code as a QUIC one; every code HTTP/3 maps to fits. +/// An HTTP/3 code as a QUIC varint. fn http3_code(code: u64) -> VarInt { VarInt::from_u64(code).expect("HTTP/3 codes are varints") } -/// An incoming stream; its WebTransport header was consumed on accept. -pub struct RecvStream { - /// Taken by [`stop`](web_transport_trait::poll::RecvStream::stop), since - /// `web_transport_trait` stops through `&mut self`. - inner: Option, - web: bool, - /// Last, so the stream's own stop goes out before a close it triggers. - _session: Rc, -} - -impl RecvStream { - fn new(mut inner: recv::Stream, session: &Rc) -> Self { - let web = session.web.is_some(); - // Same as the send side: the driver stops with a raw 0. - if web { - inner.set_drop_stop(http3_code(proto::error_to_http3(0))); - } - Self { - inner: Some(inner), - web, - _session: session.clone(), - } - } - - /// Lease the stream's unreleased bytes. See [`recv::Stream::poll_lease`]. - pub fn poll_lease(&mut self, cx: &mut Context<'_>, want: usize) -> Poll, Error>> { - let web = self.web; - match &mut self.inner { - Some(inner) => inner.poll_lease(cx, want).map_err(|err| map(web, err.into())), - None => Poll::Ready(Err(Error::Closed)), - } - } - - /// Rank this stream for eviction. See [`recv::Stream::set_priority`]. - pub fn set_priority(&mut self, priority: i64) { - if let Some(inner) = &mut self.inner { - inner.set_priority(priority); - } - } - - /// Poll until the peer resets the stream, the receiver evicts it, or the connection closes, - /// without reading it; pending for good once every byte arrived. See - /// [`recv::Stream::received_reset`]. - pub fn poll_reset(&mut self, cx: &mut Context<'_>) -> Poll { - let web = self.web; - let Some(inner) = &mut self.inner else { - return Poll::Ready(Error::Closed); - }; - match ready!(inner.poll_received_reset(cx)) { - Ok(None) => Poll::Pending, - Ok(Some(code)) => Poll::Ready(map(web, Error::Reset(code.into_inner()))), - Err(err) => Poll::Ready(map(web, err.into())), - } - } -} - -/// Unmap a stream error's code from the session's code space. -fn map(web: bool, err: Error) -> Error { - match web { - true => unmap_err(err), - false => unmap_legacy(err), - } -} - -impl web_transport_trait::poll::RecvStream for RecvStream { - type Error = Error; - - fn poll_read(&mut self, cx: &mut Context<'_>, mut dst: &mut [u8]) -> Poll, Self::Error>> { - let web = self.web; - let Some(inner) = &mut self.inner else { - return Poll::Ready(Err(Error::Closed)); - }; - inner.poll_read(cx, &mut dst).map_err(|err| map(web, err.into())) - } - - fn poll_read_chunk(&mut self, cx: &mut Context<'_>, max: usize) -> Poll, Self::Error>> { - let web = self.web; - let Some(inner) = &mut self.inner else { - return Poll::Ready(Err(Error::Closed)); - }; - if max == 0 { - return Poll::Ready(Ok(Some(Bytes::new()))); - } - inner.poll_read_chunk(cx, max).map_err(|err| map(web, err.into())) - } - - fn stop(&mut self, code: u32) { - let code = match self.web { - true => http3_code(proto::error_to_http3(code)), - false => VarInt::from_u32(code), - }; - if let Some(inner) = self.inner.take() { - inner.stop(code); - } - } - - fn poll_closed(&mut self, cx: &mut Context<'_>) -> Poll> { - let web = self.web; - let Some(inner) = &mut self.inner else { - return Poll::Ready(Ok(())); - }; - Poll::Ready(match ready!(inner.poll_closed(cx)) { - Ok(None) => Ok(()), - Ok(Some(code)) => Err(map(web, Error::Reset(code.into_inner()))), - Err(err) => Err(map(web, err.into())), - }) - } -} - -impl std::fmt::Debug for RecvStream { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match &self.inner { - Some(inner) => inner.fmt(f), - None => f.write_str("Stopped"), - } - } -} - -/// Unmap a raw-QUIC stream code that arrived in the HTTP/3 code space. -/// -/// Older moq-tokio raw sessions map RESET_STREAM and STOP_SENDING -/// codes as if they were WebTransport. No `u32` application code reaches that -/// range, so a code in it can only be the mapped form. Anything else is the -/// peer's own code and passes through. -fn unmap_legacy(err: Error) -> Error { - let unmap = |code: u64| proto::error_from_http3(code).map_or(code, u64::from); - match err { - Error::Reset(code) => Error::Reset(unmap(code)), - Error::Stop(code) => Error::Stop(unmap(code)), - err => err, - } -} - /// Rewrite HTTP/3-mapped error codes back into WebTransport code space. /// /// Only a sparse range of HTTP/3 codes names a WebTransport error. Anything @@ -1278,7 +1079,7 @@ enum VarPoll { } impl VarRead { - fn poll(&mut self, cx: &mut Context<'_>, recv: &mut recv::Stream) -> Poll { + fn poll(&mut self, cx: &mut Context<'_>, recv: &mut driver_recv::Stream) -> Poll { loop { let need = match self.have { 0 => 1, @@ -1332,7 +1133,8 @@ enum UniClass { /// The H3 control stream, positioned right after its type varint. Control, /// A QPACK stream: keep it open, never read it. - Qpack, + QpackEncoder, + QpackDecoder, /// A WebTransport stream claiming this session id. Web(u64), /// Noise (GREASE included); dropping it stops it. @@ -1341,16 +1143,16 @@ enum UniClass { /// An incoming unidirectional stream mid-header. struct PendingUni { - recv: recv::Stream, + recv: Option, typ: VarRead, session: VarRead, typ_value: Option, } impl PendingUni { - fn new(recv: recv::Stream) -> Self { + fn new(recv: driver_recv::Stream) -> Self { Self { - recv, + recv: Some(recv), typ: VarRead::default(), session: VarRead::default(), typ_value: None, @@ -1360,7 +1162,7 @@ impl PendingUni { fn poll_classify(&mut self, cx: &mut Context<'_>) -> Poll { let typ = match self.typ_value { Some(typ) => typ, - None => match ready!(self.typ.poll(cx, &mut self.recv)) { + None => match ready!(self.typ.poll(cx, self.recv.as_mut().expect("not yet classified"))) { VarPoll::Value(typ) => { self.typ_value = Some(typ); typ @@ -1369,12 +1171,14 @@ impl PendingUni { }, }; match typ { - STREAM_WEBTRANSPORT => match ready!(self.session.poll(cx, &mut self.recv)) { + STREAM_WEBTRANSPORT => match ready!(self.session.poll(cx, self.recv.as_mut().expect("not yet classified"))) + { VarPoll::Value(session) => Poll::Ready(UniClass::Web(session)), VarPoll::End => Poll::Ready(UniClass::Unknown), }, STREAM_CONTROL => Poll::Ready(UniClass::Control), - STREAM_QPACK_ENCODER | STREAM_QPACK_DECODER => Poll::Ready(UniClass::Qpack), + STREAM_QPACK_ENCODER => Poll::Ready(UniClass::QpackEncoder), + STREAM_QPACK_DECODER => Poll::Ready(UniClass::QpackDecoder), _ => { tracing::debug!(typ, "ignoring an unknown unidirectional stream"); Poll::Ready(UniClass::Unknown) @@ -1383,55 +1187,52 @@ impl PendingUni { } } -/// Progress every pending unidirectional stream; whether any became ready. -fn classify_uni(web: &Rc, cx: &mut Context<'_>) -> bool { - // Taken out of the RefCell so classification can push into the other - // queues without a re-entrant borrow; everything is single-threaded. - let pending = std::mem::take(&mut web.state.borrow_mut().pending_uni); - let mut keep = Vec::new(); - let mut progressed = false; +impl Future for PendingUni { + type Output = (UniClass, driver_recv::Stream); + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let class = ready!(self.poll_classify(cx)); + Poll::Ready((class, self.recv.take().expect("not yet classified"))) + } +} - for mut stream in pending { - match stream.poll_classify(cx) { - Poll::Pending => keep.push(stream), - Poll::Ready(UniClass::Web(session)) if session == web.session_id => { - web.state.borrow_mut().ready_uni.push_back(stream.recv); +/// Poll only newly arrived or woken headers. +fn classify_uni(session_id: u64, state: &mut State, cx: &mut Context<'_>) -> bool { + let mut progressed = false; + while let Poll::Ready(Some((class, recv))) = state.pending_uni.poll_next_unpin(cx) { + match class { + UniClass::Web(session) if session == session_id => { + state.ready_uni.push_back(recv); progressed = true; } - // A second control stream (or a stray QPACK one) is bogus but - // harmless; holding it beats closing it, which the peer would - // read as tearing down the H3 connection. Only up to a point: a - // finished stream returns its credit, so a peer that keeps opening - // them would grow this vector for as long as it cared to. - Poll::Ready(UniClass::Control | UniClass::Qpack) => { - let mut state = web.state.borrow_mut(); - if state.held_recv.len() < HELD_STREAMS { - state.held_recv.push(stream.recv); - } + UniClass::Control => { + state.held_recv[0].get_or_insert(recv); + } + UniClass::QpackEncoder => { + state.held_recv[1].get_or_insert(recv); + } + UniClass::QpackDecoder => { + state.held_recv[2].get_or_insert(recv); } - Poll::Ready(_) => {} + _ => {} } } - - let mut state = web.state.borrow_mut(); - state.pending_uni.extend(keep); progressed } /// An incoming bidirectional stream mid-header: frame type, then session id. struct PendingBi { - send: send::Stream, - recv: recv::Stream, + send: Option, + recv: Option, typ: VarRead, session: VarRead, typ_value: Option, } impl PendingBi { - fn new(send: send::Stream, recv: recv::Stream) -> Self { + fn new(send: driver_send::Stream, recv: driver_recv::Stream) -> Self { Self { - send, - recv, + send: Some(send), + recv: Some(recv), typ: VarRead::default(), session: VarRead::default(), typ_value: None, @@ -1442,7 +1243,7 @@ impl PendingBi { fn poll_classify(&mut self, cx: &mut Context<'_>) -> Poll> { let typ = match self.typ_value { Some(typ) => typ, - None => match ready!(self.typ.poll(cx, &mut self.recv)) { + None => match ready!(self.typ.poll(cx, self.recv.as_mut().expect("not yet classified"))) { VarPoll::Value(typ) => { self.typ_value = Some(typ); typ @@ -1454,38 +1255,40 @@ impl PendingBi { tracing::debug!(typ, "ignoring an unknown bidirectional stream"); return Poll::Ready(None); } - match ready!(self.session.poll(cx, &mut self.recv)) { + match ready!(self.session.poll(cx, self.recv.as_mut().expect("not yet classified"))) { VarPoll::Value(session) => Poll::Ready(Some(session)), VarPoll::End => Poll::Ready(None), } } } -/// Progress every pending bidirectional stream; whether any became ready. -fn classify_bi(web: &Rc, cx: &mut Context<'_>) -> bool { - let pending = std::mem::take(&mut web.state.borrow_mut().pending_bi); - let mut keep = Vec::new(); - let mut progressed = false; +impl Future for PendingBi { + type Output = (Option, driver_send::Stream, driver_recv::Stream); + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let class = ready!(self.poll_classify(cx)); + Poll::Ready(( + class, + self.send.take().expect("not yet classified"), + self.recv.take().expect("not yet classified"), + )) + } +} - for mut stream in pending { - match stream.poll_classify(cx) { - Poll::Pending => keep.push(stream), - Poll::Ready(Some(session)) if session == web.session_id => { - web.state.borrow_mut().ready_bi.push_back((stream.send, stream.recv)); - progressed = true; - } - Poll::Ready(_) => {} +/// Poll only newly arrived or woken headers. +fn classify_bi(session_id: u64, state: &mut State, cx: &mut Context<'_>) -> bool { + let mut progressed = false; + while let Poll::Ready(Some((class, send, recv))) = state.pending_bi.poll_next_unpin(cx) { + if class == Some(session_id) { + state.ready_bi.push_back((send, recv)); + progressed = true; } } - - let mut state = web.state.borrow_mut(); - state.pending_bi.extend(keep); progressed } // ── Handshake plumbing ────────────────────────────────────────────── -async fn write_all(send: &mut send::Stream, buf: &[u8]) -> Result<(), Error> { +async fn write_all(send: &mut driver_send::Stream, buf: &[u8]) -> Result<(), Error> { Ok(send.write_all(buf).await?) } @@ -1493,7 +1296,7 @@ async fn write_all(send: &mut send::Stream, buf: &[u8]) -> Result<(), Error> { /// /// Fails rather than letting `buf` grow past [`MESSAGE_LIMIT`], which is what /// bounds a peer that declares a huge frame and never finishes it. -async fn read_some(recv: &mut recv::Stream, buf: &mut BytesMut) -> Result { +async fn read_some(recv: &mut driver_recv::Stream, buf: &mut BytesMut) -> Result { let mut chunk = [0u8; 4096]; let n = recv.read(&mut &mut chunk[..]).await?; match n { @@ -1510,7 +1313,7 @@ async fn read_some(recv: &mut recv::Stream, buf: &mut BytesMut) -> Result Result { +async fn read_settings(recv: &mut driver_recv::Stream) -> Result { let mut buf = BytesMut::new(); // The decoder expects the stream to start with its type; re-seed the one // byte classification took. @@ -1529,7 +1332,7 @@ async fn read_settings(recv: &mut recv::Stream) -> Result Result { +async fn read_connect(recv: &mut driver_recv::Stream) -> Result { let mut buf = BytesMut::new(); loop { let mut peek: &[u8] = &buf; @@ -1549,7 +1352,7 @@ async fn read_connect(recv: &mut recv::Stream) -> Result, conn: Connection, mut recv: recv::Stream) { +async fn read_capsules(web: Rc, conn: Connection, mut recv: driver_recv::Stream) { let mut capsules = Capsules::default(); let capsule = loop { match capsules.take() { @@ -1648,6 +1451,27 @@ impl Capsules { } } +impl From for Error { + fn from(err: send::Error) -> Self { + match err { + send::Error::SessionError(err) => err, + send::Error::Stopped(code) => Self::Stop(code.into()), + err => Self::Web(err.to_string()), + } + } +} + +impl std::fmt::Debug for Handles { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.conn.fmt(f) + } +} + +/// Decode only this stream code registry; raw QUIC also accepts older peers' mapped codes. +fn decode_stream_code(raw: bool, code: VarInt) -> Option { + proto::error_from_http3(code.into_inner()).or_else(|| raw.then(|| u32::try_from(code.into_inner()).ok()).flatten()) +} + #[cfg(test)] mod tests { use super::*; @@ -1657,7 +1481,7 @@ mod tests { /// application error through the trait accessors. #[test] fn an_unmappable_code_is_not_an_application_error() { - use web_transport_trait::Error as _; + use moq_net::transport::Error as _; let app = unmap_err(Error::App { code: proto::error_to_http3(7), @@ -1689,29 +1513,15 @@ mod tests { )); } - /// A raw session reads a plain stream code as is, and an older raw peer's - /// HTTP/3-mapped one as the code it stands for. Session closes are left alone. #[test] - fn a_raw_session_unmaps_legacy_stream_codes() { - assert!(matches!(unmap_legacy(Error::Reset(5)), Error::Reset(5))); - assert!(matches!(unmap_legacy(Error::Stop(5)), Error::Stop(5))); - assert!(matches!( - unmap_legacy(Error::Reset(proto::error_to_http3(5))), - Error::Reset(5) - )); - assert!(matches!( - unmap_legacy(Error::Stop(proto::error_to_http3(5))), - Error::Stop(5) - )); - - let code = proto::error_to_http3(5); - assert!(matches!( - unmap_legacy(Error::App { - code, - reason: String::new() - }), - Error::App { code: c, .. } if c == code - )); + fn stream_codes_preserve_their_registry() { + let mapped = http3_code(proto::error_to_http3(5)); + for raw in [false, true] { + assert_eq!(decode_stream_code(raw, mapped), Some(5)); + assert_eq!(decode_stream_code(raw, http3_code(1 << 40)), None); + } + assert_eq!(decode_stream_code(true, 5u32.into()), Some(5)); + assert_eq!(decode_stream_code(false, 5u32.into()), None); } /// The CONNECT request is logged here, so its `Debug` must keep credentials diff --git a/rs/moq-uring/src/quic/web/recv.rs b/rs/moq-uring/src/quic/web/recv.rs new file mode 100644 index 00000000000..3115c2deddd --- /dev/null +++ b/rs/moq-uring/src/quic/web/recv.rs @@ -0,0 +1,295 @@ +//! Incoming WebTransport streams, their leases, and errors. + +use std::{ + rc::Rc, + task::{Context, Poll, ready}, +}; + +use bytes::{BufMut, Bytes}; + +use super::Handles; +use crate::quic::Error as SessionError; +use crate::quic::driver::recv; + +/// A stream that can be used to receive bytes. See [`crate::quic::recv::Stream`]. +/// +/// Dropping it before the end stops it with code 0. +#[derive(Debug)] +pub struct Stream { + /// Taken when moq-net stops through its mutable poll trait. + inner: Option, + close: Rc, + id: crate::quic::stream::Id, +} + +impl Stream { + pub(super) fn new(mut stream: recv::Stream, close: &Rc) -> Self { + // The driver's raw 0 is outside the HTTP/3 range, so a WebTransport peer can't decode it. + stream.set_drop_stop(close.encode_stream_code(0)); + Self { + id: stream.id(), + inner: Some(stream), + close: close.clone(), + } + } + + fn inner(&mut self) -> Result<&mut recv::Stream, Error> { + self.inner.as_mut().ok_or(Error::ClosedStream) + } + + /// Lease the stream's unreleased bytes. See [`recv::Stream::poll_lease`]. + pub fn poll_lease(&mut self, cx: &mut Context<'_>, want: usize) -> Poll, Error>> { + let close = &self.close; + match &mut self.inner { + Some(inner) => inner.poll_lease(cx, want).map_err(|e| map_error(close, e)), + None => Poll::Ready(Err(Error::ClosedStream)), + } + } + + /// Poll until the peer resets the stream, the receiver evicts it, or the session closes, + /// without reading it; pending for good once every byte arrived. See + /// [`recv::Stream::received_reset`]. + pub fn poll_reset(&mut self, cx: &mut Context<'_>) -> Poll { + let close = &self.close; + let Some(inner) = &mut self.inner else { + return Poll::Ready(Error::ClosedStream); + }; + match ready!(inner.poll_received_reset(cx)) { + Ok(None) => Poll::Pending, + Ok(Some(code)) => Poll::Ready(map_error(close, recv::Error::Reset(code))), + Err(err) => Poll::Ready(map_error(close, err)), + } + } + + /// Rank this stream for eviction. See [`recv::Stream::set_priority`]. + pub fn set_priority(&mut self, priority: i64) { + if let Some(inner) = &mut self.inner { + inner.set_priority(priority); + } + } + + /// Tell the other end to stop sending data with the given error code. See + /// [`recv::Stream::stop`]. This is a u32 with WebTransport since it shares the error + /// space with HTTP/3. A raw QUIC session sends the code as is. Does nothing once stopped. + fn stop_mut(&mut self, code: u32) { + if let Some(inner) = self.inner.take() { + inner.stop(self.close.encode_stream_code(code)); + } + } + + /// Stop receiving and tell the peer why. + pub fn stop(mut self, code: u32) { + self.stop_mut(code); + } + + /// Wait until the stream ends after its bytes are read, the peer resets it, or the session closes. + pub async fn closed(&mut self) -> Result<(), Error> { + kio::wait(|waiter| self.poll_closed(waiter)).await + } + + /// Poll until the stream ends after its bytes are read, the peer resets it, or the session closes. + pub fn poll_closed(&mut self, waiter: &kio::Waiter) -> Poll> { + let close = &self.close; + let Some(inner) = &mut self.inner else { + return Poll::Ready(Ok(())); + }; + match ready!(inner.poll_closed(&waiter.context())) { + Ok(None) => Poll::Ready(Ok(())), + Ok(Some(code)) => Poll::Ready(Err(map_error(close, recv::Error::Reset(code)))), + Err(err) => Poll::Ready(Err(map_error(close, err))), + } + } + + /// Poll for some bytes without retaining the destination buffer. + pub fn poll_read(&mut self, waiter: &kio::Waiter, mut buf: &mut [u8]) -> Poll, Error>> { + let close = self.close.clone(); + self.inner()? + .poll_read(&waiter.context(), &mut buf) + .map_err(|err| map_error(&close, err)) + } + + /// Read some data into `buf` and return the amount read. See [`recv::Stream::read`]. + pub async fn read(&mut self, buf: &mut impl BufMut) -> Result, Error> { + let result = self.inner()?.read(buf).await; + result.map_err(|e| map_error(&self.close, e)) + } + + /// Fill the entire buffer with data. See [`recv::Stream::read_exact`]. + pub async fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), Error> { + let result = self.inner()?.read_exact(buf).await; + result.map_err(|e| map_error(&self.close, e)) + } + + /// Read a chunk of data from the stream. See [`recv::Stream::read_chunk`]. + pub async fn read_chunk(&mut self, max: usize) -> Result, Error> { + let result = self.inner()?.read_chunk(max).await; + result.map_err(|e| map_error(&self.close, e)) + } + + /// Read until the end of the stream or the limit is hit. See [`recv::Stream::read_to_end`]. + pub async fn read_to_end(&mut self, limit: usize) -> Result, Error> { + let result = self.inner()?.read_to_end(limit).await; + result.map_err(|e| map_error(&self.close, e)) + } + + /// Block until the stream has been reset and return the error code. See + /// [`recv::Stream::received_reset`]. + /// + /// Returns the stream's reset code, or fails with its session error or + /// [`Error::Evicted`] once the receiver evicted it to free a stuck window. + pub async fn received_reset(&mut self) -> Result, Error> { + let Some(inner) = &mut self.inner else { + return Ok(None); + }; + match inner.received_reset().await { + Ok(None) => Ok(None), + Ok(Some(code)) => match map_error(&self.close, recv::Error::Reset(code)) { + Error::Reset(code) => Ok(Some(code)), + err => Err(err), + }, + Err(err) => Err(map_error(&self.close, err)), + } + } + + /// Return the underlying QUIC stream ID. + /// + /// > **Warning** + /// > + /// > WebTransport sessions share the QUIC connection with HTTP/3 and potentially other + /// > sessions. + /// > The [crate::quic::stream::Id::index] might not increment by 1 like expected when using raw QUIC. + /// > This is why the Javascript WebTransport API does not expose the Stream ID. + pub fn id(&self) -> crate::quic::stream::Id { + self.id + } + + // We purposely don't expose the 0RTT because it's not valid with WebTransport +} + +/// Decode the peer's reset code, and report a session error, or a reset for a session +/// that is gone, as the session's close reason. +fn map_error(close: &Handles, e: recv::Error) -> Error { + match e { + recv::Error::Reset(code) => match close.decode_stream_code(code) { + Some(code) => Error::Reset(code), + None => match close.session_gone(code) { + Some(err) => Error::SessionError(err), + None => Error::InvalidReset(code), + }, + }, + e => match Error::from(e) { + Error::SessionError(e) => Error::SessionError(close.map(e)), + e => e, + }, + } +} + +impl moq_net::transport::poll::RecvStream for Stream { + type Error = Error; + type Lease<'a> = recv::Lease<'a>; + fn poll_lease(&mut self, waiter: &kio::Waiter, want: usize) -> Poll, Self::Error>> { + Self::poll_lease(self, &mut waiter.context(), want) + } + fn set_priority(&mut self, priority: i64) { + Self::set_priority(self, priority); + } + fn stop(&mut self, code: u32) { + Self::stop_mut(self, code); + } + fn poll_reset(&mut self, waiter: &kio::Waiter) -> Poll { + Self::poll_reset(self, &mut waiter.context()) + } +} +impl moq_net::transport::poll::Lease for recv::Lease<'_> { + fn is_fin(&self) -> bool { + Self::is_fin(self) + } + fn chunks(&self) -> impl Iterator { + Self::chunks(self) + } + fn release(self) { + Self::release(self); + } +} + +/// An error when reading from [`crate::quic::web::recv::Stream`]. Similar to [`crate::quic::recv::Error`]. +#[derive(Clone, thiserror::Error, Debug)] +#[non_exhaustive] +pub enum Error { + /// The session carrying the stream failed. + #[error("session error: {0}")] + SessionError(#[from] SessionError), + + /// The peer reset this stream with its application code. + #[error("RESET_STREAM: {0}")] + Reset(u32), + + /// The peer reset the stream with a code outside the WebTransport registry. + #[error("invalid RESET_STREAM: {0}")] + InvalidReset(crate::quic::VarInt), + + /// The stream was already stopped. + #[error("stream already closed")] + ClosedStream, + + /// The connection's receive window was stuck and this stream was discarded to free it. + #[error("evicted")] + Evicted, + + /// The stream finished before [`read_exact`](crate::quic::web::recv::Stream::read_exact) + /// filled its buffer, after this many bytes. + #[error("finished early")] + FinishedEarly(usize), + + /// The stream is longer than [`read_to_end`](crate::quic::web::recv::Stream::read_to_end)'s limit. + #[error("too long")] + TooLong, + + /// A lease wanted more than the connection receive window can hold. + #[error("want exceeds the receive window")] + TooLarge, +} + +impl From for Error { + fn from(value: crate::quic::recv::Error) -> Self { + use crate::quic::recv::Error as DriverError; + match value { + DriverError::Reset(code) => match web_transport_proto::error_from_http3(code.into_inner()) { + Some(code) => Error::Reset(code), + None => Error::InvalidReset(code), + }, + DriverError::Connection(e) => Error::SessionError(e), + DriverError::Evicted => Error::Evicted, + DriverError::FinishedEarly(n) => Error::FinishedEarly(n), + DriverError::TooLong => Error::TooLong, + DriverError::TooLarge => Error::TooLarge, + } + } +} + +impl moq_net::transport::Error for Error { + fn close_kind(&self) -> moq_net::transport::CloseKind { + match self { + Self::Evicted => moq_net::transport::CloseKind::Evicted, + Self::SessionError(e) => e.close_kind(), + _ => moq_net::transport::CloseKind::Transport, + } + } + fn session_error(&self) -> Option<(u32, String)> { + if let Error::SessionError(e) = self { + return e.session_error(); + } + + None + } + + fn stream_error(&self) -> Option { + match self { + Error::Reset(code) => Some(*code), + _ => None, + } + } +} + +/// The driver's receive lease, which retains flow-control credit until released. +pub use crate::quic::recv::Lease; diff --git a/rs/moq-uring/src/quic/web/send.rs b/rs/moq-uring/src/quic/web/send.rs new file mode 100644 index 00000000000..e7496c5848b --- /dev/null +++ b/rs/moq-uring/src/quic/web/send.rs @@ -0,0 +1,359 @@ +//! Outgoing WebTransport streams, their finish handles, and errors. + +use std::{ + pin::Pin, + rc::Rc, + task::{Context, Poll}, +}; + +use bytes::Buf; + +use super::Handles; +use crate::quic::Error as SessionError; +use crate::quic::driver::send; + +/// A stream that can be used to send bytes. See [`crate::quic::send::Stream`]. +/// +/// Dropping it before [`finish`](Self::finish) resets it with code 0, still delivering the +/// WebTransport header. +/// +/// This wrapper is mainly needed for error codes, which is unfortunate. +/// WebTransport uses u32 error codes and they're mapped in a reserved HTTP/3 error space. +#[derive(Debug)] +pub struct Stream { + state: State, + close: Rc, + /// The end of the WebTransport header this side wrote first, which a reset must still deliver. + header: Option, + id: crate::quic::stream::Id, +} + +/// The mutable terminal state required by moq-net's poll trait. +#[derive(Debug)] +enum State { + Open(send::Stream), + Finished(send::Finished), + /// Reset, with the peer's stop code if it arrived first. + Reset(Result, send::Error>), +} + +impl Stream { + pub(super) fn new(mut stream: send::Stream, close: &Rc, header: Option) -> Self { + stream.set_drop_reset(header, close.encode_stream_code(0)); + Self { + id: stream.id(), + state: State::Open(stream), + close: close.clone(), + header, + } + } + + /// Decode the peer's stop code, and report a session error, or a stop for a session + /// that is gone, as the session's close reason. + fn map_error(&self, e: send::Error) -> Error { + match e { + send::Error::Stopped(code) => match self.close.decode_stream_code(code) { + Some(code) => Error::Stopped(code), + None => match self.close.session_gone(code) { + Some(err) => Error::SessionError(err), + None => Error::InvalidStopped(code), + }, + }, + e => match Error::from(e) { + Error::SessionError(e) => Error::SessionError(self.close.map(e)), + e => e, + }, + } + } + + fn open(&mut self) -> Result<&mut send::Stream, Error> { + match &mut self.state { + State::Open(stream) => Ok(stream), + _ => Err(Error::ClosedStream), + } + } + + /// Abruptly reset the stream with the provided error code. See [`send::Stream::reset`]. + /// This is a u32 with WebTransport because we share the error space with HTTP/3. + /// A raw QUIC session sends the code as is. Does nothing once already reset. + /// + /// The WebTransport header still reaches the peer, so it can tell which session the reset + /// belongs to. + fn reset_mut(&mut self, code: u32) { + let code = self.close.encode_stream_code(code); + let stopped = match &self.state { + State::Open(stream) => stream.poll_closed(&kio::Waiter::noop()), + State::Finished(stream) => stream.poll_closed(&kio::Waiter::noop()), + State::Reset(_) => return, + }; + let stopped = match stopped { + Poll::Ready(result) => result, + Poll::Pending => Ok(None), + }; + match std::mem::replace(&mut self.state, State::Reset(stopped)) { + State::Open(stream) => match self.header { + Some(header) => stream.reset_at(header, code), + None => stream.reset(code), + }, + State::Finished(stream) => match self.header { + Some(header) => stream.reset_at(header, code), + None => stream.reset(code), + }, + State::Reset(_) => unreachable!(), + } + } + + /// Poll until all data was acknowledged, the peer stopped the stream, or the session closed. + pub fn poll_closed(&self, waiter: &kio::Waiter) -> Poll, Error>> { + let result = match &self.state { + State::Open(stream) => stream.poll_closed(waiter), + State::Finished(stream) => stream.poll_closed(waiter), + State::Reset(result) => Poll::Ready(result.clone()), + }; + result.map(|result| match result { + Ok(None) => Ok(None), + Ok(Some(code)) => match self.map_error(send::Error::Stopped(code)) { + Error::Stopped(code) => Ok(Some(code)), + err => Err(err), + }, + Err(err) => Err(self.map_error(err)), + }) + } + + /// Wait until all data was acknowledged, the peer stopped the stream, or the session closed. + pub async fn stopped(&self) -> Result, Error> { + kio::wait(|w| self.poll_closed(w)).await + } + + /// Finish sending, retaining a handle to acknowledgements and peer cancellation. + pub fn finish(mut self) -> Finished { + self.finish_mut(); + Finished { + stream: self, + park: Default::default(), + } + } + + /// Reset the stream while still delivering its WebTransport header. + pub fn reset(mut self, code: u32) { + self.reset_mut(code); + } + + pub(super) fn poll_header(&mut self, cx: &mut Context<'_>, header: &mut bytes::Bytes) -> Poll> { + while header.has_remaining() { + let result = std::task::ready!(self.open()?.poll_write(cx, header)); + result.map_err(|err| self.map_error(err))?; + } + let offset = self.open()?.offset(); + let code = self.close.encode_stream_code(0); + self.open()?.set_drop_reset(Some(offset), code); + self.header = self.close.web.as_ref().map(|_| offset); + Poll::Ready(Ok(())) + } + + /// Write from `buf`, returning the size written. See [`send::Stream::write`]. + pub async fn write(&mut self, buf: &mut impl Buf) -> Result { + let result = self.open()?.write(buf).await; + result.map_err(|e| self.map_error(e)) + } + + /// Write all of `buf` to the stream. See [`send::Stream::write_all`]. + pub async fn write_all(&mut self, buf: impl Buf) -> Result<(), Error> { + let result = self.open()?.write_all(buf).await; + result.map_err(|e| self.map_error(e)) + } + + /// Mark the stream as finished, such that no more data can be written. See + /// [`send::Stream::finish`]. Fails if it was already finished or reset. + fn finish_mut(&mut self) { + if matches!(self.state, State::Open(_)) { + let State::Open(stream) = std::mem::replace(&mut self.state, State::Reset(Ok(None))) else { + unreachable!() + }; + self.state = State::Finished(stream.finish()); + } + } + + /// Set the priority within the stream's send group, or session-wide when ungrouped. + pub fn set_priority(&mut self, priority: i64) { + match &mut self.state { + State::Open(stream) => stream.set_priority(priority), + State::Finished(stream) => stream.set_priority(priority), + State::Reset(_) => {} + } + } + + /// The stream's priority. See [`send::Stream::priority`]. + pub fn priority(&self) -> i64 { + match &self.state { + State::Open(stream) => stream.priority(), + State::Finished(stream) => stream.priority(), + State::Reset(_) => 0, + } + } + + /// Move the stream into a send group, or with `None` back out. See [`send::Stream::set_group`]. + pub fn set_group(&mut self, group: Option<&send::Group>) { + match &mut self.state { + State::Open(stream) => stream.set_group(group), + State::Finished(stream) => stream.set_group(group), + State::Reset(_) => {} + } + } + + /// Return the underlying QUIC stream ID. + /// + /// > **Warning** + /// > + /// > WebTransport sessions share the QUIC connection with HTTP/3 and potentially other + /// > sessions. + /// > The [crate::quic::stream::Id::index] might not increment by 1 like expected when using raw QUIC. + /// > This is why the Javascript WebTransport API does not expose the Stream ID. + pub fn id(&self) -> crate::quic::stream::Id { + self.id + } +} + +impl moq_net::transport::poll::SendStream for Stream { + type Error = Error; + type Group = send::Group; + + fn poll_write(&mut self, waiter: &kio::Waiter, mut buf: &[u8]) -> Poll> { + self.poll_write_buf(waiter, &mut buf) + } + fn poll_write_buf(&mut self, waiter: &kio::Waiter, buf: &mut B) -> Poll> { + let result = self.open()?.poll_write(&waiter.context(), buf); + result.map_err(|e| self.map_error(e)) + } + fn set_priority(&mut self, priority: i64) { + Self::set_priority(self, priority); + } + fn set_group(&mut self, group: &Self::Group) { + Self::set_group(self, Some(group)); + } + fn finish(&mut self) -> Result<(), Self::Error> { + Self::finish_mut(self); + Ok(()) + } + fn reset(&mut self, code: u32) { + Self::reset_mut(self, code); + } + fn poll_closed(&self, waiter: &kio::Waiter) -> Poll> { + Self::poll_closed(self, waiter).map(|result| match result { + Ok(None) => Ok(()), + Ok(Some(code)) => Err(Error::Stopped(code)), + Err(err) => Err(err), + }) + } +} + +impl moq_net::transport::poll::SendGroup for send::Group { + fn set_priority(&mut self, priority: i64) { + Self::set_priority(self, priority); + } +} + +/// A finished stream whose data keeps transmitting until acknowledged or stopped. +#[derive(Debug)] +pub struct Finished { + stream: Stream, + park: kio::Park, +} +impl Finished { + /// Poll until the peer acknowledges or stops the stream, or the session closes. + pub fn poll_closed(&self, waiter: &kio::Waiter) -> Poll, Error>> { + self.stream.poll_closed(waiter) + } + /// Wait until the peer acknowledges or stops the stream, or the session closes. + pub async fn stopped(&self) -> Result, Error> { + self.stream.stopped().await + } + /// Reset the stream while still delivering its WebTransport header. + pub fn reset(self, code: u32) { + self.stream.reset(code); + } + /// Set the stream's priority within its send group. + pub fn set_priority(&mut self, priority: i64) { + self.stream.set_priority(priority); + } + /// Return the stream's priority within its send group. + pub fn priority(&self) -> i64 { + self.stream.priority() + } + /// Move the stream to a send group, or out with `None`. + pub fn set_group(&mut self, group: Option<&send::Group>) { + self.stream.set_group(group); + } + /// Return the underlying QUIC stream ID. + pub fn id(&self) -> crate::quic::stream::Id { + self.stream.id() + } +} +impl Future for Finished { + type Output = Result, Error>; + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = &mut *self; + this.stream.poll_closed(this.park.hold(cx)) + } +} + +/// An error when writing to [`crate::quic::web::send::Stream`]. Similar to [`crate::quic::send::Error`]. +#[derive(Clone, thiserror::Error, Debug)] +#[non_exhaustive] +pub enum Error { + /// The peer stopped accepting this stream with its application code. + #[error("STOP_SENDING: {0}")] + Stopped(u32), + + /// The peer stopped the stream with a code outside the WebTransport registry. + #[error("invalid STOP_SENDING: {0}")] + InvalidStopped(crate::quic::VarInt), + + /// The session carrying the stream failed. + #[error("session error: {0}")] + SessionError(#[from] SessionError), + + /// The stream was already finished or reset. + #[error("stream closed")] + ClosedStream, +} + +impl From for Error { + fn from(e: crate::quic::send::Error) -> Self { + match e { + crate::quic::send::Error::Stopped(code) => match web_transport_proto::error_from_http3(code.into_inner()) { + Some(code) => Error::Stopped(code), + None => Error::InvalidStopped(code), + }, + crate::quic::send::Error::Connection(e) => Error::SessionError(e), + // Only datagrams fail otherwise, and they report a session error. + e => Error::SessionError(e.into()), + } + } +} + +impl moq_net::transport::Error for Error { + fn close_kind(&self) -> moq_net::transport::CloseKind { + match self { + Self::SessionError(e) => e.close_kind(), + _ => moq_net::transport::CloseKind::Transport, + } + } + fn session_error(&self) -> Option<(u32, String)> { + if let Error::SessionError(e) = self { + return e.session_error(); + } + + None + } + + fn stream_error(&self) -> Option { + match self { + Error::Stopped(code) => Some(*code), + _ => None, + } + } +} + +/// The driver's send groups and scheduling options. +pub use crate::quic::send::{Group, Options}; diff --git a/rs/moq-uring/src/transport.rs b/rs/moq-uring/src/transport.rs index 7489caeef04..a43bcc33cc4 100644 --- a/rs/moq-uring/src/transport.rs +++ b/rs/moq-uring/src/transport.rs @@ -1,115 +1,3 @@ -//! Adapt backend poll sessions to moq-net without changing thread affinity. -mod adapter; +//! Bind a thread-local transport to the worker that uses it. mod local; -pub use adapter::{Error, RecvStream, SendStream, Session}; -#[cfg(feature = "quic")] -use adapter::{Grouped, GroupedSend}; pub use local::Local; - -#[cfg(feature = "quic")] -use crate::quic; - -#[cfg(feature = "quic")] -impl Grouped for quic::web::Session { - fn poll_open_request( - &mut self, - cx: &mut std::task::Context<'_>, - ) -> std::task::Poll>, Self::Error>> { - Self::poll_open_request(self, cx) - } - - // Leased in the QUIC stack, so the receive window bounds what an incomplete message holds. - type Recv = quic::web::RecvStream; - - fn recv(stream: RecvStream) -> Self::Recv { - stream.0 - } - - fn wire(&self) -> Option { - Some(quic::web::Session::wire(self)) - } - - fn probe(&self) -> Option { - quic::web::Session::probe(self) - } - - fn streams_evicted(&self) -> Option { - Some(quic::web::Session::streams_evicted(self)) - } - - fn receive_window(&self) -> Option { - Some(quic::web::Session::receive_window(self)) - } - - fn send_group(&mut self, priority: i64) -> Option { - Some(quic::web::Session::send_group(self, priority)) - } - - fn send_datagram( - &mut self, - payload: bytes::Bytes, - options: moq_net::transport::poll::SendOptions<'_, quic::send::Group>, - ) -> Result<(), Self::Error> { - let options = quic::send::Options { - group: options.group, - priority: options.priority, - }; - quic::web::Session::send_datagram(self, payload, options) - } -} - -#[cfg(feature = "quic")] -impl GroupedSend for quic::web::SendStream { - type Group = quic::send::Group; - - fn set_group(&mut self, group: &Self::Group) { - quic::web::SendStream::set_group(self, Some(group)); - } - - fn set_priority(&mut self, priority: i64) { - quic::web::SendStream::set_priority(self, priority); - } -} - -#[cfg(feature = "quic")] -impl moq_net::transport::poll::SendGroup for quic::send::Group { - fn set_priority(&mut self, priority: i64) { - quic::send::Group::set_priority(self, priority) - } -} - -#[cfg(feature = "quic")] -impl moq_net::transport::poll::RecvStream for quic::web::RecvStream { - type Error = adapter::Error; - type Lease<'a> = quic::recv::Lease<'a>; - - fn poll_lease( - &mut self, - waiter: &kio::Waiter, - want: usize, - ) -> std::task::Poll, Self::Error>> { - quic::web::RecvStream::poll_lease(self, &mut waiter.context(), want).map_err(adapter::Error) - } - fn set_priority(&mut self, priority: i64) { - quic::web::RecvStream::set_priority(self, priority) - } - fn stop(&mut self, code: u32) { - web_transport_trait::poll::RecvStream::stop(self, code) - } - fn poll_reset(&mut self, waiter: &kio::Waiter) -> std::task::Poll { - quic::web::RecvStream::poll_reset(self, &mut waiter.context()).map(adapter::Error) - } -} - -#[cfg(feature = "quic")] -impl moq_net::transport::poll::Lease for quic::recv::Lease<'_> { - fn is_fin(&self) -> bool { - quic::recv::Lease::is_fin(self) - } - fn chunks(&self) -> impl Iterator { - quic::recv::Lease::chunks(self) - } - fn release(self) { - quic::recv::Lease::release(self) - } -} diff --git a/rs/moq-uring/src/transport/adapter.rs b/rs/moq-uring/src/transport/adapter.rs deleted file mode 100644 index db65d110dc6..00000000000 --- a/rs/moq-uring/src/transport/adapter.rs +++ /dev/null @@ -1,529 +0,0 @@ -use bytes::{Buf, Bytes}; -use moq_net::transport::{self as moq, poll}; -use std::task::{Context, Poll}; -use web_transport_trait::{self as wt, poll as backend}; - -/// A backend failure preserving the session and stream code registries. -#[derive(Debug, thiserror::Error)] -#[error(transparent)] -pub struct Error(pub(super) E); - -impl moq::Error for Error { - fn close_kind(&self) -> moq::CloseKind { - close_kind(&self.0) - } - fn session_error(&self) -> Option<(u32, String)> { - self.0.session_error() - } - fn stream_error(&self) -> Option { - self.0.stream_error() - } -} - -struct Metrics { - inner: S, - streams_evicted: Option, -} -impl moq::Metrics for Metrics { - fn bytes_lost(&self) -> Option { - self.inner.bytes_lost() - } - fn packets_sent(&self) -> Option { - self.inner.packets_sent() - } - fn packets_received(&self) -> Option { - self.inner.packets_received() - } - fn packets_lost(&self) -> Option { - self.inner.packets_lost() - } - fn rtt(&self) -> Option { - self.inner.rtt() - } - fn estimated_send_rate(&self) -> Option { - self.inner.estimated_send_rate() - } - fn streams_evicted(&self) -> Option { - self.streams_evicted - } -} -/// A backend session whose send streams may join send groups, as native QUIC's do. -/// -/// `pub` only because `pub(crate)` is refused (E0446): the adapter's `type Group` projects -/// through [`GroupedSend`]. The module is private and the trait is not re-exported, so -/// other crates can neither name nor implement it. -/// -/// The backend interface has no groups, wire counters, or probe target, so moq-net's -/// [`send_group`](poll::Session::send_group), [`wire`](poll::Session::wire), and -/// [`probe`](poll::Session::probe) reach a backend through this. A backend without them -/// implements it empty. -pub trait Grouped: backend::Session { - /// The receive stream moq-net reads: the backend's own lease where it has one, or - /// [`Buffered`](poll::Buffered) reads otherwise. - type Recv: poll::RecvStream; - - /// Adapt one of the backend's receive streams, freshly accepted or opened. - fn recv(stream: RecvStream) -> Self::Recv; - - /// Open a request without waiting for stream credit. - fn poll_open_request( - &mut self, - cx: &mut Context<'_>, - ) -> Poll>, Self::Error>> { - backend::Session::poll_open_bi(self, cx).map_ok(Some) - } - - /// The wire counters the backend updates as it sends and receives, or `None` when it has none. - fn wire(&self) -> Option { - None - } - - /// The target the backend's sender probes up to, or `None` when it cannot probe. - fn probe(&self) -> Option { - None - } - - /// Receive streams evicted to free a stuck receive window, or `None` when the backend never - /// evicts. - fn streams_evicted(&self) -> Option { - None - } - - /// The receive windows now in force, or `None` when the backend cannot say; see - /// [`poll::Session::receive_window`](moq_net::transport::poll::Session::receive_window). - fn receive_window(&self) -> Option { - None - } - - /// Open a send group at `priority`, or `None` when the backend has none. - fn send_group(&mut self, priority: i64) -> Option<::Group> { - let _ = priority; - None - } - - /// Send a datagram scheduled by `options`; a backend without groups ignores them and - /// drops the datagram when it has no room. - fn send_datagram( - &mut self, - payload: Bytes, - options: poll::SendOptions<'_, ::Group>, - ) -> Result<(), Self::Error> { - let _ = options; - let mut cx = Context::from_waker(std::task::Waker::noop()); - match backend::Session::poll_send_datagram(self, &mut cx, &payload) { - Poll::Ready(res) => res, - Poll::Pending => Ok(()), - } - } -} - -/// A backend send stream that may join its [`Grouped`] session's send groups. -pub trait GroupedSend: backend::SendStream { - /// The backend's send group, [`Ungrouped`](poll::Ungrouped) when it has none. - type Group: poll::SendGroup; - - /// Move the stream into `group`. - fn set_group(&mut self, group: &Self::Group); - - /// Set the priority within the stream's send group, or session-wide when ungrouped. - /// - /// Saturated into the backend's narrower priority by default. - fn set_priority(&mut self, priority: i64) { - backend::SendStream::set_priority(self, priority.clamp(i32::MIN.into(), i32::MAX.into()) as i32); - } -} - -/// A WebTransport session adapted to moq-net. -/// -/// The backend's polls keep one waker per handle and operation, so each operation that may -/// wait polls its own clone of the session, bridged to every waiter parked on it. -pub struct Session { - accept_uni: kio::Bridge, - accept_bi: kio::Bridge, - open_uni: kio::Bridge, - open_bi: kio::Bridge, - open_request: kio::Bridge, - datagram: kio::Bridge, - closed: kio::Bridge, - /// The operations that never wait and need an exclusive borrow. - control: kio::Bridge, - /// The operations that only read. - session: S, - address: moq::Address, -} -impl Session { - /// Wrap a backend poll session. - pub fn new(session: S, address: moq::Address) -> Self { - let bridge = || kio::Bridge::new(session.clone()); - Self { - accept_uni: bridge(), - accept_bi: bridge(), - open_uni: bridge(), - open_bi: bridge(), - open_request: bridge(), - datagram: bridge(), - closed: bridge(), - control: bridge(), - session, - address, - } - } -} -impl + 'static> poll::Session for Session { - type SendStream = SendStream; - type RecvStream = S::Recv; - type Error = Error; - fn poll_accept_uni(&self, waiter: &kio::Waiter) -> Poll> { - self.accept_uni - .poll(waiter, backend::Session::poll_accept_uni) - .map_err(Error) - .map(|res| res.map(|recv| S::recv(RecvStream(recv)))) - } - fn poll_accept_bi(&self, waiter: &kio::Waiter) -> Poll, Self::Error>> { - self.accept_bi - .poll(waiter, backend::Session::poll_accept_bi) - .map_err(Error) - .map(|res| res.map(|(send, recv)| (SendStream::new(send), S::recv(RecvStream(recv))))) - } - fn poll_open_uni( - &self, - waiter: &kio::Waiter, - options: poll::SendOptions<'_, ::Group>, - ) -> Poll> { - self.open_uni - .poll(waiter, backend::Session::poll_open_uni) - .map_err(Error) - .map(|res| res.map(|send| SendStream::new(send).scheduled(options))) - } - fn poll_open_bi( - &self, - waiter: &kio::Waiter, - options: poll::SendOptions<'_, ::Group>, - ) -> Poll, Self::Error>> { - self.open_bi - .poll(waiter, backend::Session::poll_open_bi) - .map_err(Error) - .map(|res| res.map(|(send, recv)| (SendStream::new(send).scheduled(options), S::recv(RecvStream(recv))))) - } - - fn poll_open_request( - &self, - waiter: &kio::Waiter, - options: poll::SendOptions<'_, ::Group>, - ) -> Poll>, Self::Error>> { - self.open_request - .poll(waiter, Grouped::poll_open_request) - .map_err(Error) - .map_ok(|streams| { - streams.map(|(send, recv)| (SendStream::new(send).scheduled(options), S::recv(RecvStream(recv)))) - }) - } - fn send_group(&self, priority: i64) -> Option<::Group> { - Grouped::send_group(&mut *self.control.lock(), priority) - } - fn send_datagram( - &self, - payload: Bytes, - options: poll::SendOptions<'_, ::Group>, - ) -> Result<(), Self::Error> { - Grouped::send_datagram(&mut *self.control.lock(), payload, options).map_err(Error) - } - fn poll_recv_datagram(&self, waiter: &kio::Waiter) -> Poll> { - self.datagram - .poll(waiter, backend::Session::poll_recv_datagram) - .map_err(Error) - } - fn max_datagram_size(&self) -> usize { - backend::Session::max_datagram_size(&self.session) - } - fn address(&self) -> moq::Address { - self.address - } - fn protocol(&self) -> Option<&str> { - backend::Session::protocol(&self.session) - } - fn close(&self, code: u32, reason: &str) { - backend::Session::close(&mut *self.control.lock(), code, reason) - } - fn poll_closed(&self, waiter: &kio::Waiter) -> Poll { - self.closed.poll(waiter, backend::Session::poll_closed).map(Error) - } - fn metrics(&self) -> impl moq::Metrics { - Metrics { - inner: backend::Session::stats(&self.session), - streams_evicted: Grouped::streams_evicted(&self.session), - } - } - fn wire(&self) -> Option { - Grouped::wire(&self.session) - } - fn probe(&self) -> Option { - Grouped::probe(&self.session) - } - fn receive_window(&self) -> Option { - Grouped::receive_window(&self.session) - } -} -/// An outgoing WebTransport stream adapted to moq-net. -/// -/// Bridged, so its close can be watched through a shared borrow. -pub struct SendStream(kio::Bridge); -impl SendStream { - fn new(stream: S) -> Self { - Self(kio::Bridge::new(stream)) - } - - /// Schedule a newly opened stream by its options. - fn scheduled(mut self, options: poll::SendOptions<'_, S::Group>) -> Self { - let stream = self.0.get_mut(); - GroupedSend::set_priority(stream, options.priority); - if let Some(group) = options.group { - GroupedSend::set_group(stream, group); - } - self - } -} -impl poll::SendStream for SendStream { - type Error = Error; - type Group = S::Group; - fn poll_write(&mut self, waiter: &kio::Waiter, buf: &[u8]) -> Poll> { - backend::SendStream::poll_write(self.0.get_mut(), &mut waiter.context(), buf).map_err(Error) - } - fn poll_write_buf(&mut self, waiter: &kio::Waiter, buf: &mut B) -> Poll> { - backend::SendStream::poll_write_buf(self.0.get_mut(), &mut waiter.context(), buf).map_err(Error) - } - fn set_priority(&mut self, priority: i64) { - GroupedSend::set_priority(self.0.get_mut(), priority) - } - fn set_group(&mut self, group: &Self::Group) { - GroupedSend::set_group(self.0.get_mut(), group) - } - fn finish(&mut self) -> Result<(), Self::Error> { - backend::SendStream::finish(self.0.get_mut()).map_err(Error) - } - fn reset(&mut self, code: u32) { - backend::SendStream::reset(self.0.get_mut(), code) - } - fn poll_closed(&self, waiter: &kio::Waiter) -> Poll> { - self.0.poll(waiter, backend::SendStream::poll_closed).map_err(Error) - } -} -/// An incoming WebTransport stream adapted to moq-net. -pub struct RecvStream(pub(super) S); -impl poll::Chunks for RecvStream { - type Error = Error; - fn poll_read_chunk(&mut self, waiter: &kio::Waiter, max: usize) -> Poll, Self::Error>> { - backend::RecvStream::poll_read_chunk(&mut self.0, &mut waiter.context(), max).map_err(Error) - } - fn stop(&mut self, code: u32) { - backend::RecvStream::stop(&mut self.0, code) - } -} - -fn close_kind(error: &(dyn std::error::Error + 'static)) -> moq::CloseKind { - #[cfg(all(target_os = "linux", feature = "quic"))] - if let Some(error) = error.downcast_ref::() { - return match error { - crate::quic::Error::TimedOut => moq::CloseKind::IdleTimeout, - crate::quic::Error::Closed => moq::CloseKind::Closed, - crate::quic::Error::Evicted => moq::CloseKind::Evicted, - _ => moq::CloseKind::Transport, - }; - } - error.source().map(close_kind).unwrap_or_default() -} - -#[cfg(test)] -mod tests { - use super::*; - use moq_net::transport::Error as _; - use std::{cell::Cell, rc::Rc}; - - #[derive(Debug, thiserror::Error)] - #[error("peer failure")] - struct Failure; - impl wt::Error for Failure { - fn session_error(&self) -> Option<(u32, String)> { - None - } - fn stream_error(&self) -> Option { - Some(0x33) - } - } - - #[derive(Clone)] - struct Backend(Rc>); - struct Stream(Rc>); - impl backend::SendStream for Stream { - type Error = Failure; - fn poll_write(&mut self, _: &mut Context<'_>, buf: &[u8]) -> Poll> { - Poll::Ready(Ok(buf.len().min(2))) - } - fn set_priority(&mut self, order: i32) { - self.0.set(order as u32); - } - fn finish(&mut self) -> Result<(), Failure> { - Ok(()) - } - fn reset(&mut self, code: u32) { - self.0.set(code); - } - fn poll_closed(&mut self, _: &mut Context<'_>) -> Poll> { - Poll::Ready(Err(Failure)) - } - } - impl backend::RecvStream for Stream { - type Error = Failure; - fn poll_read(&mut self, _: &mut Context<'_>, dst: &mut [u8]) -> Poll, Failure>> { - let size = dst.len().min(2); - dst[..size].copy_from_slice(&b"hi"[..size]); - Poll::Ready(Ok(Some(size))) - } - fn stop(&mut self, code: u32) { - self.0.set(code); - } - fn poll_closed(&mut self, _: &mut Context<'_>) -> Poll> { - Poll::Ready(Err(Failure)) - } - } - impl GroupedSend for Stream { - type Group = poll::Ungrouped; - fn set_group(&mut self, group: &Self::Group) { - match *group {} - } - } - impl Grouped for Backend { - type Recv = poll::Buffered>; - fn recv(stream: RecvStream) -> Self::Recv { - poll::Buffered::new(stream) - } - } - struct Sample; - impl wt::Stats for Sample { - fn rtt(&self) -> Option { - Some(std::time::Duration::from_millis(7)) - } - } - impl backend::Session for Backend { - type SendStream = Stream; - type RecvStream = Stream; - type Error = Failure; - fn poll_accept_uni(&mut self, _: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(Stream(self.0.clone()))) - } - fn poll_accept_bi(&mut self, _: &mut Context<'_>) -> Poll, Failure>> { - Poll::Ready(Ok((Stream(self.0.clone()), Stream(self.0.clone())))) - } - fn poll_open_uni(&mut self, cx: &mut Context<'_>) -> Poll> { - backend::Session::poll_accept_uni(self, cx) - } - fn poll_open_bi(&mut self, cx: &mut Context<'_>) -> Poll, Failure>> { - backend::Session::poll_accept_bi(self, cx) - } - - fn poll_send_datagram(&mut self, _: &mut Context<'_>, payload: &[u8]) -> Poll> { - self.0.set(payload.len() as u32); - Poll::Ready(Ok(())) - } - fn poll_recv_datagram(&mut self, _: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(Bytes::from_static(b"data"))) - } - fn max_datagram_size(&self) -> usize { - 1200 - } - fn protocol(&self) -> Option<&str> { - Some("moq-lite-06") - } - fn close(&mut self, code: u32, _: &str) { - self.0.set(code); - } - fn poll_closed(&mut self, _: &mut Context<'_>) -> Poll { - Poll::Ready(Failure) - } - fn stats(&self) -> impl wt::Stats { - Sample - } - } - - #[derive(Debug, thiserror::Error)] - #[error("session failure")] - struct SessionFailure; - impl wt::Error for SessionFailure { - fn session_error(&self) -> Option<(u32, String)> { - Some((2, "unauthorized".into())) - } - } - #[test] - fn preserves_session_close_codes() { - let error = Error(SessionFailure); - assert_eq!(error.session_error(), Some((2, "unauthorized".into()))); - assert_eq!(error.stream_error(), None); - assert!(matches!( - moq_net::Error::from_transport(error), - moq_net::Error::Session(moq_net::SessionError::Unauthorized) - )); - } - - #[test] - fn forwards_thread_local_streams_and_keeps_error_codes() { - use moq_net::transport::Metrics as _; - let state = Rc::new(Cell::new(0)); - let session = Session::new(Backend(state.clone()), moq::Address::Setup); - let waiter = &kio::Waiter::noop(); - assert_eq!(poll::Session::protocol(&session), Some("moq-lite-06")); - assert_eq!( - poll::Session::metrics(&session).rtt(), - Some(std::time::Duration::from_millis(7)) - ); - let Poll::Ready(Ok((mut send, mut recv))) = poll::Session::poll_open_bi(&session, waiter, Default::default()) - else { - panic!("open failed") - }; - let mut buf = Bytes::from_static(b"abcd"); - assert!(matches!( - poll::SendStream::poll_write_buf(&mut send, waiter, &mut buf), - Poll::Ready(Ok(2)) - )); - assert_eq!(buf, b"cd"[..]); - poll::SendStream::set_priority(&mut send, 9); - assert_eq!(state.get(), 9); - poll::SendStream::reset(&mut send, 11); - assert_eq!(state.get(), 11); - let Poll::Ready(Ok(Some(chunk))) = poll::RecvStream::poll_read_chunk(&mut recv, waiter, 2) else { - panic!("read failed") - }; - assert_eq!(chunk, b"hi"[..]); - poll::RecvStream::stop(&mut recv, 13); - assert_eq!(state.get(), 13); - let Poll::Ready(Err(error)) = poll::SendStream::poll_closed(&send, waiter) else { - panic!("missing stream failure") - }; - assert_eq!(error.stream_error(), Some(0x33)); - assert_eq!(error.session_error(), None); - assert!(matches!( - moq_net::Error::from_transport(error), - moq_net::Error::Stream(moq_net::StreamError::NotFound) - )); - poll::Session::close(&session, 21, "done"); - assert_eq!(state.get(), 21); - } -} - -#[cfg(all(test, target_os = "linux", feature = "quic"))] -mod outcome_tests { - use super::*; - - #[test] - fn typed_ring_close_kinds_survive_the_adapter() { - let idle = Error(crate::quic::Error::TimedOut); - assert!(matches!( - moq_net::Error::from_transport(idle), - moq_net::Error::IdleTimeout - )); - let local = Error(crate::quic::Error::Closed); - assert!(matches!( - moq_net::Error::from_transport(local), - moq_net::Error::Session(moq_net::SessionError::Cancel) - )); - } -} diff --git a/rs/moq-uring/src/transport/local.rs b/rs/moq-uring/src/transport/local.rs index 90890356063..632e899ea8e 100644 --- a/rs/moq-uring/src/transport/local.rs +++ b/rs/moq-uring/src/transport/local.rs @@ -6,7 +6,7 @@ use std::task::Poll; /// A worker's transport, usable where moq-net wants a `Send` one. /// /// moq-net boxes every transport with a native `Send` bound, which a worker's -/// `!Send` [`Session`](super::Session) cannot meet, so wrap the session in this +/// `!Send` session cannot meet, so wrap the session in this /// before `Server::accept_request` or `Client::connect`. The session, its /// streams, and its send groups never leave the worker that polls them, so /// this checks the thread on every use and panics anywhere else instead of @@ -202,7 +202,7 @@ mod tests { #[test] fn meets_the_boxed_driver_bounds() { fn boxable() {} - boxable::>>(); + boxable::>(); } /// Touching it off its thread fails loud rather than racing the worker. diff --git a/rs/moq-uring/tests/integration/echo.rs b/rs/moq-uring/tests/integration/echo.rs index 8acca84fc49..c9c24e8adce 100644 --- a/rs/moq-uring/tests/integration/echo.rs +++ b/rs/moq-uring/tests/integration/echo.rs @@ -6,8 +6,8 @@ use crate::support; use std::net::UdpSocket; +use moq_net::transport::poll::SendStream as _; use moq_uring::{Config, Error, Worker, quic, udp}; -use web_transport_trait::poll::{RecvStream as _, SendStream as _, Session as _}; const PAYLOAD: usize = 512 * 1024; @@ -22,11 +22,11 @@ fn worker() -> Option { } } -async fn drain(recv: &mut quic::web::RecvStream) -> Vec { +async fn drain(recv: &mut quic::web::recv::Stream) -> Vec { let mut out = Vec::new(); let mut buf = [0u8; 64 * 1024]; loop { - match std::future::poll_fn(|cx| recv.poll_read(cx, &mut buf)) + match kio::wait(|waiter| recv.poll_read(waiter, &mut buf)) .await .expect("read") { @@ -36,14 +36,12 @@ async fn drain(recv: &mut quic::web::RecvStream) -> Vec { } } -async fn write_finish(send: &mut quic::web::SendStream, mut buf: &[u8]) { +async fn write_finish(mut send: quic::web::send::Stream, mut buf: &[u8]) { while !buf.is_empty() { - let n = std::future::poll_fn(|cx| send.poll_write(cx, buf)) - .await - .expect("write"); + let n = kio::wait(|waiter| send.poll_write(waiter, buf)).await.expect("write"); buf = &buf[n..]; } - send.finish().expect("finish"); + drop(send.finish()); } #[test] @@ -85,7 +83,7 @@ fn echo_tokio_peer() { .expect("connect"); let (mut send, mut recv) = session.open_bi(Default::default()).await.expect("open stream"); send.write_all(&payload[..]).await.expect("write"); - send.finish().expect("finish"); + drop(send.finish()); let echoed = recv.read_to_end(PAYLOAD + 1).await.expect("read"); done_tx.send(()).expect("notify server"); echoed @@ -96,12 +94,12 @@ fn echo_tokio_peer() { .block_on(async move { let conn = endpoint.accept().await.expect("accept"); let request = quic::web::Request::accept(conn).await.expect("handshake"); - let mut session = request.ok().await.expect("respond"); - let (mut send, mut recv) = std::future::poll_fn(|cx| session.poll_accept_bi(cx)) + let session = request.ok().await.expect("respond"); + let (send, mut recv) = kio::wait(|waiter| session.poll_accept_bi(waiter)) .await .expect("accept stream"); let payload = drain(&mut recv).await; - write_finish(&mut send, &payload).await; + write_finish(send, &payload).await; done_rx.await.expect("peer received echo"); }) .expect("worker"); diff --git a/rs/moq-uring/tests/integration/endpoint.rs b/rs/moq-uring/tests/integration/endpoint.rs index 7c31b113f21..09826120b06 100644 --- a/rs/moq-uring/tests/integration/endpoint.rs +++ b/rs/moq-uring/tests/integration/endpoint.rs @@ -13,8 +13,8 @@ use std::sync::{Arc, Mutex}; use std::task::Poll; use std::time::Duration; +use moq_net::transport::poll::SendStream as _; use moq_uring::{Config, Error, Worker, quic, udp}; -use web_transport_trait::poll::{SendStream as _, Session as _}; fn worker() -> Option { match Worker::new(Config::default()) { @@ -136,12 +136,12 @@ fn dropped_raw_session_closes(explicit: bool) { .expect("dial"); let peer = server.accept().await.expect("accept"); let session = quic::web::Session::raw(conn); - let mut remaining = session.clone(); + let remaining = session.clone(); drop(session); - let mut send = std::future::poll_fn(|cx| remaining.poll_open_uni(cx)) + let mut send = kio::wait(|waiter| remaining.poll_open_uni(waiter, Default::default())) .await .expect("surviving clone opens a stream"); - std::future::poll_fn(|cx| send.poll_write(cx, b"alive")) + kio::wait(|waiter| send.poll_write(waiter, b"alive")) .await .expect("write"); let mut recv = peer.accept_uni().await.expect("accept stream"); @@ -158,7 +158,7 @@ fn dropped_raw_session_closes(explicit: bool) { drop(remaining); if !explicit { // The stream outlives every session clone and keeps it open. - std::future::poll_fn(|cx| send.poll_write(cx, b"still")) + kio::wait(|waiter| send.poll_write(waiter, b"still")) .await .expect("the stream keeps the session open"); let read = recv.read(&mut &mut payload[..]).await.expect("read"); diff --git a/rs/moq-uring/tests/integration/main.rs b/rs/moq-uring/tests/integration/main.rs index 71b84715c90..81a21e98da5 100644 --- a/rs/moq-uring/tests/integration/main.rs +++ b/rs/moq-uring/tests/integration/main.rs @@ -10,10 +10,6 @@ #![cfg(target_os = "linux")] -// `transport` includes the adapter source, which names `crate::quic`. -#[cfg(feature = "quic")] -use moq_uring::quic; - mod credit; mod echo; mod ecn; @@ -27,6 +23,6 @@ mod session; mod socket_queues; mod support; mod teardown; -mod transport; mod web; +mod web_headers; mod workers; diff --git a/rs/moq-uring/tests/integration/receive_credit.rs b/rs/moq-uring/tests/integration/receive_credit.rs index 7c4d7f17428..d98f6ddeaa2 100644 --- a/rs/moq-uring/tests/integration/receive_credit.rs +++ b/rs/moq-uring/tests/integration/receive_credit.rs @@ -123,10 +123,7 @@ fn partial_frame_gives_way() { .with_publisher(&pub_origin) .accept( moq_net::time::Instant::now(), - moq_uring::transport::Local::new(moq_uring::transport::Session::new( - quic::web::Session::raw(conn), - moq_net::transport::Address::Setup, - )), + moq_uring::transport::Local::new(quic::web::Session::raw(conn)), ) .await .expect("accept"); @@ -144,10 +141,7 @@ fn partial_frame_gives_way() { .with_subscriber(sub.clone()) .connect( moq_net::time::Instant::now(), - moq_uring::transport::Local::new(moq_uring::transport::Session::new( - quic::web::Session::raw(conn), - moq_net::transport::Address::Setup, - )), + moq_uring::transport::Local::new(quic::web::Session::raw(conn)), ) .await .expect("connect"); @@ -294,18 +288,12 @@ fn javascript_partial_frame_gives_way() { .await .expect("handshake"); let session = request - .respond(quic::web::Response::default().with_protocol(ALPN)) + .respond(quic::web::proto::ConnectResponse::OK.with_protocol(ALPN)) .await .expect("respond"); let (session, driver) = moq_net::Server::new() .with_subscriber(sub.clone()) - .accept( - moq_net::time::Instant::now(), - moq_uring::transport::Local::new(moq_uring::transport::Session::new( - session, - moq_net::transport::Address::Uri, - )), - ) + .accept(moq_net::time::Instant::now(), moq_uring::transport::Local::new(session)) .await .expect("accept"); let task_handle = handle.clone(); diff --git a/rs/moq-uring/tests/integration/session.rs b/rs/moq-uring/tests/integration/session.rs index fd61aaf3f3d..b54d67642a4 100644 --- a/rs/moq-uring/tests/integration/session.rs +++ b/rs/moq-uring/tests/integration/session.rs @@ -95,10 +95,7 @@ fn lite_session_over_the_worker() { .with_publisher(&pub_origin) .accept( moq_net::time::Instant::now(), - moq_uring::transport::Local::new(moq_uring::transport::Session::new( - quic::web::Session::raw(conn), - moq_net::transport::Address::Setup, - )), + moq_uring::transport::Local::new(quic::web::Session::raw(conn)), ) .await .expect("accept"); @@ -119,10 +116,7 @@ fn lite_session_over_the_worker() { .with_subscriber(sub.clone()) .connect( moq_net::time::Instant::now(), - moq_uring::transport::Local::new(moq_uring::transport::Session::new( - quic::web::Session::raw(conn), - moq_net::transport::Address::Setup, - )), + moq_uring::transport::Local::new(quic::web::Session::raw(conn)), ) .await .expect("connect"); @@ -232,10 +226,7 @@ fn two_lite_sessions_share_the_server_socket() { .with_publisher(&pub_origin) .accept( moq_net::time::Instant::now(), - moq_uring::transport::Local::new(moq_uring::transport::Session::new( - quic::web::Session::raw(conn), - moq_net::transport::Address::Setup, - )), + moq_uring::transport::Local::new(quic::web::Session::raw(conn)), ) .await .expect("accept"); @@ -263,10 +254,7 @@ fn two_lite_sessions_share_the_server_socket() { .with_subscriber(sub.clone()) .connect( moq_net::time::Instant::now(), - moq_uring::transport::Local::new(moq_uring::transport::Session::new( - quic::web::Session::raw(conn), - moq_net::transport::Address::Setup, - )), + moq_uring::transport::Local::new(quic::web::Session::raw(conn)), ) .await .expect("connect"); diff --git a/rs/moq-uring/tests/integration/transport.rs b/rs/moq-uring/tests/integration/transport.rs deleted file mode 100644 index eb3353e6f25..00000000000 --- a/rs/moq-uring/tests/integration/transport.rs +++ /dev/null @@ -1,3 +0,0 @@ -// Exercise the transport adapter on native hosts without a browser or ring. -#[path = "../../src/transport/adapter.rs"] -mod adapter; diff --git a/rs/moq-uring/tests/integration/web.rs b/rs/moq-uring/tests/integration/web.rs index 57db4c7687f..3e0be722106 100644 --- a/rs/moq-uring/tests/integration/web.rs +++ b/rs/moq-uring/tests/integration/web.rs @@ -12,8 +12,8 @@ use crate::support; use std::net::UdpSocket; use moq_net::origin; +use moq_net::transport::poll::SendStream as _; use moq_uring::{Config, Error, Worker, quic, udp}; -use web_transport_trait::poll::{RecvStream as _, SendStream as _, Session as _}; fn worker() -> Option { match Worker::new(Config::default()) { @@ -77,7 +77,7 @@ fn tokio_client( type ClientFuture = std::pin::Pin + Send>>; /// Read a tokio-side stream to its end. -async fn read_all(recv: &mut moq_tokio::quic::web::RecvStream) -> Vec { +async fn read_all(recv: &mut moq_tokio::quic::web::recv::Stream) -> Vec { let mut out = Vec::new(); let mut buf = [0u8; 4096]; while let Some(n) = recv.read(&mut &mut buf[..]).await.expect("read") { @@ -87,11 +87,11 @@ async fn read_all(recv: &mut moq_tokio::quic::web::RecvStream) -> Vec { } /// Read a uring-side stream to its end. -async fn drain(recv: &mut quic::web::RecvStream) -> Vec { +async fn drain(recv: &mut quic::web::recv::Stream) -> Vec { let mut out = Vec::new(); let mut buf = [0u8; 4096]; loop { - let n = std::future::poll_fn(|cx| recv.poll_read(cx, &mut buf)) + let n = kio::wait(|waiter| recv.poll_read(waiter, &mut buf)) .await .expect("read"); match n { @@ -101,14 +101,12 @@ async fn drain(recv: &mut quic::web::RecvStream) -> Vec { } } -async fn write_finish(send: &mut quic::web::SendStream, mut buf: &[u8]) { +async fn write_finish(mut send: quic::web::send::Stream, mut buf: &[u8]) { while !buf.is_empty() { - let n = std::future::poll_fn(|cx| send.poll_write(cx, buf)) - .await - .expect("write"); + let n = kio::wait(|waiter| send.poll_write(waiter, buf)).await.expect("write"); buf = &buf[n..]; } - send.finish().expect("finish"); + drop(send.finish()); } /// Streams, datagrams, and the close code, hand-driven through the framing: @@ -128,19 +126,19 @@ fn webtransport_echo_end_to_end() { // Bidirectional echo. let (mut send, mut recv) = session.open_bi(Default::default()).await.expect("open_bi"); send.write_all(PAYLOAD).await.expect("write"); - send.finish().expect("finish"); + drop(send.finish()); assert_eq!(read_all(&mut recv).await, PAYLOAD, "bidi echo"); // Unidirectional, both directions. let mut send = session.open_uni(Default::default()).await.expect("open_uni"); send.write_all(PAYLOAD).await.expect("write"); - send.finish().expect("finish"); + drop(send.finish()); let mut recv = session.accept_uni().await.expect("accept_uni"); assert_eq!(read_all(&mut recv).await, PAYLOAD, "uni echo"); // Datagram echo. session - .send_datagram(PAYLOAD.to_vec().into(), Default::default()) + .send_datagram(PAYLOAD, Default::default()) .expect("send datagram"); let echoed = session.read_datagram().await.expect("read datagram"); assert_eq!(&echoed[..], PAYLOAD, "datagram echo"); @@ -157,48 +155,48 @@ fn webtransport_echo_end_to_end() { assert_eq!(conn.protocol(), Some("h3"), "negotiated ALPN"); let request = quic::web::Request::accept(conn).await.expect("handshake"); - assert_eq!(request.url().path(), "/echo"); - assert_eq!(request.url().query(), Some("token=abc")); + assert_eq!(request.url.path(), "/echo"); + assert_eq!(request.url.query(), Some("token=abc")); let debug = format!("{request:?}"); assert!( debug.contains("/echo") && !debug.contains("token=abc"), "query leaked: {debug}" ); - assert_eq!(request.protocols(), [PROTO.to_string()]); - let mut session = request - .respond(quic::web::Response::default().with_protocol(PROTO)) + assert_eq!(request.protocols, [PROTO.to_string()]); + let session = request + .respond(quic::web::proto::ConnectResponse::OK.with_protocol(PROTO)) .await .expect("respond"); assert_eq!(session.protocol(), Some(PROTO), "negotiated subprotocol"); // Bidirectional echo. - let (mut send, mut recv) = std::future::poll_fn(|cx| session.poll_accept_bi(cx)) + let (send, mut recv) = kio::wait(|waiter| session.poll_accept_bi(waiter)) .await .expect("accept_bi"); let payload = drain(&mut recv).await; - write_finish(&mut send, &payload).await; + write_finish(send, &payload).await; // Unidirectional, both directions. - let mut recv = std::future::poll_fn(|cx| session.poll_accept_uni(cx)) + let mut recv = kio::wait(|waiter| session.poll_accept_uni(waiter)) .await .expect("accept_uni"); let payload = drain(&mut recv).await; - let mut send = std::future::poll_fn(|cx| session.poll_open_uni(cx)) + let send = kio::wait(|waiter| session.poll_open_uni(waiter, Default::default())) .await .expect("open_uni"); - write_finish(&mut send, &payload).await; + write_finish(send, &payload).await; // Datagram echo. - let datagram = std::future::poll_fn(|cx| session.poll_recv_datagram(cx)) + let datagram = kio::wait(|waiter| session.poll_recv_datagram(waiter)) .await .expect("recv datagram"); - std::future::poll_fn(|cx| session.poll_send_datagram(cx, &datagram)) - .await + session + .send_datagram(datagram, Default::default()) .expect("send datagram"); // The peer's close arrives as a capsule; its code must come back // out of the H3 mapping as itself. - let err = std::future::poll_fn(|cx| session.poll_closed(cx)).await; + let err = kio::wait(|waiter| session.poll_closed(waiter)).await; match err { quic::Error::App { code, reason } => { assert_eq!(code, u64::from(CLOSE_CODE), "close code"); @@ -222,7 +220,7 @@ fn webtransport_accepts_a_buffered_uni_after_the_peer_close() { Box::pin(async move { let mut send = session.open_uni(Default::default()).await.expect("open uni"); send.write_all(PAYLOAD).await.expect("write"); - send.finish().expect("finish"); + let send = send.finish(); assert!(send.stopped().await.expect("FIN delivered").is_none()); session.close(0, "done"); session.closed().await; @@ -232,23 +230,22 @@ fn webtransport_accepts_a_buffered_uni_after_the_peer_close() { .block_on(async move { let conn = endpoint.accept().await.expect("accept"); let request = quic::web::Request::accept(conn).await.expect("handshake"); - let mut session = request - .respond(quic::web::Response::default().with_protocol(PROTO)) + let session = request + .respond(quic::web::proto::ConnectResponse::OK.with_protocol(PROTO)) .await .expect("respond"); - std::future::poll_fn(|cx| session.poll_closed(cx)).await; - let mut recv = std::future::poll_fn(|cx| session.poll_accept_uni(cx)) + kio::wait(|waiter| session.poll_closed(waiter)).await; + let mut recv = kio::wait(|waiter| session.poll_accept_uni(waiter)) .await .expect("buffered uni precedes the close"); assert_eq!(drain(&mut recv).await, PAYLOAD); - assert!(std::future::poll_fn(|cx| session.poll_accept_uni(cx)).await.is_err()); + assert!(kio::wait(|waiter| session.poll_accept_uni(waiter)).await.is_err()); }) .expect("worker"); client.join().expect("client thread"); } -/// An empty stream's FIN closes it with no read, and a second `finish` keeps -/// the first one's stream. +/// A FIN closes a receive stream without reading, and a finished sender observes its ACK. #[test] fn a_clean_finish_closes_a_stream() { let Some(mut worker) = worker() else { return }; @@ -257,8 +254,8 @@ fn a_clean_finish_closes_a_stream() { let endpoint = h3_endpoint(&handle, &certs); let client = tokio_client(format!("https://{}/fin", endpoint.local_addr()), |session| { Box::pin(async move { - let mut send = session.open_uni(Default::default()).await.expect("open uni"); - send.finish().expect("finish"); + let send = session.open_uni(Default::default()).await.expect("open uni"); + drop(send.finish()); let mut recv = session.accept_uni().await.expect("accept uni"); assert_eq!(read_all(&mut recv).await, PAYLOAD); session.close(0, "done"); @@ -269,24 +266,23 @@ fn a_clean_finish_closes_a_stream() { .block_on(async move { let conn = endpoint.accept().await.expect("accept"); let request = quic::web::Request::accept(conn).await.expect("handshake"); - let mut session = request - .respond(quic::web::Response::default().with_protocol(PROTO)) + let session = request + .respond(quic::web::proto::ConnectResponse::OK.with_protocol(PROTO)) .await .expect("respond"); - let mut recv = std::future::poll_fn(|cx| session.poll_accept_uni(cx)) + let mut recv = kio::wait(|waiter| session.poll_accept_uni(waiter)) .await .expect("accept uni"); - let closed = within(&handle, "the FIN", std::future::poll_fn(|cx| recv.poll_closed(cx))).await; + let closed = within(&handle, "the FIN", kio::wait(|waiter| recv.poll_closed(waiter))).await; assert!(closed.is_ok(), "{closed:?}"); - let mut send = std::future::poll_fn(|cx| session.poll_open_uni(cx)) + let mut send = kio::wait(|waiter| session.poll_open_uni(waiter, Default::default())) .await .expect("open uni"); - write_finish(&mut send, PAYLOAD).await; - send.finish().expect("a second finish"); - let closed = within(&handle, "the ACK", std::future::poll_fn(|cx| send.poll_closed(cx))).await; + send.write_all(PAYLOAD).await.expect("write"); + let closed = within(&handle, "the ACK", send.finish()).await; assert!(closed.is_ok(), "{closed:?}"); - std::future::poll_fn(|cx| session.poll_closed(cx)).await; + kio::wait(|waiter| session.poll_closed(waiter)).await; }) .expect("worker"); client.join().expect("client thread"); @@ -331,10 +327,7 @@ fn lite_session_over_webtransport() { let (moq, session_driver) = moq_net::Client::new() .with_subscriber(sub_origin.clone()) - .connect( - moq_net::time::Instant::now(), - moq_tokio::transport::Session::new(session, moq_net::transport::Address::Uri), - ) + .connect(moq_net::time::Instant::now(), session) .await .expect("connect"); tokio::spawn(moq_net::time::run(session_driver)); @@ -374,8 +367,8 @@ fn lite_session_over_webtransport() { let conn = endpoint.accept().await.expect("accept"); let request = quic::web::Request::accept(conn).await.expect("handshake"); // The WebTransport equivalent of ALPN: pick the moq version. - let protocol = request.protocols().iter().find(|p| *p == PROTO).cloned(); - let mut response = quic::web::Response::default(); + let protocol = request.protocols.iter().find(|p| *p == PROTO).cloned(); + let mut response = quic::web::proto::ConnectResponse::OK; if let Some(protocol) = &protocol { response = response.with_protocol(protocol); } @@ -383,13 +376,7 @@ fn lite_session_over_webtransport() { let (session, driver) = moq_net::Server::new() .with_publisher(&serve_origin) - .accept( - moq_net::time::Instant::now(), - moq_uring::transport::Local::new(moq_uring::transport::Session::new( - session, - moq_net::transport::Address::Uri, - )), - ) + .accept(moq_net::time::Instant::now(), moq_uring::transport::Local::new(session)) .await .expect("accept"); let task_handle = handle.clone(); @@ -474,7 +461,7 @@ fn an_abandoned_handshake_closes_the_connection() { let watch = conn.clone(); let request = quic::web::Request::accept(conn).await.expect("handshake"); let err = request - .respond(quic::web::Response::default().with_protocol("never-offered")) + .respond(quic::web::proto::ConnectResponse::OK.with_protocol("never-offered")) .await .expect_err("a subprotocol the peer did not offer"); assert!(matches!(err, quic::Error::Web(_)), "got {err:?}"); @@ -534,7 +521,7 @@ fn a_rejection_reaches_the_peer() { within( &handle, "the rejection to be delivered", - request.reject(quic::web::Rejected::NotFound), + request.reject(quic::web::http::StatusCode::NOT_FOUND), ) .await .expect("reject"); @@ -577,10 +564,13 @@ fn a_dropped_stream_carries_a_webtransport_code() { // Tell it we have the payload, so the reset below cannot race it. let mut ack = session.open_uni(Default::default()).await.expect("open_uni"); ack.write_all(&b"ack"[..]).await.expect("write"); - ack.finish().expect("finish"); + drop(ack.finish()); let err = recv.read(&mut &mut buf[..]).await.expect_err("the server dropped it"); - assert!(matches!(err, moq_tokio::quic::web::ReadError::Reset(0)), "got {err:?}"); + assert!( + matches!(err, moq_tokio::quic::web::recv::Error::Reset(0)), + "got {err:?}" + ); // And the other direction: the server drops the read half of this // one, which must arrive as a WebTransport cancellation too. @@ -592,7 +582,7 @@ fn a_dropped_stream_carries_a_webtransport_code() { } }; assert!( - matches!(err, moq_tokio::quic::web::WriteError::Stopped(0)), + matches!(err, moq_tokio::quic::web::send::Error::Stopped(0)), "got {err:?}" ); @@ -605,39 +595,39 @@ fn a_dropped_stream_carries_a_webtransport_code() { .block_on(async move { let conn = endpoint.accept().await.expect("accept"); let request = quic::web::Request::accept(conn).await.expect("handshake"); - let mut session = request - .respond(quic::web::Response::default().with_protocol(PROTO)) + let session = request + .respond(quic::web::proto::ConnectResponse::OK.with_protocol(PROTO)) .await .expect("respond"); - let mut send = std::future::poll_fn(|cx| session.poll_open_uni(cx)) + let mut send = kio::wait(|waiter| session.poll_open_uni(waiter, Default::default())) .await .expect("open_uni"); let mut payload = PAYLOAD; while !payload.is_empty() { - let n = std::future::poll_fn(|cx| send.poll_write(cx, payload)) + let n = kio::wait(|waiter| send.poll_write(waiter, payload)) .await .expect("write"); payload = &payload[n..]; } - let mut ack = std::future::poll_fn(|cx| session.poll_accept_uni(cx)) + let mut ack = kio::wait(|waiter| session.poll_accept_uni(waiter)) .await .expect("accept_uni"); assert_eq!(drain(&mut ack).await, b"ack"); drop(send); // The client's stream, whose read half we abandon. - let mut recv = std::future::poll_fn(|cx| session.poll_accept_uni(cx)) + let mut recv = kio::wait(|waiter| session.poll_accept_uni(waiter)) .await .expect("accept_uni"); let mut buf = [0u8; 4096]; - std::future::poll_fn(|cx| recv.poll_read(cx, &mut buf)) + kio::wait(|waiter| recv.poll_read(waiter, &mut buf)) .await .expect("read"); drop(recv); - std::future::poll_fn(|cx| session.poll_closed(cx)).await; + kio::wait(|waiter| session.poll_closed(waiter)).await; }) .expect("worker"); @@ -675,7 +665,10 @@ fn a_reset_stream_keeps_its_header() { Box::pin(async move { let mut recv = session.accept_uni().await.expect("accept_uni"); let err = recv.read_chunk(usize::MAX).await.expect_err("the server reset it"); - assert!(matches!(err, moq_tokio::quic::web::ReadError::Reset(7)), "got {err:?}"); + assert!( + matches!(err, moq_tokio::quic::web::recv::Error::Reset(7)), + "got {err:?}" + ); session.open_uni(Default::default()).await.expect("open_uni").reset(8); session.closed().await; @@ -686,13 +679,13 @@ fn a_reset_stream_keeps_its_header() { .block_on(async move { let conn = endpoint.accept().await.expect("accept"); let request = quic::web::Request::accept(conn).await.expect("handshake"); - let mut session = request - .respond(quic::web::Response::default().with_protocol(PROTO)) + let session = request + .respond(quic::web::proto::ConnectResponse::OK.with_protocol(PROTO)) .await .expect("respond"); // Reset before any payload. - let mut send = std::future::poll_fn(|cx| session.poll_open_uni(cx)) + let send = kio::wait(|waiter| session.poll_open_uni(waiter, Default::default())) .await .expect("open_uni"); send.reset(7); @@ -700,18 +693,18 @@ fn a_reset_stream_keeps_its_header() { let mut recv = within( &handle, "the client's reset stream", - std::future::poll_fn(|cx| session.poll_accept_uni(cx)), + kio::wait(|waiter| session.poll_accept_uni(waiter)), ) .await .expect("accept_uni"); let mut buf = [0u8; 16]; - let err = std::future::poll_fn(|cx| recv.poll_read(cx, &mut buf)) + let err = kio::wait(|waiter| recv.poll_read(waiter, &mut buf)) .await .expect_err("the client reset it"); - assert!(matches!(err, quic::Error::Reset(8)), "got {err:?}"); + assert!(matches!(err, quic::web::recv::Error::Reset(8)), "got {err:?}"); session.close(CLOSE_CODE, CLOSE_REASON); - std::future::poll_fn(|cx| session.poll_closed(cx)).await; + kio::wait(|waiter| session.poll_closed(waiter)).await; }) .expect("worker"); @@ -752,7 +745,10 @@ fn a_reset_stream_keeps_its_header_without_credit() { let accept = kio::clock::timeout(std::time::Duration::from_secs(5), session.accept_uni()); let mut recv = accept.await.expect("the reset stream arrives").expect("accept_uni"); let err = recv.read_chunk(usize::MAX).await.expect_err("the server reset it"); - assert!(matches!(err, moq_tokio::quic::web::ReadError::Reset(7)), "got {err:?}"); + assert!( + matches!(err, moq_tokio::quic::web::recv::Error::Reset(7)), + "got {err:?}" + ); // Only now, so the close cannot overtake the reset stream. session.close(CLOSE_CODE, CLOSE_REASON); session.closed().await; @@ -763,27 +759,27 @@ fn a_reset_stream_keeps_its_header_without_credit() { .block_on(async move { let conn = endpoint.accept().await.expect("accept"); let request = quic::web::Request::accept(conn).await.expect("handshake"); - let mut session = request - .respond(quic::web::Response::default().with_protocol(PROTO)) + let session = request + .respond(quic::web::proto::ConnectResponse::OK.with_protocol(PROTO)) .await .expect("respond"); // Spend all of the connection credit on a filler stream. - let mut filler = std::future::poll_fn(|cx| session.poll_open_uni(cx)) + let mut filler = kio::wait(|waiter| session.poll_open_uni(waiter, Default::default())) .await .expect("open_uni"); let chunk = [0u8; 1024]; - while let Poll::Ready(n) = std::future::poll_fn(|cx| Poll::Ready(filler.poll_write(cx, &chunk))).await { + while let Poll::Ready(n) = filler.poll_write(&kio::Waiter::noop(), &chunk) { n.expect("write"); } - filler.finish().expect("finish"); + drop(filler.finish()); - let mut send = std::future::poll_fn(|cx| session.poll_open_uni(cx)) + let send = kio::wait(|waiter| session.poll_open_uni(waiter, Default::default())) .await .expect("open_uni"); send.reset(7); - std::future::poll_fn(|cx| session.poll_closed(cx)).await; + kio::wait(|waiter| session.poll_closed(waiter)).await; }) .expect("worker"); @@ -851,7 +847,7 @@ fn dropped_session_delivers_close(explicit: bool) { .await .expect("handshake"); let session = request.ok().await.expect("respond"); - let mut remaining = session.clone(); + let remaining = session.clone(); drop(session); let mut response = Vec::new(); @@ -1012,3 +1008,17 @@ fn cancelling_before_peer_settings_closes_the_connection() { fn cancelling_while_awaiting_connect_closes_the_connection() { cancelling_a_pending_web_handshake(true); } + +/// Stalled headers hold stream credit while ready streams and fragmented tails still arrive. +#[test] +fn partial_headers_do_not_block_ready_streams() { + let Some(worker) = worker() else { + return; + }; + drop(worker); + for pending in [0, 64, 256] { + for ready in [1, 16] { + super::web_headers::run(pending, ready, 1); + } + } +} diff --git a/rs/moq-uring/tests/integration/web_headers.rs b/rs/moq-uring/tests/integration/web_headers.rs new file mode 100644 index 00000000000..82b3bddb9e5 --- /dev/null +++ b/rs/moq-uring/tests/integration/web_headers.rs @@ -0,0 +1,125 @@ +//! Partial stream headers must not make unrelated ready streams cost a scan of every stream. + +#![cfg(feature = "quic")] + +use std::net::UdpSocket; +use std::time::Duration; + +use moq_uring::{Config, Worker, quic, udp}; + +use crate::support; + +/// Serve ready uni and bi streams while other streams hold partial headers, then finish them. +#[expect( + clippy::disallowed_types, + clippy::disallowed_methods, + reason = "shared benchmark measures host time" +)] +pub fn run(pending: usize, ready: usize, iterations: u64) -> Duration { + let mut worker = Worker::new(Config::default()).expect("worker"); + let handle = worker.handle(); + let certs = support::certs().expect("certificates"); + let mut server = quic::server::Config::new( + quic::Identity::open(&certs.cert, &certs.key) + .expect("identity") + .server(&["h3"], quic::server::ClientAuth::None) + .expect("TLS"), + ); + server.transport.limits.max_streams = std::num::NonZeroU64::new((pending + ready + 3) as u64); + let socket = handle + .udp(UdpSocket::bind("127.0.0.1:0").expect("bind"), udp::Config::default()) + .expect("socket"); + let endpoint = + quic::Endpoint::new(socket, quic::endpoint::Config::default().with_server(server)).expect("endpoint"); + let address = endpoint.local_addr(); + let (start_tx, mut start_rx) = tokio::sync::mpsc::channel::>(1); + let client = std::thread::spawn(move || { + let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("runtime"); + runtime.block_on(async { + let request = moq_tokio::quic::web::proto::ConnectRequest::new( + url::Url::parse(&format!("https://{address}/headers")).unwrap(), + ); + let session = support::web_connect(request, Default::default()) + .await + .expect("connect"); + let mut uni = Vec::new(); + let mut bi = Vec::new(); + for _ in 0..pending { + let mut send = session.connection().open_uni(Default::default()).await.unwrap(); + send.write_all(&[0x40][..]).await.unwrap(); + uni.push(send); + let (mut send, recv) = session.connection().open_bi(Default::default()).await.unwrap(); + send.write_all(&[0x40][..]).await.unwrap(); + bi.push((send, recv)); + } + while let Some(done) = start_rx.recv().await { + for sequence in 0..ready { + let mut send = session.open_uni(Default::default()).await.unwrap(); + send.write_all(sequence.to_be_bytes().as_slice()).await.unwrap(); + drop(send.finish()); + let (mut send, recv) = session.open_bi(Default::default()).await.unwrap(); + send.write_all(sequence.to_be_bytes().as_slice()).await.unwrap(); + drop(send.finish()); + drop(recv); + } + done.send(()).unwrap(); + } + // The receiver has delivered every ready stream while these were mid-header. + // Complete a two-byte type and an eight-byte session id, preserving the payload. + for mut send in uni { + send.write_all(&[0x54, 0xc0, 0, 0, 0, 0, 0, 0, 0, 0xa5][..]) + .await + .unwrap(); + drop(send.finish()); + } + for (mut send, recv) in bi { + send.write_all(&[0x41, 0xc0, 0, 0, 0, 0, 0, 0, 0, 0xa5][..]) + .await + .unwrap(); + drop(send.finish()); + drop(recv); + } + // Wait for acknowledgement so the client stays alive until all payloads arrived. + let mut recv = session.accept_uni().await.unwrap(); + assert_eq!(recv.read_to_end(1).await.unwrap(), [0xff]); + }); + }); + let elapsed = worker + .block_on(async { + let conn = endpoint.accept().await.unwrap(); + let session = quic::web::Request::accept(conn).await.unwrap().ok().await.unwrap(); + let start = std::time::Instant::now(); + for _ in 0..iterations { + let (done, sent) = tokio::sync::oneshot::channel(); + start_tx.send(done).await.unwrap(); + for sequence in 0..ready { + let mut recv = session.accept_uni().await.unwrap(); + assert_eq!(recv.read_to_end(8).await.unwrap(), sequence.to_be_bytes()); + let (send, mut recv) = session.accept_bi().await.unwrap(); + assert_eq!(recv.read_to_end(8).await.unwrap(), sequence.to_be_bytes()); + drop(send.finish()); + } + sent.await.unwrap(); + } + let elapsed = start.elapsed(); + drop(start_tx); + for _ in 0..pending { + let mut recv = session.accept_uni().await.unwrap(); + assert_eq!(recv.read_to_end(1).await.unwrap(), [0xa5]); + let (send, mut recv) = session.accept_bi().await.unwrap(); + assert_eq!(recv.read_to_end(1).await.unwrap(), [0xa5]); + drop(send.finish()); + } + let mut send = session.open_uni(Default::default()).await.unwrap(); + send.write_all(&[0xff][..]).await.unwrap(); + assert_eq!(send.finish().await.unwrap(), None); + elapsed + }) + .expect("worker"); + client.join().expect("client thread"); + elapsed +} diff --git a/sh/gh/nightly.sh b/sh/gh/nightly.sh index 60c4ef42c57..758b4abf1a5 100755 --- a/sh/gh/nightly.sh +++ b/sh/gh/nightly.sh @@ -154,7 +154,7 @@ case "$1" in rust-uring) # One run per target makes an unsupported/skipped ring fail report # validation instead of hiding behind the Tokio target's measurements. - for target in udp_tokio udp_uring echo_quic session_lite unreachable_dials timers; do + for target in udp_tokio udp_uring echo_quic session_lite unreachable_dials timers web_headers; do criterion "uring-$target" -p moq-uring --bench "$target" done ;; From 9d39cc4b4ec8e7d51d87eaa78414f6d918cbe3d9 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 3 Nov 2026 15:25:51 -0800 Subject: [PATCH 03/17] fix(quic): preserve wakes and buffered stream tails Co-Authored-By: GPT-6.1 Sol --- rs/moq-tokio/src/quic/web/session.rs | 56 +++++++++++++++++++++------- rs/moq-uring/src/quic/web.rs | 14 +++++-- 2 files changed, 53 insertions(+), 17 deletions(-) diff --git a/rs/moq-tokio/src/quic/web/session.rs b/rs/moq-tokio/src/quic/web/session.rs index c97a1e5f830..7966bc5732c 100644 --- a/rs/moq-tokio/src/quic/web/session.rs +++ b/rs/moq-tokio/src/quic/web/session.rs @@ -266,7 +266,7 @@ impl Session { self.opening.poll(waiter, |opening, cx| { if opening.uni.is_none() { let send = std::task::ready!(self.conn.poll_open_uni( - waiter, + opening.park.hold(cx), send::Options { group: None, priority: i64::MAX @@ -321,7 +321,7 @@ impl Session { pair } else { std::task::ready!(self.conn.poll_open_bi( - waiter, + opening.park.hold(cx), send::Options { group: None, priority: i64::MAX @@ -614,6 +614,8 @@ impl Session { #[derive(Default)] struct Opening { + // Driver wakes must go through the bridge, so they cannot reenter its held lock. + park: kio::Park, uni: Option<(send::Stream, Bytes)>, bi: Option<(send::Stream, crate::quic::driver::recv::Stream, Bytes)>, request: Option<(send::Stream, crate::quic::driver::recv::Stream, Bytes)>, @@ -727,6 +729,7 @@ struct SessionAccept { bi_park: kio::Park, // Pending WebTransport headers must be decoded before returning this error. uni_end: Option, + bi_end: Option, // Keep track of work being done to read/write the WebTransport stream header. pending_uni: FuturesUnordered>>, @@ -772,6 +775,7 @@ impl SessionAccept { uni_park: Default::default(), bi_park: Default::default(), uni_end: None, + bi_end: None, pending_uni: early.into_iter().collect(), pending_bi: FuturesUnordered::new(), @@ -869,18 +873,18 @@ impl SessionAccept { loop { // Accept any new streams. - if let Poll::Ready(res) = self.conn.poll_accept_bi(self.bi_park.hold(cx)) { + if self.bi_end.is_none() + && let Poll::Ready(res) = self.conn.poll_accept_bi(self.bi_park.hold(cx)) + { // Start decoding the header and add the future to the list of pending streams. - let (send, recv) = match res { - Ok(pair) => pair, - Err(e) => { - return Poll::Ready(Err(e.into())); + match res { + Ok((send, recv)) => { + self.pending_bi + .push(Box::pin(Self::decode_bi(send, recv, self.session_id))); + continue; } - }; - let pending = Self::decode_bi(send, recv, self.session_id); - self.pending_bi.push(Box::pin(pending)); - - continue; + Err(err) => self.bi_end = Some(err.into()), + } } // Poll the list of pending streams. @@ -890,7 +894,8 @@ impl SessionAccept { log_header_error(err, "bidirectional"); continue; } - Poll::Ready(None) | Poll::Pending => return Poll::Pending, + Poll::Ready(None) => return self.bi_end.clone().map_or(Poll::Pending, |err| Poll::Ready(Err(err))), + Poll::Pending => return Poll::Pending, }; if let Some((send, recv)) = res { @@ -1286,6 +1291,31 @@ mod tests { } } + /// Accepted headers buffered before the connection closed precede its terminal error. + #[tokio::test] + async fn buffered_stream_headers_precede_the_close() { + let (client, server, _endpoints) = pair().await; + let (client, server) = tokio::join!( + Session::connect(client, url::Url::parse("https://localhost/").unwrap()), + async { crate::quic::web::Request::accept(server).await.unwrap().ok().await }, + ); + let (client, server) = (client.unwrap(), server.unwrap()); + let mut uni = client.open_uni(Default::default()).await.unwrap(); + uni.write_all(&b"uni"[..]).await.unwrap(); + assert!(uni.finish().await.unwrap().is_none()); + let (mut bi, _recv) = client.open_bi(Default::default()).await.unwrap(); + bi.write_all(&b"bi"[..]).await.unwrap(); + assert!(bi.finish().await.unwrap().is_none()); + client.close(0, "done"); + server.closed().await; + let mut uni = server.accept_uni().await.unwrap(); + assert_eq!(uni.read_to_end(3).await.unwrap(), b"uni"); + let (_send, mut bi) = server.accept_bi().await.unwrap(); + assert_eq!(bi.read_to_end(2).await.unwrap(), b"bi"); + assert!(server.accept_uni().await.is_err()); + assert!(server.accept_bi().await.is_err()); + } + #[tokio::test] async fn request_credit_refusal_does_not_leave_an_open() { let mut transport = crate::quic::driver::transport::Config::default(); diff --git a/rs/moq-uring/src/quic/web.rs b/rs/moq-uring/src/quic/web.rs index 919a1c74e9c..63d5c27bf8b 100644 --- a/rs/moq-uring/src/quic/web.rs +++ b/rs/moq-uring/src/quic/web.rs @@ -331,6 +331,8 @@ struct Web { } struct State { + uni_park: kio::Park, + bi_park: kio::Park, /// Incoming streams whose headers are still being read. pending_uni: FuturesUnordered, pending_bi: FuturesUnordered, @@ -457,6 +459,8 @@ impl Session { header_bi: header_bi.into(), header_datagram: header_datagram.into(), state: kio::Bridge::new(State { + uni_park: Default::default(), + bi_park: Default::default(), // Still mid-header when the control stream turned up; the // session finishes classifying them, so a pipelined // WebTransport stream is not lost to the handshake ending. @@ -523,6 +527,8 @@ impl std::fmt::Debug for Session { #[derive(Default)] struct Opening { + // Driver wakes must go through the bridge, so they cannot reenter its held lock. + park: kio::Park, uni: Option<(SendStream, Bytes)>, bi: Option<(SendStream, RecvStream, Bytes)>, request: Option<(SendStream, RecvStream, Bytes)>, @@ -543,7 +549,7 @@ impl Session { return Poll::Ready(Ok(RecvStream::new(recv, &self.handles))); } let ended = loop { - match self.conn.poll_accept_uni(waiter) { + match self.conn.poll_accept_uni(state.uni_park.hold(cx)) { Poll::Ready(Ok(recv)) => state.pending_uni.push(PendingUni::new(recv)), Poll::Ready(Err(err)) => break Some(err), Poll::Pending => break None, @@ -580,7 +586,7 @@ impl Session { ))); } let ended = loop { - match self.conn.poll_accept_bi(waiter) { + match self.conn.poll_accept_bi(state.bi_park.hold(cx)) { Poll::Ready(Ok((send, recv))) => state.pending_bi.push(PendingBi::new(send, recv)), Poll::Ready(Err(err)) => break Some(err), Poll::Pending => break None, @@ -607,7 +613,7 @@ impl Session { self.opening.poll(waiter, |opening, cx| { if opening.uni.is_none() { let inner = ready!(self.conn.poll_open_uni( - waiter, + opening.park.hold(cx), driver_send::Options { group: None, priority: i64::MAX @@ -662,7 +668,7 @@ impl Session { pair } else { ready!(self.conn.poll_open_bi( - waiter, + opening.park.hold(cx), driver_send::Options { group: None, priority: i64::MAX From cfa0123f6f48a283a84a3ef065e93c954d1928d0 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 3 Nov 2026 15:29:01 -0800 Subject: [PATCH 04/17] quest: retain native WebTransport validation work Co-Authored-By: GPT-6.1 Sol --- quest/m1/quic-api/native-web.md | 76 +++++++++++---------------------- 1 file changed, 26 insertions(+), 50 deletions(-) diff --git a/quest/m1/quic-api/native-web.md b/quest/m1/quic-api/native-web.md index 7b05e666295..0a8804b7d89 100644 --- a/quest/m1/quic-api/native-web.md +++ b/quest/m1/quic-api/native-web.md @@ -14,56 +14,32 @@ tests. ## Plan -Moved from drop web-transport-trait (maintainer, 2026-10-23): the adapter -exists because quinn's driver was async-only, so it goes with the rest of -quinn's shape before the cut. - -What it deletes, from the 2026-10-23 consumer audit (about 400 lines): -moq-tokio's `transport.rs` QUIC paths (a boxed future per operation, a -`Bytes::copy_from_slice` per write and datagram, `finish` and `reset` -deferred until an in-flight operation settles, an emulated `closed()`), -`grouped.rs`, the QUIC glue in `transport.rs` and `owned.rs` (the `Lease` -impl and `close_kind` downcasting through `web::ReadError`), the -`web_transport_trait` impls in `web/session.rs`, `web/send.rs`, and -`web/recv.rs`, the `i32` priority clamp, and the `futures::stream::unfold` -around `accept_uni`. `session.rs`'s `finish()` then `stopped().await` becomes -awaiting `send::Finished`. - -Align the two stacks while each gets its impl: - -- moq-tokio's `quic::web::Session` derefs to `quic::driver::Connection`, so - it has two `send_datagram`s with different framing: its own prefixes the - WebTransport session id, the driver's does not. Drop the `Deref`. -- The session id is chained onto each datagram with `send(impl Buf)` - ([moq-dev/moq#6950](https://github.com/moq-dev/moq/pull/6950)) instead of allocated and copied - per datagram (quinn-rs/quinn#1724). -- `quic::web` is supported API in both crates (maintainer, 2026-10-23): a - WebTransport-over-moq-quic library usable without MoQ, documented, no - longer `#[doc(hidden)]`, with the same names and signatures in moq-tokio - and moq-uring and the driver's `send::`/`recv::` shapes and errors. -- moq-uring's header classification (`classify_uni`, `classify_bi` in - `web.rs`) re-polls every pending stream and rebuilds the pending `Vec` - whenever any stream arrives or wakes, so a peer that opens N streams with - partial headers costs O(N²) CPU, with N up to its stream limit. moq-tokio - keeps them in a `FuturesUnordered`, which polls only the woken ones. Bring - uring to the same shape. Test with pending × ready headers and fragmented - arrival, preserving FIN handling, the held control streams, and stream - credit (2026-10-27 audit, maintainer). -- A cleanly finished stream ends `closed()`: today moq-tokio's adapter waits - only for a reset, so a peer's clean FIN never returns it - ([moq-dev/moq#6950](https://github.com/moq-dev/moq/pull/6950) fixed the same - bug in moq-uring). Test a FIN on both stacks (maintainer, 2026-10-30). -- `transport::poll_op` and `OpSlot` go with moq-tokio's adapter, their last - user. -- moq-relay's io_uring server and its tests call the uring session through - `web_transport_trait` (`uring.rs`, `tests/integration/runtime_uring.rs`); - move them to the new impls. -- Report the public API impact per crate: deleted `Grouped`/`GroupedSend`, - `web_transport_trait` impls, and generic `transport::Session` adapters, - and new `poll::Session` impls. - -Measure relay CPU per Gbps and allocations per stream before and after on -the same workload. +The native trait migration is implemented on this quest's branch. Finish its +validation before removing this quest: + +- Run local checks and the cross-runtime stream, FIN, send-group and receive-credit + tests on the final committed head. The empty-FIN close watch fails on the + driver-poll base and must pass on both stacks. +- Run the io_uring `web_headers` pending × ready sweep. It covers uni and bi + headers, later fragmented completion, preserved payloads and FIN, and stream + credit returning after consumption. The classifier polls only woken entries; + three HTTP/3 critical-stream slots replace the old retained-stream cap. +- Measure relay CPU per Gbps and allocations per stream before and after, with + the same workload and disclosed host load. `fanout` also measures the native + poll interface directly with allocation counters (`NATIVE_ALLOCS=1`). +- Review the checked head locally and address findings before readiness. + +The supported common API uses `web::send::Stream`, `web::recv::Stream`, their +role-local errors, consuming terminal methods, and `send::Finished` futures. +Sessions implement moq-net's poll traits directly with real send groups; +io_uring retains only `transport::Local` for moq-net's thread bound. +Both handshakes use `proto::ConnectResponse` and HTTP status codes. Connection +access is explicit, rather than through the Tokio session's `Deref`. + +The shared strict-flake runner failure writing `/proc/self/uid_map` is an +existing environment limitation. Preserve the probe/skip policy for ordinary +checks and the intentional failure under strict CI, and record base/head +failures without changing the harness to pass. ## Related From 4a8d59641c97e567c1b791a9512843b0aa20fbfd Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 3 Nov 2026 15:36:08 -0800 Subject: [PATCH 05/17] test(quic): keep peers alive through FIN acknowledgement Co-Authored-By: GPT-6.1 Sol --- rs/moq-tokio/src/quic/web/session.rs | 9 ++++----- rs/moq-uring/tests/integration/web_headers.rs | 3 +++ 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/rs/moq-tokio/src/quic/web/session.rs b/rs/moq-tokio/src/quic/web/session.rs index 7966bc5732c..c8535da177f 100644 --- a/rs/moq-tokio/src/quic/web/session.rs +++ b/rs/moq-tokio/src/quic/web/session.rs @@ -1277,15 +1277,14 @@ mod tests { }; let mut send = client.open_uni(Default::default()).await.unwrap(); send.write_all(payload.clone()).await.unwrap(); - let _finished = send.finish(); + let finished = send.finish(); let mut recv = server.accept_uni().await.unwrap(); if !payload.is_empty() { assert_eq!(recv.read_to_end(payload.len()).await.unwrap(), payload); } - kio::clock::timeout(Duration::from_secs(2), recv.closed()) - .await - .unwrap_or_else(|err| panic!("FIN watch: framed={framed}, bytes={}, {err}", payload.len())) - .unwrap(); + // The FIN is acknowledged, so checking close needs no timing assumption. + assert!(finished.await.unwrap().is_none()); + assert!(matches!(recv.poll_closed(&Waiter::noop()), Poll::Ready(Ok(())))); assert!(recv.read_chunk(1).await.unwrap().is_none()); } } diff --git a/rs/moq-uring/tests/integration/web_headers.rs b/rs/moq-uring/tests/integration/web_headers.rs index 82b3bddb9e5..a0d48e6f07c 100644 --- a/rs/moq-uring/tests/integration/web_headers.rs +++ b/rs/moq-uring/tests/integration/web_headers.rs @@ -33,6 +33,7 @@ pub fn run(pending: usize, ready: usize, iterations: u64) -> Duration { quic::Endpoint::new(socket, quic::endpoint::Config::default().with_server(server)).expect("endpoint"); let address = endpoint.local_addr(); let (start_tx, mut start_rx) = tokio::sync::mpsc::channel::>(1); + let (acked, acknowledgement) = tokio::sync::oneshot::channel(); let client = std::thread::spawn(move || { let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); let runtime = tokio::runtime::Builder::new_current_thread() @@ -86,6 +87,7 @@ pub fn run(pending: usize, ready: usize, iterations: u64) -> Duration { // Wait for acknowledgement so the client stays alive until all payloads arrived. let mut recv = session.accept_uni().await.unwrap(); assert_eq!(recv.read_to_end(1).await.unwrap(), [0xff]); + acknowledgement.await.expect("server received the ACK"); }); }); let elapsed = worker @@ -117,6 +119,7 @@ pub fn run(pending: usize, ready: usize, iterations: u64) -> Duration { let mut send = session.open_uni(Default::default()).await.unwrap(); send.write_all(&[0xff][..]).await.unwrap(); assert_eq!(send.finish().await.unwrap(), None); + acked.send(()).expect("release client"); elapsed }) .expect("worker"); From 808e2696b9b6927a4b4453dc6755fa44a11399a6 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 3 Nov 2026 15:39:33 -0800 Subject: [PATCH 06/17] style(quic): format header completion fixture Co-Authored-By: GPT-6.1 Sol --- rs/moq-uring/tests/integration/web_headers.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rs/moq-uring/tests/integration/web_headers.rs b/rs/moq-uring/tests/integration/web_headers.rs index a0d48e6f07c..245d9f03095 100644 --- a/rs/moq-uring/tests/integration/web_headers.rs +++ b/rs/moq-uring/tests/integration/web_headers.rs @@ -87,7 +87,7 @@ pub fn run(pending: usize, ready: usize, iterations: u64) -> Duration { // Wait for acknowledgement so the client stays alive until all payloads arrived. let mut recv = session.accept_uni().await.unwrap(); assert_eq!(recv.read_to_end(1).await.unwrap(), [0xff]); - acknowledgement.await.expect("server received the ACK"); + acknowledgement.await.expect("server received the ACK"); }); }); let elapsed = worker From b49f2eb648bf059463f3e7c0fa688e005a734f4e Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 3 Nov 2026 15:40:27 -0800 Subject: [PATCH 07/17] docs(quic): describe terminal stream polling Co-Authored-By: GPT-6.1 Sol --- rs/moq-tokio/src/quic/web/recv.rs | 2 +- rs/moq-tokio/src/quic/web/send.rs | 2 +- rs/moq-uring/src/quic/web/send.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/rs/moq-tokio/src/quic/web/recv.rs b/rs/moq-tokio/src/quic/web/recv.rs index 9b7b71d9e1a..2611883cc74 100644 --- a/rs/moq-tokio/src/quic/web/recv.rs +++ b/rs/moq-tokio/src/quic/web/recv.rs @@ -48,7 +48,7 @@ impl Stream { } /// Poll until the peer resets the stream, the receiver evicts it, or the session closes, - /// without reading it; pending for good once every byte arrived. See + /// without reading it; pending for good once every byte was read. See /// [`recv::Stream::received_reset`]. pub fn poll_reset(&mut self, cx: &mut Context<'_>) -> Poll { let close = &self.close; diff --git a/rs/moq-tokio/src/quic/web/send.rs b/rs/moq-tokio/src/quic/web/send.rs index 3800c169c17..2b970686158 100644 --- a/rs/moq-tokio/src/quic/web/send.rs +++ b/rs/moq-tokio/src/quic/web/send.rs @@ -152,7 +152,7 @@ impl Stream { } /// Mark the stream as finished, such that no more data can be written. See - /// [`send::Stream::finish`]. Fails if it was already finished or reset. + /// [`send::Stream::finish`]. Does nothing if it was already finished or reset. fn finish_mut(&mut self) { if matches!(self.state, State::Open(_)) { let State::Open(stream) = std::mem::replace(&mut self.state, State::Reset(Ok(None))) else { diff --git a/rs/moq-uring/src/quic/web/send.rs b/rs/moq-uring/src/quic/web/send.rs index e7496c5848b..f375696d3f7 100644 --- a/rs/moq-uring/src/quic/web/send.rs +++ b/rs/moq-uring/src/quic/web/send.rs @@ -164,7 +164,7 @@ impl Stream { } /// Mark the stream as finished, such that no more data can be written. See - /// [`send::Stream::finish`]. Fails if it was already finished or reset. + /// [`send::Stream::finish`]. Does nothing if it was already finished or reset. fn finish_mut(&mut self) { if matches!(self.state, State::Open(_)) { let State::Open(stream) = std::mem::replace(&mut self.state, State::Reset(Ok(None))) else { From 374c8b244a3eb5a9fba1213d5b235dae19b394e9 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 3 Nov 2026 15:41:08 -0800 Subject: [PATCH 08/17] docs(uring): link the shared CONNECT request URL field Co-Authored-By: GPT-6.1 Sol --- rs/moq-uring/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rs/moq-uring/src/lib.rs b/rs/moq-uring/src/lib.rs index b13d7550aa7..62ee4401793 100644 --- a/rs/moq-uring/src/lib.rs +++ b/rs/moq-uring/src/lib.rs @@ -61,7 +61,7 @@ pub use timer::Timer; pub use worker::{Config, Handle, Worker}; /// The `url` crate, re-exported because -/// [`quic::web::Request::url`](crate::quic::web::Request::url) hands one back. +/// [`quic::web::proto::ConnectRequest::url`](crate::quic::web::proto::ConnectRequest::url) hands one back. /// Naming that type otherwise means depending on a matching `url` version /// directly, so a major bump here is a breaking change for this crate. pub use url; From daa27366625a4b07f4e22334c5a82ea837dd0b36 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 3 Nov 2026 15:49:50 -0800 Subject: [PATCH 09/17] test(uring): recycle ready-stream credit with partial headers held Co-Authored-By: GPT-6.1 Sol --- rs/moq-uring/tests/integration/web.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rs/moq-uring/tests/integration/web.rs b/rs/moq-uring/tests/integration/web.rs index 3e0be722106..1304a16523f 100644 --- a/rs/moq-uring/tests/integration/web.rs +++ b/rs/moq-uring/tests/integration/web.rs @@ -1018,7 +1018,7 @@ fn partial_headers_do_not_block_ready_streams() { drop(worker); for pending in [0, 64, 256] { for ready in [1, 16] { - super::web_headers::run(pending, ready, 1); + super::web_headers::run(pending, ready, 16); } } } From b5f43b7c7dfae8f569df15a5e9b9cb906e13187d Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 3 Nov 2026 15:58:42 -0800 Subject: [PATCH 10/17] fix(quic): return stream credit before the update batch can deadlock Co-Authored-By: GPT-6.1 Sol --- rs/moq-quic/README.md | 1 + rs/moq-quic/src/connection/streams/state.rs | 61 ++++++++++++++++++++- 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/rs/moq-quic/README.md b/rs/moq-quic/README.md index 31981d63440..45dba2d7c8e 100644 --- a/rs/moq-quic/README.md +++ b/rs/moq-quic/README.md @@ -96,6 +96,7 @@ Changes on top of the upstream commit, besides the renames: - **Stopped after a local reset**: moq-tokio's driver resolves `send::Finished::stopped` with `None` once the stream is reset locally, as it already did once the reset was acknowledged and the stream freed. A local reset wakes `send::Stream::poll_end`, where quinn reports no event for it, so quinn leaves a pending `stopped` waiting, and its waker registered, until the connection closes. Not offered upstream yet; quinn 0.11 has the same gap. - **Namespaced streams** (`send`, `recv`, `stream`): quinn's `SendStream`, `RecvStream`, and their error zoo become `send::Stream` and `recv::Stream` with one `send::Error` (streams and datagrams) and one `recv::Error` (reads and leases), adding `Blocked`, and `Closed` for an ID the stream no longer has state for. `send::Stream::write` takes `&mut impl Buf`, splitting a `Bytes` source off without a copy, in place of `write` and `write_chunks`, and `Datagrams::send` takes `impl Buf` the same way, so a chained header costs no copy. An outgoing stream's `send::End` (finished and acknowledged, stopped, or reset) stays queryable until `send::Stream::release`, so a driver needs no table of its own for how a stream ended. A `send::Offset` comes only from `offset()` and is tagged to its stream, so `reset_at` and `acked` cannot be handed one past the data written. A port touching a public stream method has to be redone against these names. - **Connection limits** (`Limits`, `Connection::set_limits`, and the drivers' `set_limits`): one validated call changes a live connection's stream limit and windows, refusing values a peer would reject and changing nothing then. moq-tokio and moq-uring re-export the type, so both runtimes check limits the same way. Not offered upstream: it only bundles quinn's existing setters. +- **Stream credit with held streams**: batch `MAX_STREAMS` updates only while the peer still has advertised stream credit. Once it opens the last advertised stream, return freed credit even below the batch threshold, so long-lived streams cannot deadlock the others. The same threshold remains in quinn `main`; not offered upstream yet. - **Indexed send buffer** (`SendBuffer::get`): each retained write keeps its stream offset, and a STREAM frame's data comes from one binary search, instead of quinn walking every write from the oldest unacknowledged one for each piece, which costs a triangular number of visits when many small writes are in flight. Not offered upstream yet. - **No receive-state recycling** (`StreamsState::stream_recv_freed`): a completed stream's `Recv` is dropped, where quinn keeps it in `free_recv` with its chunk list at peak capacity for the next stream. Recycled state stayed allocated per peak concurrent stream for the connection's lifetime: about 550 B per stream, and about 113 KiB for one that once buffered a 2,048-packet backlog. Reusing it saved only 0.1 to 0.3% of a burst's instructions. `benches/receive_bursts.rs` measures both. Stream ID hash tables and waiter tables contract geometrically as streams go away and release every slot when empty; drained control queues and obsolete credit updates are dropped when nothing remains to send. A surviving stream cannot pin a burst's hash-table capacity. A cherry-pick touching `StreamRecv`, `free_recv`, or `reinit` has nothing to apply to. Not offered upstream yet. - **Bandwidth probing** (`Connection::set_probe_target`, `connection/bandwidth_probe.rs`): an application-limited sender below its target fills the headroom BBR3 opens, in Startup and in one ProbeBW\_UP per probe wait, with duplicates of STREAM data still in flight, paced to the target, so the controller's samples measure the path rather than the application. That inflight bounds an attempt like a window once it sends its first duplicate; media alone past it, as beside another flow's standing queue, leaves the sender application-limited. `Controller::probes` and `Metrics::probe_inflight` tell the sender when and how far it may fill; CUBIC never does. A duplicate is never retransmitted, a reset or stopped stream's data is never resent, and a loss retransmits only the ranges no copy delivered (`SendBuffer::retransmit`). An attempt that raises nothing for two round trips, or ends short of the target, sits out 3 s and then 4 s before retrying the same target; reaching the target or a new one retries at once. `PathStats::sent_bandwidth_probe_bytes` counts the redundant bytes. A cherry-pick touching `poll_transmit`'s space loop or `populate_packet` has to keep the probe after the ordinary frames. Not offered upstream. diff --git a/rs/moq-quic/src/connection/streams/state.rs b/rs/moq-quic/src/connection/streams/state.rs index 32dd543c783..c974da8677f 100644 --- a/rs/moq-quic/src/connection/streams/state.rs +++ b/rs/moq-quic/src/connection/streams/state.rs @@ -958,9 +958,12 @@ impl StreamsState { let mut queued = false; for dir in Dir::iter() { let diff = self.max_remote[dir as usize] - self.sent_max_remote[dir as usize]; - // To reduce traffic, only announce updates if at least 1/8 of the flow control window - // has been consumed. - if diff > self.max_concurrent_remote_count[dir as usize] / 8 { + // Batch updates while the peer still has advertised credit. Long-lived streams can + // hold most of the window, so waiting for a batch after it is spent would deadlock. + if diff > 0 + && (diff > self.max_concurrent_remote_count[dir as usize] / 8 + || self.next_remote[dir as usize] >= self.sent_max_remote[dir as usize]) + { pending.max_stream_id[dir as usize] = true; queued = true; } @@ -1364,6 +1367,58 @@ mod tests { ) } + /// Long-lived streams cannot hold the update batch hostage when one other stream turns over. + #[test] + fn held_streams_do_not_withhold_returned_credit() { + for dir in Dir::iter() { + let mut server = make(Side::Server); + let mut pending = Retransmits::default(); + for index in 0..127 { + let id = StreamId::new(Side::Client, dir, index); + data(&mut server, id, 0, 1, false); + assert_eq!(Streams::new(&mut server, &ConnState::Established).accept(dir), Some(id)); + } + for index in 127..131 { + let id = StreamId::new(Side::Client, dir, index); + data(&mut server, id, 0, 1, true); + assert_eq!(Streams::new(&mut server, &ConnState::Established).accept(dir), Some(id)); + let mut stream = recv(&mut server, &mut pending, id); + let mut chunks = stream.read().unwrap(); + assert_eq!(chunks.next(1).unwrap().unwrap().bytes.len(), 1); + assert!(chunks.next(1).unwrap().is_none()); + chunks.finalize(); + if dir == Dir::Bi { + let mut stream = send::Stream { + id, + state: &mut server, + pending: &mut pending, + conn_state: &ConnState::Established, + }; + stream.finish().unwrap(); + stream.release(); + server.received_ack_of(frame::StreamMeta { + id, + offsets: 0..0, + fin: true, + }); + } + server.queue_max_stream_id(&mut pending); + assert!( + pending.max_stream_id[dir as usize], + "the peer has spent every advertised stream" + ); + server.write_control_frames( + &mut Vec::new(), + &mut pending, + &mut ThinRetransmits::default(), + &mut FrameStats::default(), + 1200, + ); + assert_eq!(server.sent_max_remote[dir as usize], index + 2); + } + } + } + /// Notifications coalesce while a reader is parked, and freed streams leave no table storage. #[test] fn receive_bursts_release_stream_storage() { From 814249d90a4671db6e68bbefe37b67c3918e5fd2 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 3 Nov 2026 16:02:47 -0800 Subject: [PATCH 11/17] quest: complete native WebTransport poll transports Co-Authored-By: GPT-6.1 Sol --- quest/m1/quic-api/README.md | 4 --- quest/m1/quic-api/native-web.md | 46 --------------------------- quest/m2/perf/quic-lock-contention.md | 4 --- quest/m2/perf/relay-allocs.md | 4 --- 4 files changed, 58 deletions(-) delete mode 100644 quest/m1/quic-api/native-web.md diff --git a/quest/m1/quic-api/README.md b/quest/m1/quic-api/README.md index 354aced6f89..d6d42e547cf 100644 --- a/quest/m1/quic-api/README.md +++ b/quest/m1/quic-api/README.md @@ -44,10 +44,6 @@ This README's own work, after the children: read the three layers' rustdoc side by side and fix any name or signature that still differs, and check `doc/lib/rs/index.md`'s driver line still holds. -## Required - -- [Native WebTransport](/quest/m1/quic-api/native-web.md) - `quic::web` in both crates implements moq-net's poll traits, deleting the generic `web_transport_trait` adapters and the dependency on it - ## Related - [poll_acked](/quest/m2/quic-ack-hook.md) - the m2 consumer that keeps `send::Stream::acked` in the API diff --git a/quest/m1/quic-api/native-web.md b/quest/m1/quic-api/native-web.md deleted file mode 100644 index 0a8804b7d89..00000000000 --- a/quest/m1/quic-api/native-web.md +++ /dev/null @@ -1,46 +0,0 @@ -# [L] Native WebTransport implements moq-net's traits - -## Goal - -`moq_tokio::quic::web` and `moq_uring::quic::web` implement moq-net's -`transport::poll` traits directly over their drivers' poll forms, keeping -real send groups. Their `web_transport_trait` impls, the sealed -`Grouped`/`GroupedSend` pair, and the generic adapters over -`web_transport_trait` are gone: moq-tokio's async-to-poll -`transport::Session` and moq-uring's `transport::Session`, which serve -only native QUIC. moq-tokio, moq-uring and moq-relay then no longer depend on -`web-transport-trait`; qmux keeps it as a dev-dependency for its 0.6 interop -tests. - -## Plan - -The native trait migration is implemented on this quest's branch. Finish its -validation before removing this quest: - -- Run local checks and the cross-runtime stream, FIN, send-group and receive-credit - tests on the final committed head. The empty-FIN close watch fails on the - driver-poll base and must pass on both stacks. -- Run the io_uring `web_headers` pending × ready sweep. It covers uni and bi - headers, later fragmented completion, preserved payloads and FIN, and stream - credit returning after consumption. The classifier polls only woken entries; - three HTTP/3 critical-stream slots replace the old retained-stream cap. -- Measure relay CPU per Gbps and allocations per stream before and after, with - the same workload and disclosed host load. `fanout` also measures the native - poll interface directly with allocation counters (`NATIVE_ALLOCS=1`). -- Review the checked head locally and address findings before readiness. - -The supported common API uses `web::send::Stream`, `web::recv::Stream`, their -role-local errors, consuming terminal methods, and `send::Finished` futures. -Sessions implement moq-net's poll traits directly with real send groups; -io_uring retains only `transport::Local` for moq-net's thread bound. -Both handshakes use `proto::ConnectResponse` and HTTP status codes. Connection -access is explicit, rather than through the Tokio session's `Deref`. - -The shared strict-flake runner failure writing `/proc/self/uid_map` is an -existing environment limitation. Preserve the probe/skip policy for ordinary -checks and the intentional failure under strict CI, and record base/head -failures without changing the harness to pass. - -## Related - -- [Relay allocations](/quest/m2/perf/relay-allocs.md) - retakes its profile after the adapter's per-stream boxing is gone diff --git a/quest/m2/perf/quic-lock-contention.md b/quest/m2/perf/quic-lock-contention.md index 8996e7e750f..00b2d56b673 100644 --- a/quest/m2/perf/quic-lock-contention.md +++ b/quest/m2/perf/quic-lock-contention.md @@ -62,10 +62,6 @@ weighs. A measured no-win is a valid outcome. Report public API and wire impact; the staging queue and batch dispatch should need none. -## Required - -- [Native WebTransport](/quest/m1/quic-api/native-web.md) - removes the adapter's per-operation boxing, which would otherwise dominate the profile - ## Related - [Relay egress profile](/quest/m2/quic-egress-profile.md) - the transmit side of the same path diff --git a/quest/m2/perf/relay-allocs.md b/quest/m2/perf/relay-allocs.md index 2f98fd54800..7dc343be085 100644 --- a/quest/m2/perf/relay-allocs.md +++ b/quest/m2/perf/relay-allocs.md @@ -47,7 +47,3 @@ removal is [native WebTransport](/quest/m1/quic-api/native-web.md), so the relay's QUIC profile waits on it. Public API: none expected. Wire: none. - -## Required - -- [Native WebTransport](/quest/m1/quic-api/native-web.md) - removes the QUIC path's per-stream session clone and boxed future, so the profile is retaken after it From 20e94509dd9a2507614a25d82b96751b9a35abec Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 3 Nov 2026 16:05:05 -0800 Subject: [PATCH 12/17] quest: refresh references to the completed native transport work Co-Authored-By: GPT-6.1 Sol --- quest/m1/quic-api/README.md | 2 +- quest/m2/net-wrap/README.md | 2 +- quest/m2/perf/relay-allocs.md | 6 +++--- quest/m2/quic-sansio-driver.md | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/quest/m1/quic-api/README.md b/quest/m1/quic-api/README.md index d6d42e547cf..e7e42d11495 100644 --- a/quest/m1/quic-api/README.md +++ b/quest/m1/quic-api/README.md @@ -1,4 +1,4 @@ -# moq-quic's API improves on quinn's +# [S] moq-quic's API improves on quinn's ## Goal diff --git a/quest/m2/net-wrap/README.md b/quest/m2/net-wrap/README.md index 85ac12834f5..ed1ca3d9ead 100644 --- a/quest/m2/net-wrap/README.md +++ b/quest/m2/net-wrap/README.md @@ -16,7 +16,7 @@ moq-flate, so moq-flate is out of scope. Non-goals: - A runtime seam: libraries poll and spawn nothing ([Libraries spawn nothing](/quest/m1/poll-first/no-spawn/README.md)). -- The transport seam: custom transports implement moq-net's `transport::poll` traits ([native WebTransport](/quest/m1/quic-api/native-web.md) moves the last backend onto them). +- The transport seam: custom transports implement moq-net's `transport::poll` traits, including both native WebTransport backends. ## Plan diff --git a/quest/m2/perf/relay-allocs.md b/quest/m2/perf/relay-allocs.md index 7dc343be085..c85120c8685 100644 --- a/quest/m2/perf/relay-allocs.md +++ b/quest/m2/perf/relay-allocs.md @@ -42,8 +42,8 @@ Decided 2026-10-17 (maintainer): m2, under perf. Decided 2026-10-22 (maintainer): this waits on the work that replaces the session clones and boxed opens at the top of this profile; re-profile after -it lands before fixing what remains. Since 2026-10-23 the QUIC half of that -removal is [native WebTransport](/quest/m1/quic-api/native-web.md), so the -relay's QUIC profile waits on it. +it lands before fixing what remains. The QUIC adapters are now removed by +[moq-dev/moq#7771](https://github.com/moq-dev/moq/pull/7771); re-profile the +native poll implementations before fixing what remains. Public API: none expected. Wire: none. diff --git a/quest/m2/quic-sansio-driver.md b/quest/m2/quic-sansio-driver.md index f95ac9f13e7..21f53f8779d 100644 --- a/quest/m2/quic-sansio-driver.md +++ b/quest/m2/quic-sansio-driver.md @@ -35,7 +35,7 @@ Open, for whoever starts it: - Where it lives: a module in moq-quic, which doesn't depend on moq-net today, or a crate of its own. - Whether it ships with WebTransport over HTTP/3, sharing - [native WebTransport](/quest/m1/quic-api/native-web.md)'s `quic::web`, or with raw + the drivers' supported `quic::web` API, or with raw QUIC's moq ALPN first. - How it builds its TLS configs (identity, roots, fingerprint pinning) without moq-tokio's `tls` module. From 5d47c8556c82a61f707ef3b84cac8ad122804c23 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 3 Nov 2026 16:20:57 -0800 Subject: [PATCH 13/17] fix(quic): validate rejections and retain header stop codes Co-Authored-By: GPT-6.1 Sol --- rs/moq-tokio/MIGRATE.md | 2 +- rs/moq-tokio/src/quic/web/error.rs | 18 +++++++++++++++ rs/moq-tokio/src/quic/web/server.rs | 5 ++++- rs/moq-tokio/src/quic/web/session.rs | 23 +++++++++++++++++++ rs/moq-uring/MIGRATE.md | 2 +- rs/moq-uring/src/quic/web.rs | 5 ++++- rs/moq-uring/tests/integration/web.rs | 32 +++++++++++++++++++++++++++ 7 files changed, 83 insertions(+), 4 deletions(-) diff --git a/rs/moq-tokio/MIGRATE.md b/rs/moq-tokio/MIGRATE.md index 7f0a11787ad..4e8fdb9fbf1 100644 --- a/rs/moq-tokio/MIGRATE.md +++ b/rs/moq-tokio/MIGRATE.md @@ -23,7 +23,7 @@ Session polls take `&self` and a caller-owned `&kio::Waiter`; opens also take th `Request` dereferences to `proto::ConnectRequest`; read its `url` and `protocols` fields there. `Request::connection()` names the underlying QUIC connection. -`Request::respond` takes `impl Into` and requires status OK; `reject` takes an `http::StatusCode`. +`Request::respond` takes `impl Into` and requires status OK; `reject` takes an `http::StatusCode` and requires a 4xx or 5xx status. The Tokio `web::Server` helper and `Request::connect()` accessor are removed; accept from the driver endpoint, then call `Request::accept(connection)`. `send::Stream`, `send::Finished`, and `recv::Stream` expose their stream ID as `id()`. diff --git a/rs/moq-tokio/src/quic/web/error.rs b/rs/moq-tokio/src/quic/web/error.rs index f3d29d8cd79..9d0d36b4ef5 100644 --- a/rs/moq-tokio/src/quic/web/error.rs +++ b/rs/moq-tokio/src/quic/web/error.rs @@ -239,6 +239,15 @@ impl moq_net::transport::Error for SessionError { None } + fn stream_error(&self) -> Option { + // An open can fail while writing its WebTransport header, before it returns a stream. + if let Self::WebTransportError(WebTransportError::WriteError(crate::quic::driver::send::Error::Stopped(code))) = + self + { + return web_transport_proto::error_from_http3(code.into_inner()); + } + None + } } #[cfg(test)] @@ -270,6 +279,15 @@ mod tests { let stream = super::super::recv::Error::Reset(7); assert_eq!(stream.session_error(), None); assert_eq!(stream.stream_error(), Some(7)); + let close = CloseReason::new(false); + let header = SessionError::WebTransportError(WebTransportError::WriteError( + crate::quic::driver::send::Error::Stopped(close.encode_stream_code(7)), + )); + assert_eq!(header.stream_error(), Some(7)); + let invalid = SessionError::WebTransportError(WebTransportError::WriteError( + crate::quic::driver::send::Error::Stopped(7u32.into()), + )); + assert_eq!(invalid.stream_error(), None); } /// WebTransport keeps the HTTP/3 mapping in both directions, and a code outside its diff --git a/rs/moq-tokio/src/quic/web/server.rs b/rs/moq-tokio/src/quic/web/server.rs index 48955255b2f..63a29aab58a 100644 --- a/rs/moq-tokio/src/quic/web/server.rs +++ b/rs/moq-tokio/src/quic/web/server.rs @@ -45,8 +45,11 @@ impl Request { Ok(Session::new(self.conn, self.settings, connect)) } - /// Reject the session with the given status code. + /// Reject the session with a 4xx or 5xx status code. pub async fn reject(self, status: http::StatusCode) -> Result<(), ServerError> { + if !status.is_client_error() && !status.is_server_error() { + return Err(super::ConnectError::ErrorStatus(status).into()); + } self.connect.reject(status).await?; Ok(()) } diff --git a/rs/moq-tokio/src/quic/web/session.rs b/rs/moq-tokio/src/quic/web/session.rs index c8535da177f..a4c9f927867 100644 --- a/rs/moq-tokio/src/quic/web/session.rs +++ b/rs/moq-tokio/src/quic/web/session.rs @@ -1261,6 +1261,29 @@ mod tests { } /// An empty FIN needs no read; payload remains readable before the close watch ends. + /// Rejecting a CONNECT cannot accidentally advertise success or an interim response. + #[tokio::test] + async fn a_rejection_refuses_non_error_statuses() { + use futures::FutureExt; + for status in [ + http::StatusCode::CONTINUE, + http::StatusCode::OK, + http::StatusCode::NO_CONTENT, + http::StatusCode::FOUND, + ] { + let (client, server, _endpoints) = pair().await; + let (client, ()) = tokio::join!( + Session::connect(client, url::Url::parse("https://localhost/").unwrap()), + async { + let request = crate::quic::web::Request::accept(server).await.unwrap(); + assert!(matches!(request.reject(status).now_or_never(), + Some(Err(crate::quic::web::ServerError::ConnectError(crate::quic::web::ConnectError::ErrorStatus(actual)))) if actual == status)); + }, + ); + assert!(client.is_err()); + } + } + #[tokio::test] async fn clean_fin_ends_close_watch() { for framed in [false, true] { diff --git a/rs/moq-uring/MIGRATE.md b/rs/moq-uring/MIGRATE.md index 65583a3f7d8..4d53379829e 100644 --- a/rs/moq-uring/MIGRATE.md +++ b/rs/moq-uring/MIGRATE.md @@ -20,7 +20,7 @@ Session polls take `&self` and a caller-owned `&kio::Waiter`; opens also take th `Request` dereferences to `proto::ConnectRequest`; read its `url` and `protocols` fields there. `Request::connection()` names the underlying QUIC connection. -`Request::respond` takes `impl Into` and requires status OK; `reject` takes an `http::StatusCode`. +`Request::respond` takes `impl Into` and requires status OK; `reject` takes an `http::StatusCode` and requires a 4xx or 5xx status. Replace `web::Response::default()` with `web::proto::ConnectResponse::OK` and `web::Rejected` variants with HTTP status codes. `send::Stream`, `send::Finished`, and `recv::Stream` expose their stream ID as `id()`. diff --git a/rs/moq-uring/src/quic/web.rs b/rs/moq-uring/src/quic/web.rs index 63d5c27bf8b..a4fd045848e 100644 --- a/rs/moq-uring/src/quic/web.rs +++ b/rs/moq-uring/src/quic/web.rs @@ -264,7 +264,7 @@ impl Request { self.respond(proto::ConnectResponse::OK).await } - /// Refuse with `reason`, ending the handshake. + /// Refuse with a 4xx or 5xx status code, ending the handshake. /// /// Returns once the peer has the response, or after a grace period if it /// never acknowledges one, and closes the connection deliberately. The @@ -273,6 +273,9 @@ impl Request { /// error, so tearing them down here would show the peer an H3 failure /// instead of the status it was sent. pub async fn reject(mut self, status: http::StatusCode) -> Result<(), Error> { + if !status.is_client_error() && !status.is_server_error() { + return Err(Error::Web("reject requires a 4xx or 5xx status".into())); + } let response = proto::ConnectResponse::new(status); let mut buf = Vec::new(); response.encode(&mut buf).map_err(|err| Error::Web(err.to_string()))?; diff --git a/rs/moq-uring/tests/integration/web.rs b/rs/moq-uring/tests/integration/web.rs index 1304a16523f..533994cb4a7 100644 --- a/rs/moq-uring/tests/integration/web.rs +++ b/rs/moq-uring/tests/integration/web.rs @@ -540,6 +540,38 @@ fn a_rejection_reaches_the_peer() { ); } +/// A local rejection refuses success and non-error statuses before writing any response. +#[test] +fn a_rejection_refuses_non_error_statuses() { + use futures::FutureExt; + let Some(mut worker) = worker() else { return }; + let handle = worker.handle(); + let certs = support::certs().expect("certificates"); + let endpoint = h3_endpoint(&handle, &certs); + let addr = endpoint.local_addr(); + for status in [ + http::StatusCode::CONTINUE, + http::StatusCode::OK, + http::StatusCode::NO_CONTENT, + http::StatusCode::FOUND, + ] { + let client = tokio_client_err(format!("https://{addr}/nope")); + worker + .block_on(async { + let conn = endpoint.accept().await.unwrap(); + let watch = conn.clone(); + let request = quic::web::Request::accept(conn).await.unwrap(); + assert!(matches!( + request.reject(status).now_or_never(), + Some(Err(quic::Error::Web(_))) + )); + watch.closed().await; + }) + .unwrap(); + client.join().expect("client thread"); + } +} + /// Dropping a web-mode stream cancels it with a WebTransport code, not the /// raw zero the inner stream would send. /// From fd5d25e5c79d3de5e5e3ec512a7e8aebc93c989a Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 3 Nov 2026 16:27:49 -0800 Subject: [PATCH 14/17] docs(quic): attach the FIN test comment to its test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply the local reviewer’s comment move unchanged. Co-Authored-By: GPT-6.1 Sol --- rs/moq-tokio/src/quic/web/session.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rs/moq-tokio/src/quic/web/session.rs b/rs/moq-tokio/src/quic/web/session.rs index a4c9f927867..10b8ab822a9 100644 --- a/rs/moq-tokio/src/quic/web/session.rs +++ b/rs/moq-tokio/src/quic/web/session.rs @@ -1260,7 +1260,6 @@ mod tests { (client_conn, server_conn, [client, server], gate) } - /// An empty FIN needs no read; payload remains readable before the close watch ends. /// Rejecting a CONNECT cannot accidentally advertise success or an interim response. #[tokio::test] async fn a_rejection_refuses_non_error_statuses() { @@ -1284,6 +1283,7 @@ mod tests { } } + /// An empty FIN needs no read; payload remains readable before the close watch ends. #[tokio::test] async fn clean_fin_ends_close_watch() { for framed in [false, true] { From 57b6382b408c15dc3a03708e0b260e4e680eeb67 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 3 Nov 2026 22:33:41 -0800 Subject: [PATCH 15/17] fix(quic): keep reset, stop, lease, and datagram outcomes in native WebTransport - Tokio send reset reads the driver-recorded stop after the reset again, so a STOP_SENDING racing the reset still reports its code. - Tokio's receive close watch returns a cached reset instead of a clean end. - moq-quic does not report the end of a finished stream while leased bytes are unread, so a close watch no longer discards them. - Both stacks drop a datagram that names no session, or another one, instead of failing the session. Co-Authored-By: Claude Opus 5.5 --- rs/moq-quic/src/connection/streams/lease.rs | 6 ++ rs/moq-quic/src/connection/streams/recv.rs | 7 +- rs/moq-quic/src/connection/streams/state.rs | 23 ++++ rs/moq-tokio/src/quic/driver/recv.rs | 4 + rs/moq-tokio/src/quic/web/send.rs | 47 ++++---- rs/moq-tokio/src/quic/web/session.rs | 111 +++++++++++++++++-- rs/moq-uring/src/quic/web.rs | 23 ++-- rs/moq-uring/tests/integration/web.rs | 112 ++++++++++++++++++++ 8 files changed, 297 insertions(+), 36 deletions(-) diff --git a/rs/moq-quic/src/connection/streams/lease.rs b/rs/moq-quic/src/connection/streams/lease.rs index ecf377ef018..95ee5d78016 100644 --- a/rs/moq-quic/src/connection/streams/lease.rs +++ b/rs/moq-quic/src/connection/streams/lease.rs @@ -70,6 +70,12 @@ impl Leased { matches!(self.end, Some(End::Fin)) } + /// Whether the reader leased bytes it has not read: they sit here, or are out in a + /// [`Lease`] that may hand some back. + pub(super) fn has_unread(&self) -> bool { + self.len > 0 || self.out + } + /// The number of leased bytes not out in a [`Lease`]. #[cfg(test)] pub(super) fn len(&self) -> usize { diff --git a/rs/moq-quic/src/connection/streams/recv.rs b/rs/moq-quic/src/connection/streams/recv.rs index ab5bf48b0fc..b90348ebe2c 100644 --- a/rs/moq-quic/src/connection/streams/recv.rs +++ b/rs/moq-quic/src/connection/streams/recv.rs @@ -455,7 +455,8 @@ impl<'a> Chunks<'a> { /// The next contiguous chunk of at most `max_length` bytes, or `None` once the stream finished /// - /// Fails with [`Error::Blocked`] until more arrives, or [`Error::Reset`]. Call + /// Fails with [`Error::Blocked`] until more arrives, or [`Error::Reset`]. A finished stream + /// whose leased bytes are not all read stays blocked rather than finish without them. Call /// [`Self::finalize()`] when done. pub fn next(&mut self, max_length: usize) -> Result, Error> { let rs = match &mut self.state { @@ -490,7 +491,9 @@ impl<'a> Chunks<'a> { Err(Error::Reset(error_code)) } RecvState::Recv { size } => { - if size == Some(rs.end) && rs.assembler.bytes_read() == rs.end { + // Leased bytes left the assembler unread, so the end is not reached until they + // are read: freeing the stream now would discard them. + if size == Some(rs.end) && rs.assembler.bytes_read() == rs.end && !rs.lease.has_unread() { let state = mem::replace(&mut self.state, ChunksState::Finished); // At this point if we have `rs` self.state must be `ChunksState::Readable` let recv = match state { diff --git a/rs/moq-quic/src/connection/streams/state.rs b/rs/moq-quic/src/connection/streams/state.rs index c974da8677f..2795a4c4354 100644 --- a/rs/moq-quic/src/connection/streams/state.rs +++ b/rs/moq-quic/src/connection/streams/state.rs @@ -4488,6 +4488,29 @@ mod tests { assert_eq!(stream.lease(0).unwrap().remaining(), 350); } + /// A read cannot reach the end of a finished stream past leased bytes nobody read, since + /// freeing the stream then would discard them. Once they are read, it ends. + #[test] + fn unread_lease_holds_the_end_off() { + let mut client = make(Side::Client); + let id = StreamId::new(Side::Server, Dir::Uni, 0); + data(&mut client, id, 0, 32, true); + let mut pending = Retransmits::default(); + let mut stream = recv(&mut client, &mut pending, id); + + let lease = stream.lease(0).unwrap(); + assert!(lease.is_fin()); + stream.restore(lease); + assert!(matches!(stream.read().unwrap().next(0), Err(recv::Error::Blocked))); + + let mut lease = stream.lease(0).unwrap(); + assert_eq!(lease.remaining(), 32, "the bytes survive"); + lease.advance(32); + stream.release(lease); + assert!(matches!(stream.read().unwrap().next(0), Ok(None))); + assert!(!client.recv.contains_key(&id), "freed once read"); + } + /// Leasing again before handing the last lease back would lose its bytes. #[test] #[should_panic(expected = "leased again before the last lease was handed back")] diff --git a/rs/moq-tokio/src/quic/driver/recv.rs b/rs/moq-tokio/src/quic/driver/recv.rs index 209cd8063f8..adb6283ace6 100644 --- a/rs/moq-tokio/src/quic/driver/recv.rs +++ b/rs/moq-tokio/src/quic/driver/recv.rs @@ -231,6 +231,10 @@ impl Stream { /// Observe FIN without consuming application bytes, including an empty stream's end. #[cfg(feature = "quic")] pub(crate) fn poll_closed(&mut self, cx: &Context<'_>) -> Poll, Error>> { + // A read that met the reset marks every byte read, so only the cached code still says how it ended. + if let Some(code) = self.reader.reset { + return Poll::Ready(Ok(Some(code))); + } let waiter = self.park.hold(cx); let read = self .reader diff --git a/rs/moq-tokio/src/quic/web/send.rs b/rs/moq-tokio/src/quic/web/send.rs index 2b970686158..f0b47c7e3f1 100644 --- a/rs/moq-tokio/src/quic/web/send.rs +++ b/rs/moq-tokio/src/quic/web/send.rs @@ -8,6 +8,7 @@ use std::{ }; use bytes::Buf; +use futures::FutureExt; use crate::quic::driver::send; use crate::quic::web::{CloseReason, SessionError}; @@ -81,26 +82,34 @@ impl Stream { /// belongs to. fn reset_mut(&mut self, code: u32) { let code = self.close.encode_stream_code(code); - let stopped = match &self.state { - State::Open(stream) => stream.poll_closed(&kio::Waiter::noop()), - State::Finished(stream) => stream.poll_closed(&kio::Waiter::noop()), - State::Reset(_) => return, - }; - let stopped = match stopped { - Poll::Ready(result) => result, - Poll::Pending => Ok(None), + // The driver task can process a STOP_SENDING at any moment. The reset settles, under the + // connection lock, whether the peer's stop came first, and only a `stopped` future taken + // before the reset can still read that once the stream is consumed. The reset, or the + // lost connection that made it fail, settles it at once. + let stopped = match std::mem::replace(&mut self.state, State::Reset(Ok(None))) { + State::Open(stream) => { + let stopped = stream.stopped(); + match self.header { + Some(header) => stream.reset_at(header, code), + None => stream.reset(code), + } + stopped.now_or_never() + } + State::Finished(stream) => { + let stopped = stream.stopped(); + match self.header { + Some(header) => stream.reset_at(header, code), + None => stream.reset(code), + } + stopped.now_or_never() + } + state @ State::Reset(_) => { + self.state = state; + return; + } }; - match std::mem::replace(&mut self.state, State::Reset(stopped)) { - State::Open(stream) => match self.header { - Some(header) => stream.reset_at(header, code), - None => stream.reset(code), - }, - State::Finished(stream) => match self.header { - Some(header) => stream.reset_at(header, code), - None => stream.reset(code), - }, - State::Reset(_) => unreachable!(), - } + let stopped = stopped.expect("a reset settles its stopped future"); + self.state = State::Reset(stopped); } /// Poll until all data was acknowledged, the peer stopped the stream, or the session closed. diff --git a/rs/moq-tokio/src/quic/web/session.rs b/rs/moq-tokio/src/quic/web/session.rs index 10b8ab822a9..4b6968fff51 100644 --- a/rs/moq-tokio/src/quic/web/session.rs +++ b/rs/moq-tokio/src/quic/web/session.rs @@ -352,16 +352,24 @@ impl Session { } /// Poll for a datagram with its WebTransport session header removed. + /// + /// A datagram that does not name this session, such as another session's on the same + /// connection, is dropped: datagrams are unreliable, so a stray one must not end the session. pub fn poll_recv_datagram(&self, waiter: &Waiter) -> Poll> { - let mut datagram = std::task::ready!(self.conn.poll_recv_datagram(waiter)).map_err(|e| self.map_error(e))?; - let mut cursor = Cursor::new(&datagram); - if let Some(session_id) = self.session_id { - let actual = VarInt::decode(&mut cursor).map_err(|_| WebTransportError::UnknownSession)?; - if actual != session_id { - return Poll::Ready(Err(WebTransportError::UnknownSession.into())); + loop { + let mut datagram = + std::task::ready!(self.conn.poll_recv_datagram(waiter)).map_err(|e| self.map_error(e))?; + let Some(session_id) = self.session_id else { + return Poll::Ready(Ok(datagram)); + }; + let mut cursor = Cursor::new(&datagram); + match VarInt::decode(&mut cursor) { + Ok(actual) if actual == session_id => { + return Poll::Ready(Ok(datagram.split_off(cursor.position() as usize))); + } + _ => tracing::debug!("dropping a datagram for an unknown session"), } } - Poll::Ready(Ok(datagram.split_off(cursor.position() as usize))) } /// Sends an application datagram to the remote peer, scheduled by `options`. @@ -1313,6 +1321,95 @@ mod tests { } } + /// Both a WebTransport session over `client`/`server`, or both raw QUIC. + async fn sessions( + framed: bool, + client: crate::quic::driver::connection::Connection, + server: crate::quic::driver::connection::Connection, + ) -> (Session, Session) { + if !framed { + return (Session::raw(client), Session::raw(server)); + } + let (client, server) = tokio::join!( + Session::connect(client, url::Url::parse("https://localhost/").unwrap()), + async { crate::quic::web::Request::accept(server).await.unwrap().ok().await }, + ); + (client.unwrap(), server.unwrap()) + } + + /// Once a read met the peer's reset, the close watch keeps reporting it rather than a clean end. + #[tokio::test] + async fn a_read_reset_stays_reset_for_the_close_watch() { + for framed in [false, true] { + let (client, server, _endpoints) = pair().await; + let (client, server) = sessions(framed, client, server).await; + let mut send = client.open_uni(Default::default()).await.unwrap(); + send.write_all(&b"partial"[..]).await.unwrap(); + let mut recv = server.accept_uni().await.unwrap(); + send.reset(7); + loop { + match recv.read_chunk(usize::MAX).await { + Ok(Some(_)) => continue, + Err(crate::quic::web::recv::Error::Reset(7)) => break, + other => panic!("expected the reset, got {other:?}"), + } + } + for _ in 0..2 { + assert!(matches!(recv.closed().await, Err(crate::quic::web::recv::Error::Reset(7)))); + } + } + } + + /// Leased bytes handed back unread hold the close watch off, rather than being discarded + /// behind a clean end; once read through a lease, the stream ends. + #[tokio::test] + async fn an_unread_lease_holds_the_close_watch_off() { + for framed in [false, true] { + let (client, server, _endpoints) = pair().await; + let (client, server) = sessions(framed, client, server).await; + let mut send = client.open_uni(Default::default()).await.unwrap(); + send.write_all(&b"leased"[..]).await.unwrap(); + let finished = send.finish(); + let mut recv = server.accept_uni().await.unwrap(); + // The FIN is acknowledged, so it has arrived and only the lease holds the end off. + assert!(finished.await.unwrap().is_none()); + + let mut cx = Context::from_waker(std::task::Waker::noop()); + let Poll::Ready(lease) = recv.poll_lease(&mut cx, 6) else { + panic!("acknowledged bytes are leasable"); + }; + let lease = lease.unwrap(); + assert!(lease.is_fin()); + drop(lease); + assert!(recv.poll_closed(&Waiter::noop()).is_pending()); + + let Poll::Ready(lease) = recv.poll_lease(&mut cx, 6) else { + panic!("restored bytes are leasable"); + }; + let mut lease = lease.unwrap(); + assert_eq!(lease.copy_to_bytes(6), &b"leased"[..]); + lease.release(); + assert!(matches!(recv.poll_closed(&Waiter::noop()), Poll::Ready(Ok(())))); + } + } + + /// A datagram that names no session, or another one, is dropped rather than ending the session. + #[tokio::test] + async fn a_stray_datagram_is_dropped() { + let (client, server, _endpoints) = pair().await; + let (client, server) = sessions(true, client, server).await; + // Session IDs are client bidi stream IDs, so 1 is never one; an empty datagram names none. + for stray in [&[0x01, b'x'][..], &[][..]] { + client.connection().send_datagram(stray, Default::default()).unwrap(); + } + client.send_datagram(&b"ok"[..], Default::default()).unwrap(); + let datagram = tokio::time::timeout(std::time::Duration::from_secs(10), server.read_datagram()) + .await + .expect("no datagram arrived") + .unwrap(); + assert_eq!(datagram, &b"ok"[..]); + } + /// Accepted headers buffered before the connection closed precede its terminal error. #[tokio::test] async fn buffered_stream_headers_precede_the_close() { diff --git a/rs/moq-uring/src/quic/web.rs b/rs/moq-uring/src/quic/web.rs index a4fd045848e..02df07b986e 100644 --- a/rs/moq-uring/src/quic/web.rs +++ b/rs/moq-uring/src/quic/web.rs @@ -698,15 +698,22 @@ impl Session { } /// Poll for the next datagram after its session header. + /// + /// A datagram that does not name this session, such as another session's on the same + /// connection, is dropped: datagrams are unreliable, so a stray one must not end the session. pub fn poll_recv_datagram(&self, waiter: &kio::Waiter) -> Poll> { - let datagram = ready!(self.conn.poll_recv_datagram(waiter)).map_err(|e| self.map_err(e))?; - let Some(web) = &self.web else { - return Poll::Ready(Ok(datagram)); - }; - let mut peek: &[u8] = &datagram; - match decode_varint(&mut peek) { - Some(id) if id == web.session_id => Poll::Ready(Ok(datagram.slice(datagram.len() - peek.len()..))), - _ => Poll::Ready(Err(Error::Web("unknown datagram session".into()))), + loop { + let datagram = ready!(self.conn.poll_recv_datagram(waiter)).map_err(|e| self.map_err(e))?; + let Some(web) = &self.web else { + return Poll::Ready(Ok(datagram)); + }; + let mut peek: &[u8] = &datagram; + match decode_varint(&mut peek) { + Some(id) if id == web.session_id => { + return Poll::Ready(Ok(datagram.slice(datagram.len() - peek.len()..))); + } + _ => tracing::debug!("dropping a datagram for an unknown session"), + } } } diff --git a/rs/moq-uring/tests/integration/web.rs b/rs/moq-uring/tests/integration/web.rs index 533994cb4a7..a81aed48993 100644 --- a/rs/moq-uring/tests/integration/web.rs +++ b/rs/moq-uring/tests/integration/web.rs @@ -288,6 +288,118 @@ fn a_clean_finish_closes_a_stream() { client.join().expect("client thread"); } +/// A datagram that names no session, or another one, is dropped rather than ending the session. +#[test] +fn a_stray_datagram_is_dropped() { + let Some(mut worker) = worker() else { return }; + let handle = worker.handle(); + let certs = support::certs().expect("certificates"); + let endpoint = h3_endpoint(&handle, &certs); + let client = tokio_client(format!("https://{}/stray", endpoint.local_addr()), |session| { + Box::pin(async move { + // Session IDs are client bidi stream IDs, so 1 is never one; an empty datagram names none. + for stray in [&[0x01, b'x'][..], &[][..]] { + session + .connection() + .send_datagram(stray, Default::default()) + .expect("send stray datagram"); + } + session + .send_datagram(PAYLOAD, Default::default()) + .expect("send datagram"); + let echoed = session.read_datagram().await.expect("read datagram"); + assert_eq!(&echoed[..], PAYLOAD, "datagram echo"); + session.close(0, "done"); + session.closed().await; + }) + }); + worker + .block_on(async move { + let conn = endpoint.accept().await.expect("accept"); + let request = quic::web::Request::accept(conn).await.expect("handshake"); + let session = request + .respond(quic::web::proto::ConnectResponse::OK.with_protocol(PROTO)) + .await + .expect("respond"); + let datagram = within(&handle, "the datagram", kio::wait(|waiter| session.poll_recv_datagram(waiter))) + .await + .expect("a stray datagram does not end the session"); + assert_eq!(&datagram[..], PAYLOAD); + session + .send_datagram(datagram, Default::default()) + .expect("send datagram"); + kio::wait(|waiter| session.poll_closed(waiter)).await; + }) + .expect("worker"); + client.join().expect("client thread"); +} + +/// Leased bytes handed back unread hold the close watch off, rather than being discarded +/// behind a clean end; once read through a lease, the stream ends. +#[test] +fn an_unread_lease_holds_the_close_watch_off() { + use bytes::Buf as _; + use std::task::{Context, Poll, Waker}; + + let Some(mut worker) = worker() else { return }; + let handle = worker.handle(); + let certs = support::certs().expect("certificates"); + let endpoint = h3_endpoint(&handle, &certs); + let client = tokio_client(format!("https://{}/lease", endpoint.local_addr()), |session| { + Box::pin(async move { + let mut send = session.open_uni(Default::default()).await.expect("open uni"); + send.write_all(PAYLOAD).await.expect("write"); + assert!(send.finish().await.expect("FIN delivered").is_none()); + let mut recv = session.accept_uni().await.expect("accept uni"); + assert_eq!(read_all(&mut recv).await, b"read"); + session.close(0, "done"); + session.closed().await; + }) + }); + worker + .block_on(async move { + let conn = endpoint.accept().await.expect("accept"); + let request = quic::web::Request::accept(conn).await.expect("handshake"); + let session = request + .respond(quic::web::proto::ConnectResponse::OK.with_protocol(PROTO)) + .await + .expect("respond"); + let mut recv = kio::wait(|waiter| session.poll_accept_uni(waiter)) + .await + .expect("accept uni"); + // Wanting more than was sent resolves only at the FIN; the lease drops unread. + let fin = within( + &handle, + "the FIN", + kio::wait(|waiter| { + recv.poll_lease(&mut waiter.context(), PAYLOAD.len() + 1) + .map(|lease| lease.map(|lease| lease.is_fin())) + }), + ) + .await; + assert!(fin.expect("lease")); + assert!(recv.poll_closed(&kio::Waiter::noop()).is_pending()); + + let mut cx = Context::from_waker(Waker::noop()); + let Poll::Ready(lease) = recv.poll_lease(&mut cx, PAYLOAD.len()) else { + panic!("restored bytes are leasable"); + }; + let mut lease = lease.expect("lease"); + assert_eq!(lease.copy_to_bytes(PAYLOAD.len()), PAYLOAD); + lease.release(); + assert!(matches!(recv.poll_closed(&kio::Waiter::noop()), Poll::Ready(Ok(())))); + + // Tell the peer the stream was read through, so it can finish. + let send = kio::wait(|waiter| session.poll_open_uni(waiter, Default::default())) + .await + .expect("open uni"); + write_finish(send, b"read").await; + kio::wait(|waiter| session.poll_closed(waiter)).await; + }) + .expect("worker"); + client.join().expect("client thread"); +} + /// A whole moq-lite session over WebTransport: subprotocol negotiation, SETUP /// on the bidirectional stream, announce, subscribe, and a group on a /// unidirectional stream, with the tokio stack as the subscriber. This is the From 32e6224677bbd621bfbb24010a5f75a9feef3564 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 3 Nov 2026 22:52:26 -0800 Subject: [PATCH 16/17] test(quic): use the ambient clock in the stray datagram test Co-Authored-By: Claude Opus 5.5 --- rs/moq-tokio/src/quic/web/session.rs | 7 +++++-- rs/moq-uring/tests/integration/web.rs | 10 +++++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/rs/moq-tokio/src/quic/web/session.rs b/rs/moq-tokio/src/quic/web/session.rs index 4b6968fff51..f773e82b8f4 100644 --- a/rs/moq-tokio/src/quic/web/session.rs +++ b/rs/moq-tokio/src/quic/web/session.rs @@ -1355,7 +1355,10 @@ mod tests { } } for _ in 0..2 { - assert!(matches!(recv.closed().await, Err(crate::quic::web::recv::Error::Reset(7)))); + assert!(matches!( + recv.closed().await, + Err(crate::quic::web::recv::Error::Reset(7)) + )); } } } @@ -1403,7 +1406,7 @@ mod tests { client.connection().send_datagram(stray, Default::default()).unwrap(); } client.send_datagram(&b"ok"[..], Default::default()).unwrap(); - let datagram = tokio::time::timeout(std::time::Duration::from_secs(10), server.read_datagram()) + let datagram = kio::clock::timeout(Duration::from_secs(10), server.read_datagram()) .await .expect("no datagram arrived") .unwrap(); diff --git a/rs/moq-uring/tests/integration/web.rs b/rs/moq-uring/tests/integration/web.rs index a81aed48993..e88e62f0893 100644 --- a/rs/moq-uring/tests/integration/web.rs +++ b/rs/moq-uring/tests/integration/web.rs @@ -321,9 +321,13 @@ fn a_stray_datagram_is_dropped() { .respond(quic::web::proto::ConnectResponse::OK.with_protocol(PROTO)) .await .expect("respond"); - let datagram = within(&handle, "the datagram", kio::wait(|waiter| session.poll_recv_datagram(waiter))) - .await - .expect("a stray datagram does not end the session"); + let datagram = within( + &handle, + "the datagram", + kio::wait(|waiter| session.poll_recv_datagram(waiter)), + ) + .await + .expect("a stray datagram does not end the session"); assert_eq!(&datagram[..], PAYLOAD); session .send_datagram(datagram, Default::default()) From 01cf8b10faf44830d81f388715d96b1cbc744d57 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 3 Nov 2026 23:10:47 -0800 Subject: [PATCH 17/17] docs(quic): align io_uring's reset watch and reset-stop notes with the driver Co-Authored-By: Claude Opus 5.5 --- rs/moq-uring/src/quic/web/recv.rs | 2 +- rs/moq-uring/src/quic/web/send.rs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/rs/moq-uring/src/quic/web/recv.rs b/rs/moq-uring/src/quic/web/recv.rs index 3115c2deddd..03b20c80bf1 100644 --- a/rs/moq-uring/src/quic/web/recv.rs +++ b/rs/moq-uring/src/quic/web/recv.rs @@ -47,7 +47,7 @@ impl Stream { } /// Poll until the peer resets the stream, the receiver evicts it, or the session closes, - /// without reading it; pending for good once every byte arrived. See + /// without reading it; pending for good once every byte was read. See /// [`recv::Stream::received_reset`]. pub fn poll_reset(&mut self, cx: &mut Context<'_>) -> Poll { let close = &self.close; diff --git a/rs/moq-uring/src/quic/web/send.rs b/rs/moq-uring/src/quic/web/send.rs index f375696d3f7..2f5ae510d25 100644 --- a/rs/moq-uring/src/quic/web/send.rs +++ b/rs/moq-uring/src/quic/web/send.rs @@ -81,6 +81,8 @@ impl Stream { /// belongs to. fn reset_mut(&mut self, code: u32) { let code = self.close.encode_stream_code(code); + // The connection is driven on this thread, so no STOP_SENDING lands between this check + // and the reset: what the stream reports now is what the reset settles. let stopped = match &self.state { State::Open(stream) => stream.poll_closed(&kio::Waiter::noop()), State::Finished(stream) => stream.poll_closed(&kio::Waiter::noop()),