Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion quest/m1/quic/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ MoQ never negotiates. One core serves the tokio backend, the thread-per-core
`moq-uring` backend, and qmux; iroh stays on upstream noq. BBR correctness,
reliable stream resets (#5690), hierarchical send groups (#5711, #5770), one
`Limits` type for relay peers (#5871), and endpoint sharding (#5688) are on
`main`. What remains is bounded incomplete-message memory; per-broadcast
`main`. What remains is bounded incomplete-message memory and clean exits
for connection-blocked writers; per-broadcast
fairness gives the crate-private send domains (#5872) their consumer in
[Broadcast fairness](/quest/m2/broadcast-fairness.md).
qmux already runs on the shared stream state machine, outside this epic.
Expand Down Expand Up @@ -71,6 +72,7 @@ cache limits remain separate; bitrate pacing composes with this memory cap.
## Required

- [Bound message assembly](/quest/m1/quic/receive-memory.md) - complete headers and frames return credit, and receiver priority cancellation restores progress
- [Connection-blocked writers end cleanly](/quest/m1/quic/connection-blocked.md) - an ended stream leaves the blocked-writer list, and STOP_SENDING surfaces under exhausted connection credit

## Related

Expand Down
42 changes: 42 additions & 0 deletions quest/m1/quic/connection-blocked.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# [S] Connection-blocked writers end cleanly

## Goal

In `moq-quic`, a send stream waiting on connection credit leaves the
blocked-writer list when it ends, and a write to a stopped stream returns
`Stopped` even while connection credit is exhausted. The fix is made once in the
shared stream engine, so tokio, io_uring, and qmux all get it.

Today a peer that grants streams but never MAX_DATA makes the relay keep an
entry for every stream it opens and cancels, with no bound. A writer parked on
`Blocked` also sleeps through a STOP_SENDING that has already arrived, until
unrelated credit shows up or the connection closes.

## Plan

Decided in the 2026-10-22 audit plan: one quest for both, because both bugs
sit on the same connection-blocked path in
`rs/moq-quic/src/connection/streams/`.

- Leak: `Streams::connection_blocked` is a `Vec` that `poll` drains only
once `write_limit() > 0`, and `stream_freed` leaves it alone. Tie
membership to stream lifetime by making it an insertion-ordered set (such as
an `IndexSet`) and removing the entry when a send stream is freed or reset.
Decided over deleting the list and scanning live streams when credit
returns: a removal stays O(1), and the set never holds more entries than
there are live streams.
- Hidden stop: `SendStream::write_source` returns `Blocked` on
`limit == 0` before `Send::write` can look at `stop_reason`. Check for
terminal stream state before connection-level flow control.
- Regression tests go in the existing `state.rs` unit tests. One test
writes to many streams under zero connection credit, each write returning
`Blocked`, then resets and frees those streams and asserts that the blocked
set is empty. The other sends STOP_SENDING on one stream while
another holds the connection window, and asserts that the write returns
`Stopped`.

Declined from the same audit: qmux's `closed()` resolving once the FIN is
serialized, before the batch reaches the transport. qmux has no ACKs, so `Ok`
only ever meant "handed off locally". A FIN already in the kernel's
TCP/WebSocket buffer is lost the same way if the transport dies, and record
order still keeps finish, closed, close correct.
Loading