Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
a62b26d
quest: claim quest/m1/quic/fork/switch
kixelated Oct 16, 2026
590d323
feat(moq-tokio): import quinn's async layer and web-transport-moq ver…
kixelated Oct 16, 2026
e8195bd
feat(quic)!: run moq-tokio and moq-uring on moq-quic
kixelated Oct 16, 2026
866b13d
fix(moq-tokio): log a CONNECT stream cut off by the close at debug
kixelated Oct 16, 2026
88a6fae
docs: point fork and advisory guidance at moq-quic
kixelated Oct 16, 2026
2d50267
docs: format the upgrade note
kixelated Oct 16, 2026
c2993ca
quest: finish quest/m1/quic/fork/switch
kixelated Oct 16, 2026
fcd8ea4
Merge origin/main into quest/m1/quic/fork/switch
kixelated Oct 16, 2026
5c7093f
claim quest/m1/quic/receive-credit
kixelated Oct 16, 2026
15d4076
feat(quic): hold connection credit until the receiver releases it
kixelated Oct 16, 2026
e111abf
test(moq-tokio): gate the timed helper with its only caller
kixelated Oct 16, 2026
a945ea1
Merge remote-tracking branch 'origin/quest/m1/quic/fork/switch' into …
kixelated Oct 16, 2026
5b7bff2
Merge origin/main into quest/m1/quic/fork/switch
kixelated Oct 16, 2026
43193f9
Merge remote-tracking branch 'origin/main' into quest-switch-alias
kixelated Oct 16, 2026
188b08c
fix(tokio): mark quic::web and quic::driver error enums non_exhaustive
kixelated Oct 16, 2026
d35e2cd
feat(quic): hand native receivers owned Credit handles
kixelated Oct 16, 2026
5d21b4f
fix(tokio): count in-flight accepts in wait_all_draining
kixelated Oct 16, 2026
50ebb4e
style(moq-tokio): apply quinn formatting to the held credit code
kixelated Oct 16, 2026
21cac2b
fix(tokio): wake draining waiters when accept empties the queue
kixelated Oct 16, 2026
2c3b97b
Merge remote-tracking branch 'origin/quest/m1/quic/fork/switch' into …
kixelated Oct 16, 2026
c1f4cfd
fix(tokio): surface non-WouldBlock receive errors, cap moq_quic logs
kixelated Oct 16, 2026
b389763
refactor(moq-uring): rename Connection::credit to credit_stats
kixelated Oct 16, 2026
c9b8403
quest: claim quest/m1/quic/receive-memory
kixelated Oct 16, 2026
420f102
Merge remote-tracking branch 'origin/quest/m1/quic/fork/switch' into …
kixelated Oct 16, 2026
f793080
Merge remote-tracking branch 'origin/quest/m1/quic/receive-credit' in…
kixelated Oct 16, 2026
738c16e
refactor(moq-uring): expose Connection::stats instead of credit_stats
kixelated Oct 16, 2026
888523e
Merge remote-tracking branch 'origin/main' into iter/receive-credit
kixelated Oct 16, 2026
c0f8705
wip(moq-net): carry receive credit through transport and Reader
kixelated Oct 16, 2026
a787581
Merge remote-tracking branch 'origin/quest/m1/quic/receive-credit' in…
kixelated Oct 16, 2026
0d565cf
wip: Unheld credit for the WebTransport adapters
kixelated Oct 16, 2026
ecd05da
wip(moq-net): track every lite and ietf reader for receive-credit rec…
kixelated Oct 16, 2026
8e780f4
test(moq-net): receive-credit recovery over a finite memory window
kixelated Oct 16, 2026
b2f62b3
feat(moq-quic): report when held credit is all the peer may send
kixelated Oct 16, 2026
8528176
feat(moq-tokio): hold receive credit through native QUIC
kixelated Oct 16, 2026
c31d16d
Merge remote-tracking branch 'origin/main' into iter/receive-credit
kixelated Oct 16, 2026
5ed1dd3
refactor(moq-tokio): fold held reads into read and read_chunk
kixelated Oct 16, 2026
78dfcfb
Merge remote-tracking branch 'origin/main' into iter/receive-credit
kixelated Oct 16, 2026
1d06361
style: rustfmt receive credit changes
kixelated Oct 16, 2026
cc58807
fix(moq-quic): bind Credit to the connection that handed it out
kixelated Oct 16, 2026
b857247
feat(moq-uring): hold receive credit through native QUIC
kixelated Oct 16, 2026
2a57949
Merge remote-tracking branch 'origin/quest/m1/quic/receive-credit' in…
kixelated Oct 16, 2026
c97deab
Merge branch 'quest/m1/quic/receive-credit' of github.com:moq-dev/moq…
kixelated Oct 16, 2026
33479f3
test(moq-uring): a partial frame gives way over the worker's QUIC path
kixelated Oct 16, 2026
e1bac99
fix(moq-tokio): keep the scripted session behind the websocket feature
kixelated Oct 16, 2026
071d21f
quest: split JavaScript sender and benchmark coverage out of receive-…
kixelated Oct 16, 2026
cd4bade
Merge branch 'main' of github.com:moq-dev/moq into quest/m1/quic/rece…
kixelated Oct 16, 2026
62d3fe8
quest: complete receive-memory
kixelated Oct 16, 2026
b0250ca
Merge branch 'main' of github.com:moq-dev/moq into quest/m1/quic/rece…
kixelated Oct 16, 2026
661b8bb
fix(moq-net): pick a pressure victim in linear time
kixelated Oct 16, 2026
a19b7c1
Merge branch 'main' of github.com:moq-dev/moq into quest/m1/quic/rece…
kixelated Oct 16, 2026
2d468d4
fix(moq-net): report a memory window fill only while it lasts
kixelated Oct 16, 2026
093c45b
fix(moq-quic): credit a reset stream's bytes once when it is then sto…
kixelated Oct 16, 2026
d6891e0
Merge main into quest/m1/quic/receive-memory
kixelated Oct 16, 2026
a1a4765
Merge main into quest/m1/quic/receive-memory
kixelated Oct 16, 2026
9f787af
Merge branch 'main' of github.com:moq-dev/moq into quest/m1/quic/rece…
kixelated Oct 16, 2026
ebb81d9
test: link the receive credit tests into each crate's integration binary
kixelated Oct 16, 2026
f229baa
fix(moq-net): evict a subgroup tail parked on its fill
kixelated Oct 16, 2026
78065cf
fix(moq-net): end an evicted data stream wherever its handler waits
kixelated Oct 16, 2026
9a25891
Merge the parallel fill-handoff eviction fix
kixelated Oct 16, 2026
baee41e
fix(moq-net): end evicted control streams wherever their handler waits
kixelated Oct 17, 2026
d53a229
Merge branch 'main' of github.com:moq-dev/moq into quest/m1/quic/rece…
kixelated Oct 17, 2026
26ae1e7
fix(moq-net): evict a stream without its reader
kixelated Oct 17, 2026
4af7531
style(moq-tokio): format the driver's stopper as quinn does
kixelated Oct 17, 2026
6fa775c
Merge branch 'main' of github.com:moq-dev/moq into quest/m1/quic/rece…
kixelated Oct 17, 2026
95af801
docs(moq-tokio): bound iroh's incomplete messages by its stream limit…
kixelated Oct 17, 2026
12c00ec
Merge branch 'main' of github.com:moq-dev/moq into quest/m1/quic/rece…
kixelated Oct 17, 2026
27d3b18
Merge branch 'main' of github.com:moq-dev/moq into quest/m1/quic/rece…
kixelated Oct 17, 2026
2f5be7e
fix(moq-net): free a parked reader's buffer with its evicted credit
kixelated Oct 17, 2026
98c7189
Merge branch 'main' of github.com:moq-dev/moq into quest/m1/quic/rece…
kixelated Oct 17, 2026
9c14af1
fix(moq-net): never return credit before its buffered bytes are freed
kixelated Oct 17, 2026
e1d4e86
Merge branch 'main' of github.com:moq-dev/moq into quest/m1/quic/rece…
kixelated Oct 17, 2026
b9ebaf2
fix(moq-net): keep a partial frame's credit until the frame is given up
kixelated Oct 17, 2026
b961141
fix(moq-net): wait out an evicted partial unit before cancelling another
kixelated Oct 17, 2026
01ab75d
fix(moq-tokio,moq-uring): wake a parked read when a Stopper stops it
kixelated Oct 17, 2026
8d22af6
Merge branch 'main' of github.com:moq-dev/moq into quest/m1/quic/rece…
kixelated Oct 17, 2026
4dcd59c
refactor(moq-net): give a partial frame's credit to the frame that ow…
kixelated Oct 17, 2026
0b0fcd7
fix(moq-uring): keep a stop from reading as the stream's end in poll_…
kixelated Oct 17, 2026
a5a71b5
Merge branch 'main' of github.com:moq-dev/moq into quest/m1/quic/rece…
kixelated Oct 17, 2026
76660fe
test: move this branch's tests onto moq-clock
kixelated Oct 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 13 additions & 7 deletions doc/bin/relay/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ idle_timeout = "10s" # Drop a connection after this long with no
keep_alive = "3s" # Ping interval; "0s" disables it. iroh still pings at least every 5s. Default.
gso = true # UDP segmentation offload. iroh cannot turn it off.
mtu_discovery = false # Path MTU discovery. Default.
receive_window = 67108864 # Whole-connection window, in bytes. Default (64 MiB).
stream_receive_window = 8388608 # Per-stream window. Omit for the backend default.
receive_window = 134217728 # Whole-connection window, in bytes. Default (128 MiB).
stream_receive_window = 8388608 # Per-stream window. Omit to follow receive_window.
send_window = 33554432 # Unacknowledged outgoing data. Omit for the backend default.
qlog = "/var/log/moq/qlog" # Existing directory. Needs the `qlog` build feature.
```
Expand All @@ -72,11 +72,17 @@ sessions, and so its [cluster routes](/bin/relay/cluster#failure-detection).
QUIC uses the smaller of the two endpoints' values, so this also bounds the
other end. Keep `keep_alive` under a third of it.

Raise the receive windows when a fat, long path idles below the link rate. The
64 MiB `receive_window` default carries several Gbps at a 100 ms RTT and bounds
how much unread data one peer can make the relay buffer. Keep
`stream_receive_window` well under it so one slow group cannot starve the
connection.
Raise `receive_window` when a fat, long path idles below the link rate. The
128 MiB default carries several Gbps at a 100 ms RTT and bounds how much unread
data and incomplete messages one peer can make the relay buffer. A message holds
its share until it is complete, so the window must fit the largest one, just over
64 MiB, and the relay refuses to start with less. When incomplete messages fill
it, the relay cancels the least urgent stream and carries on. iroh returns credit
as it reads, so there the window bounds only unread data, and incomplete messages
are bounded by `max_streams` streams of up to the largest message each.

Stream credit returns as data is read, so a `stream_receive_window` under
`receive_window` only adds round trips to a large frame.

## \[session] and \[peer]

Expand Down
2 changes: 1 addition & 1 deletion quest/m1/quic/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ cache limits remain separate; bitrate pacing composes with this memory cap.
## Required

- [Regression net](/quest/m1/quic/coverage/README.md) - benchmarks, fuzzing, characterization tests, and a seeded simulation, so later QUIC changes see every behavior or cost change
- [Bound message assembly](/quest/m1/quic/receive-memory.md) - complete headers and frames return credit, and receiver priority cancellation restores progress
- [Prove receive memory](/quest/m1/quic/receive-memory-coverage.md) - a JavaScript sender exhausts both native receivers and they recover; nightly measures the cost
- [A reset group keeps its header](/quest/m1/quic/group-reset.md) - a lite
publisher resets a group with `RESET_STREAM_AT`, so its subscriber drops the
tail grace
Expand Down
7 changes: 4 additions & 3 deletions quest/m1/quic/peer-limits.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,10 @@ its own.
- Refuse a `peer` value below the client default rather than silently
shrinking.

The [receive-memory plan](/quest/m1/quic/receive-memory.md) requires client
and peer memory allowances to fit the largest supported encoded unit,
including framing. Preserve that validation and retained-credit accounting
Message assembly holds connection credit until each unit is complete, so
client and peer receive windows must fit the largest supported encoded unit,
`moq_net::transport::MIN_RECEIVE_WINDOW`; the relay refuses a smaller
`[quic]` window at config load. Preserve that validation and retained-credit accounting
when applying runtime limits; wider stream counts must not create a second
unbounded assembly pool.

Expand Down
39 changes: 39 additions & 0 deletions quest/m1/quic/receive-memory-coverage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# [M] Prove receive memory with JavaScript senders and benchmarks

## Goal

A JavaScript sender drives both native receivers into receive-credit
exhaustion and they recover, and nightly measures what bounded receive memory
costs. This is coverage for the message-assembly work in #5735, with no new
transport behavior.

## Plan

Split out of #5735, which lands native credit holding, pressure recovery, and
the minimum receive window with Rust-only end-to-end tests over tokio and
io_uring.

A JavaScript sender over the stock receivers cannot stall them. The minimum
window fits the largest unit, and a strict-priority sender finishes one
stream before the next, so every frame completes. The test therefore needs a
receiver whose connection window is far below the minimum. Neither stock
config allows that, so the test assembles a WebTransport (HTTP/3) server
from each raw driver: tokio's `quic::driver` with `quic::web::Server`, and
an io_uring endpoint in web mode. Keep it test-only; do not add a public way
to configure a window below `MIN_RECEIVE_WINDOW`.

The sender is a Bun script using `@moq/web-transport`, pinned to the test
server's certificate hash with WebSocket disabled. It mirrors the Rust tests
in `rs/moq-tokio/tests/integration/receive_credit.rs`. A low-priority track sends a frame
larger than the window. A high-priority frame then has to complete, and a
later low-priority group has to arrive after the low stream is stopped. Run
it from Rust the way `rs/moq-tokio/tests/integration/max_age.rs` drives its Bun client,
and wire it into the interop workflow, since normal CI has no Bun.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

The benchmark sweeps concurrent streams and frame sizes, and compares a
finite stream window with one that follows the connection window. Report
retained bytes (held plus unread credit), throughput, and frame completion
latency. Sweep streams and frame sizes on separate axes, so a cost that grows
with the stream count shows up as a slope. Wire it into `sh/gh/nightly.sh`
next to `benchmark_held_reads`. Unit tests stay on mocked time; the benchmark
is the only wall-clock part.
110 changes: 0 additions & 110 deletions quest/m1/quic/receive-memory.md

This file was deleted.

5 changes: 2 additions & 3 deletions quest/m2/rate-quic.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ backpressure the peer cannot ignore, rather than metering and closing.
so a modified client cannot exceed the cap. An honest one goes
flow-control limited (its congestion controller does not back off) and
queues until [the grant](/quest/m2/rate-grant.md) clamps its encoder.
Keep rate pacing separate from the connection memory allowance in
[message assembly](/quest/m1/quic/receive-memory.md). The memory allowance
Keep rate pacing separate from the connection memory allowance that
message assembly holds credit against. The memory allowance
must fit the largest supported encoded unit even when it takes many rate
intervals to receive. Limit each grant by both rate allowance and available
memory backing; do not wait for a whole-message credit release to grant
Expand Down Expand Up @@ -90,4 +90,3 @@ that a one-second rate window can also be the complete assembly allowance.
## Required

- [Peer limits](/quest/m1/quic/peer-limits.md) - adds the runtime `set_limits(Limits)` seam and shrink-as-debt behavior this extends
- [Bound message assembly](/quest/m1/quic/receive-memory.md) - separate retained memory from paced outstanding credit
5 changes: 3 additions & 2 deletions quest/m2/unbounded-groups/segmented-frames.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ See the [epic](/quest/m2/unbounded-groups/README.md) for the decisions.
- Choose the segment size from benchmarks, and keep it a storage detail
rather than a public knob. Coalescing the chunks the transport delivers is
fine.
- Update [Receive memory](/quest/m1/quic/receive-memory.md) so a frame's
payload releases credit per segment. The frame header stays a whole unit.
- Change `moq-net`'s `coding::Reader`, which holds connection credit until a
whole frame is handed off (#5735), so a frame's payload releases credit per
segment. The frame header stays a whole unit.
- Measure `group.rs`, `frame.rs` (`frame_recv`), and `memory.rs` before and
after. Add a large-frame case swept over frame size.

Expand Down
36 changes: 29 additions & 7 deletions rs/moq-ffi/src/transport/adapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ impl<S: backend::Session<SendStream: 'static, RecvStream: 'static> + Clone + 'st
fn poll_closed(&mut self, cx: &mut Context<'_>) -> Poll<Self::Error> {
backend::Session::poll_closed(&mut self.0, cx).map(Error)
}
fn poll_credit_exhausted(&mut self, _: &mut Context<'_>) -> Poll<()> {
Poll::Pending
}
fn stats(&self) -> impl moq::Stats {
Stats(backend::Session::stats(&self.0))
}
Expand Down Expand Up @@ -141,20 +144,39 @@ impl<S: backend::SendStream + 'static> poll::SendStream for SendStream<S> {
}
/// An incoming WebTransport stream adapted to moq-net.
pub struct RecvStream<S>(S);
// The WebTransport backend returns connection credit as it reads, so there is none to hold.
impl<S: backend::RecvStream + 'static> poll::RecvStream for RecvStream<S> {
type Error = Error<S::Error>;
fn poll_read(&mut self, cx: &mut Context<'_>, dst: &mut [u8]) -> Poll<Result<Option<usize>, Self::Error>> {
backend::RecvStream::poll_read(&mut self.0, cx, dst).map_err(Error)
type Credit = moq::Unheld;
type Stopper = ();
fn stopper(&self) {}
fn poll_read(&mut self, cx: &mut Context<'_>, dst: &mut [u8]) -> Poll<Result<Option<moq::Unheld>, Self::Error>> {
backend::RecvStream::poll_read(&mut self.0, cx, dst)
.map_err(Error)
.map_ok(|n| n.map(|n| moq::Unheld(n as u64)))
}
fn poll_read_buf<B: BufMut>(
&mut self,
cx: &mut Context<'_>,
buf: &mut B,
) -> Poll<Result<Option<usize>, Self::Error>> {
backend::RecvStream::poll_read_buf(&mut self.0, cx, buf).map_err(Error)
) -> Poll<Result<Option<moq::Unheld>, Self::Error>> {
backend::RecvStream::poll_read_buf(&mut self.0, cx, buf)
.map_err(Error)
.map_ok(|n| n.map(|n| moq::Unheld(n as u64)))
}
fn poll_read_chunk(&mut self, cx: &mut Context<'_>, max: usize) -> Poll<Result<Option<Bytes>, Self::Error>> {
backend::RecvStream::poll_read_chunk(&mut self.0, cx, max).map_err(Error)
fn poll_read_chunk(
&mut self,
cx: &mut Context<'_>,
max: usize,
) -> Poll<Result<Option<(Bytes, moq::Unheld)>, Self::Error>> {
backend::RecvStream::poll_read_chunk(&mut self.0, cx, max)
.map_err(Error)
.map_ok(|chunk| {
chunk.map(|b| {
let n = b.len() as u64;
(b, moq::Unheld(n))
})
})
}
fn stop(&mut self, code: u32) {
backend::RecvStream::stop(&mut self.0, code)
Expand Down Expand Up @@ -309,7 +331,7 @@ mod tests {
let Poll::Ready(Ok(Some(chunk))) = poll::RecvStream::poll_read_chunk(&mut recv, &mut cx, 2) else {
panic!("read failed")
};
assert_eq!(chunk, b"hi"[..]);
assert_eq!(chunk.0, b"hi"[..]);
poll::RecvStream::stop(&mut recv, 13);
assert_eq!(state.get(), 13);
let Poll::Ready(Err(error)) = poll::SendStream::poll_closed(&mut send, &mut cx) else {
Expand Down
28 changes: 25 additions & 3 deletions rs/moq-net/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,10 @@ mod tests {
Poll::Ready(FakeError)
}

fn poll_credit_exhausted(&mut self, _cx: &mut std::task::Context<'_>) -> std::task::Poll<()> {
std::task::Poll::Pending
}

fn stats(&self) -> impl crate::transport::Stats {
FakeStats {
send_rate: *self.state.send_rate.lock().unwrap(),
Expand Down Expand Up @@ -717,8 +721,15 @@ mod tests {

impl crate::transport::poll::RecvStream for FakeRecvStream {
type Error = FakeError;
type Credit = crate::transport::Unheld;
type Stopper = ();
fn stopper(&self) {}

fn poll_read(&mut self, _cx: &mut Context<'_>, dst: &mut [u8]) -> Poll<Result<Option<usize>, Self::Error>> {
fn poll_read(
&mut self,
_cx: &mut Context<'_>,
dst: &mut [u8],
) -> Poll<Result<Option<Self::Credit>, Self::Error>> {
if self.data.is_empty() {
return Poll::Ready(Ok(None));
}
Expand All @@ -727,7 +738,7 @@ mod tests {
for slot in dst.iter_mut().take(size) {
*slot = self.data.pop_front().unwrap();
}
Poll::Ready(Ok(Some(size)))
Poll::Ready(Ok(Some(crate::transport::Unheld(size as u64))))
}

fn stop(&mut self, _code: u32) {}
Expand Down Expand Up @@ -1056,6 +1067,10 @@ mod tests {
self.inner.poll_closed(cx)
}

fn poll_credit_exhausted(&mut self, _cx: &mut std::task::Context<'_>) -> std::task::Poll<()> {
std::task::Poll::Pending
}

fn stats(&self) -> impl crate::transport::Stats {
self.inner.stats()
}
Expand Down Expand Up @@ -1092,8 +1107,15 @@ mod tests {

impl crate::transport::poll::RecvStream for LocalRecv {
type Error = FakeError;
type Credit = crate::transport::Unheld;
type Stopper = ();
fn stopper(&self) {}

fn poll_read(&mut self, cx: &mut Context<'_>, dst: &mut [u8]) -> Poll<Result<Option<usize>, Self::Error>> {
fn poll_read(
&mut self,
cx: &mut Context<'_>,
dst: &mut [u8],
) -> Poll<Result<Option<Self::Credit>, Self::Error>> {
self.inner.poll_read(cx, dst)
}

Expand Down
Loading
Loading