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
1 change: 1 addition & 0 deletions quest/m2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ ranked just below deterministic TS legs, which it requires.
- [Per-stream deadlines](/quest/m2/quic-deadline.md) - hopeless retransmits
become resets, and a tail loss probe fires early while there is still time
- [Drop web-transport-trait](/quest/m2/drop-web-transport-trait.md) - every backend implements moq-net's `transport::poll` traits, and the generic `web_transport_trait` adapters are deleted
- [QUIC driver poll waits](/quest/m2/driver-poll.md) - the `moq_tokio::quic::driver` stream, datagram, close, and accept waits get leak-free `poll_*` forms, named like moq-uring's driver; `handshake_confirmed` and `Endpoint::wait_idle` stay async
- [Measure ECN on the backbone](/quest/m2/quic-ecn-measure.md) - a written
verdict on marking versus dropping, and whether Linode and OVH keep marks
- [poll_acked on moq-net's send stream](/quest/m2/quic-ack-hook.md) - the
Expand Down
92 changes: 92 additions & 0 deletions quest/m2/driver-poll.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# [M] The QUIC driver's waits poll without leaking

## Goal

The stream, datagram, close, and accept waits in `moq_tokio::quic::driver`
have `poll_*` forms that leak nothing: `Connection::poll_open_uni`, `poll_open_bi`, `poll_accept_uni`,
`poll_accept_bi`, `poll_recv_datagram` and `poll_closed`,
`SendStream::poll_closed` (today's `stopped`), and `Endpoint::poll_accept`.
`SendStream::poll_acked`, which [poll_acked](/quest/m2/quic-ack-hook.md)
adds, uses the same mechanism. The names follow moq-uring's driver, which
already follows moq-net's `transport::poll` traits; return types stay the
driver's own. A waiter that polls once and is dropped leaves nothing alive:
no live registration and no task.

## Plan

The driver's waits are futures over `tokio::sync::Notify`: the connection's
`Shared` fields, the per-stream `stopped` map, and the endpoint's `incoming`.
`Notify` cannot hold a bare waker, so a poll-based caller has to keep the
future, and moq-tokio's `transport::Session` adapter boxes one per pending
operation. [Drop web-transport-trait](/quest/m2/drop-web-transport-trait.md)
wants a native `poll::Session` on the QUIC backend, and
[poll_acked](/quest/m2/quic-ack-hook.md) wants a real poll method. Both need
these.

The trap is a stateless `&self` poll that pushes the caller's waker into a
shared list. Nothing removes the entry when its caller gives up, so it lives,
holding its task, until the next signal; for `poll_closed` that is the end of
the connection. A trial of that shape kept all 1000 abandoned `poll_closed`
waiters. Leak-free needs per-caller state that dies with the caller, as
tokio's `Notified` has, and as kio's `Park` and `WaiterList` give through
`Weak` entries.

Shape: poll forms take `&mut self, cx`, with a `kio::Park` in each handle. A
clone gets an empty park, so a registration belongs to the handle that made
it and dies with it. A handle that stops polling but stays alive keeps at most
its one registration until its next poll or the signal; that is the standard
poll contract, and it is bounded per handle. This is moq-uring's driver shape
and moq-net's `poll::Session` shape, so a native impl forwards in one line,
and kio stays private.

```rust
impl Connection {
pub fn poll_accept_uni(&mut self, cx: &mut Context<'_>) -> Poll<Result<RecvStream, ConnectionError>>;
pub fn poll_open_uni(&mut self, cx: &mut Context<'_>, options: SendOptions<'_>) -> Poll<Result<SendStream, ConnectionError>>;
pub fn poll_closed(&mut self, cx: &mut Context<'_>) -> Poll<ConnectionError>;
}
impl SendStream {
pub fn poll_closed(&mut self, cx: &mut Context<'_>) -> Poll<Result<Option<VarInt>, StoppedError>>;
}
```

Rejected shapes:

- **`&self, &kio::Waiter`**, the convention in `rs/AGENTS.md`. Leak-free,
since the caller owns the waiter, but kio enters the driver's public
signatures, so a kio break is a driver break, and every `cx`-based caller
(moq-net's traits) holds a `Park` anyway.
- **A caller-owned `Wait` token** (`poll_closed(&self, cx, &mut Wait)`),
slab-backed and released on drop: a second waiter primitive beside kio's,
with its own slab and tests to carry.
- **A stateless shared list**: the leak above.

Mechanism: swap the `Notify`s, and the `stopped` map's entries,
for `kio::WaiterList`s woken where the `Notify`s are signalled today, under
the connection lock, and make the existing async forms thin wrappers. They can
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
keep `&self` by owning their waiter (`kio::wait`), so no caller changes. Keep
the existing guarantees, such as draining accepted streams and datagrams
from a closed connection before its error. The other `Notify` waits
(`handshake_confirmed`, `Endpoint::wait_idle`) may move to the same lists,
but get no poll form until a caller needs one.

kio becomes a direct dependency of the `driver` feature. It is already in the
driver-only tree through `moq-quic`'s moq-clock, which #6132 accepted (its
decision 4), so no crate is added, and it stays out of the public API.

Builds on the `stopped` waiter map as #6196 left it, which already records a
local reset. It blocks the driver half of [poll_acked](/quest/m2/quic-ack-hook.md).

The acceptance test is the measured leak: 1000 cloned handles that each poll
once and are dropped unresolved leave no live registration behind, checked before the
condition is ever signalled, so release on drop is what passes it, not the
next wake. Also, on paused time: two handles waiting on each condition both
wake; one handle polled 100 times holds one live registration; a clone's poll
leaves the original's registration in place. Report the public API: new poll
methods on `Connection`, `SendStream` and `Endpoint`, with the async forms
unchanged.

## Related

- [Drop web-transport-trait](/quest/m2/drop-web-transport-trait.md) - its native QUIC `poll::Session` calls these
- [poll_acked on moq-net's send stream](/quest/m2/quic-ack-hook.md) - adds `SendStream::poll_acked` in this shape