Skip to content
Merged
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
63 changes: 49 additions & 14 deletions quest/m2/quic-ack-hook.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,30 +38,65 @@ use. A default body cannot construct a backend's own `Self::Error`, so
unsupported has to live in the return type rather than the error, and a
consumer must treat `None` as unknown, never as delivered.

Implement it in the moq-tokio and moq-uring adapters over `moq-quic`'s
`SendStream::acked(offset)`, which returns whether every byte before the
offset is acknowledged and fails once it never will be. A `false` asks
`moq-quic` to report `StreamEvent::Acked { id, offset }` when the stream's
acknowledged prefix reaches it, so the adapter wakes waiters at or below that
offset; moq-uring's `sweep_stream` currently ignores the event. `acked` fails
once the stream is freed, after `StreamEvent::Finished`, which means
everything was acknowledged, so the adapter answers from that. The trait
method takes a byte offset, like `moq-quic` (decided 2026-10-17 on #5817);
the starvation quest's writer counts the bytes it writes. Leave the browser transport on the default; the browser's `WebTransportSendStream.getStats()` is
Each QUIC driver, moq-tokio's `quic::driver` and moq-uring's, gets the hook
first, in the shape of [the driver poll waits](/quest/m2/driver-poll.md),
with an async wrapper:

```rust
impl SendStream {
pub fn poll_acked(&mut self, cx: &mut Context<'_>, offset: u64) -> Poll<Result<(), AckedError>>;
pub async fn acked(&mut self, offset: u64) -> Result<(), AckedError>;
}
```

`AckedError` says why the bytes will never be acknowledged: a reset, the
peer's STOP_SENDING, the connection's loss, or an offset past the data
written. Both drivers sit on `moq-quic`'s `SendStream::acked(offset)`, whose
`false` asks for a `StreamEvent::Acked { id, offset }` once the acknowledged
prefix reaches the offset. Neither driver handles that event today
(moq-tokio's catch-all arm, moq-uring's `sweep_stream`), so waking the
stream's waiter on it is the core of the work. Three traps:

- Once a stream is freed, `acked` fails with `ClosedStream`, which cannot tell
a finished stream whose every byte was acknowledged from a reset one. The
driver answers from how the handle saw the stream end: finished, with no
reset or STOP_SENDING, means delivered. moq-uring's driver already records
that end; moq-tokio's handle tracks `finished` and, since #6196, the reset.
- `moq-quic`'s `acked` panics on an offset past the data written to an
unreset stream. The driver returns the error instead, since a panic under
the connection lock poisons it for every handle.
- The offset counts bytes on the QUIC stream. The trait method takes a byte
offset, like `moq-quic` (decided 2026-10-17 on #5817), and the starvation
quest's writer counts the bytes it writes, so neither driver needs a public
`offset()`. A layer that writes its own prefix adds its length: `quic::web`
already tracks its WebTransport header for `reset_at`.

The moq-tokio and moq-uring adapters implement the trait method over the
driver's. Leave the browser transport on the default; the browser's `WebTransportSendStream.getStats()` is
unimplemented in shipping Chrome and its `bytesAcknowledged` is at risk in the
W3C draft. Decided 2026-10-22 (maintainer): qmux's `poll_acked` reports
unsupported too, since writing into TCP is not delivery; over qmux the
histogram stays at group granularity.

Tests cover: offset already acknowledged before the first poll, an offset in
the middle of an in-flight frame, several waiters on one stream in offset
order, a waiter whose offset lies beyond the final size, reset by sender,
STOP_SENDING by the receiver, and session close.
Acceptance includes the first two traps, in each driver: a finished stream
that `moq-quic` has freed (so it answers `ClosedStream`) resolves `poll_acked`
as delivered, while a reset one resolves it with the error; and an offset past
the data written returns an error, not a panic, before and after finish.

Tests also cover: offset already acknowledged before the first poll, an
offset in the middle of an in-flight frame, several waiters on one stream in
offset order, reset by sender, STOP_SENDING by the receiver, and session
close.


Decided in the 2026-09-30 audit: moved to m2 with its only consumer,
[frame-granularity starvation](/quest/m2/starvation-frames.md).

## Required

- [QUIC driver poll waits](/quest/m2/driver-poll.md) - the leak-free poll
shape and waiter lists `poll_acked` joins

## Related

- [Starvation at frame granularity](/quest/m2/starvation-frames.md) - the
Expand Down