Skip to content

feat(net)!: transports report no stream credit - #8267

Open
kixelated wants to merge 4 commits into
mainfrom
quest/m1/stream-credit/no-credit-hook
Open

kixelated wants to merge 4 commits into
mainfrom
quest/m1/stream-credit/no-credit-hook

Conversation

@kixelated

@kixelated kixelated commented Nov 8, 2026 •

Copy link
Copy Markdown
Collaborator

Finishes quest/m1/stream-credit/no-credit-hook.md and deletes it.

Summary

  • moq_net::transport::StreamCredit, transport::Dir, and poll::Session::stream_credit are gone, along with every implementation: moq-tokio, moq-uring (QUIC and transport::local), qmux, and the memory transport. Dir lives on privately in transport/open.rs, where it picks a line.
  • moq-quic's Streams::credit and Streams::blocked went too. moq-dev/moq#7894 added them only to feed the hook. Their README entry and test assertions are removed.
  • The open line (rs/moq-net/src/transport/open.rs) treats a pending head as a wait for the peer's credit, the same way js/net does. make_room no longer asks the transport first, and Lines no longer holds a weak reference to the transport. The memory transport's blocked flag, which only fed the hook, is removed.
  • The other kind of wait was the WebTransport header. Now each transport removes that case where it starts. In moq-tokio and moq-uring, web::Session::poll_open_uni/poll_open_bi return as soon as QUIC grants the stream. The send stream holds the header it still has to write and writes it before any data: write, write_all, poll_write_buf, and tokio's AsyncWrite. When the header fits, new writes it right away, so the peer can accept the stream before its first data, same as before. Both web sessions drop their Opening bridge, which existed only to hold a half-written header across polls.

Choices made without asking

  • A stream reset or finished before its header fits goes out as a plain reset. This reverses one property from moq-dev/moq#5690. That PR made uring's open wait for the header's credit so that every reset carried the header. The quest's plan prescribes the opposite: open completes on credit, and the header is written after. Such a stream carried no data, and RFC 9114 §6.2 requires receivers to tolerate a unidirectional stream reset before its header. So the peer drops it unseen and loses nothing. A header that fits at open, which is the normal case, still goes out with reset_at as before (a_reset_stream_keeps_its_header is unchanged and passes). finish on a stream whose header still cannot be queued resets it and fails loudly with the new quic::web::send::Error::Unsent, from finish, poll_closed, and tokio's shutdown. It no longer reports a clean close the peer never saw. Holding the FIN until the header fits was rejected because it needs the same owed-header driver state. The old a_reset_stream_keeps_its_header_without_credit now asserts the new contract: the reset stream never reaches the session, and the next stream arrives intact. Rejected alternative: keep the header-on-reset guarantee by holding the reset until the header fits. That needs owed-header state polled by a driver task in both runtimes, which is the state feat(quic)!: reliable reset from a send-stream checkpoint #5690 removed, and it buys nothing for a stream that carried no data.
  • While the header is still unwritten, acknowledged() reports Some(false), so takeover never picks a stream that still owes bytes.
  • The open passes the caller's send::Options straight to QUIC. Before, it opened at priority i64::MAX and reprioritized after the header. The header now goes out at the stream's own priority, ahead of its data.

Tests

  • New an_open_waits_only_for_stream_credit in moq-tokio (raw QUIC and WebTransport) and moq-uring (uring server to tokio client). Once the peer's connection window is spent, a second poll_open_uni returns Ready right away, and both streams arrive intact once the peer reads. Against the old open path, the tokio test fails at the poll_open_uni assertion.
  • just check: 12788 tests passed, 149 skipped.

Performance

The open line's hot path is unchanged: an open that gets credit right away never reaches make_room or Line::start. A waiting open now skips a Weak::upgrade and a transport call. A WebTransport open now takes no bridge lock, and in the common case it writes the header once at open. No benchmark was run because no fan-out path changed.

Impact

Breaking for transport implementers (feat(net)!). Nothing on the wire changes.

  • moq_net::transport::{StreamCredit, Dir} and poll::Session::stream_credit are removed. Delete your stream_credit impl. Nothing replaces it: the session treats any pending open as waiting for the peer's credit, so poll_open_uni/poll_open_bi must pend only for stream credit. If you write a stream header, write it as the stream's first data instead of waiting on it inside the open.
  • moq_quic::Streams::{credit, blocked} are removed. They had no consumer besides the hook.
  • moq-tokio and moq-uring quic::web::Session::poll_open_* no longer wait to write the WebTransport header. A stream reset before its header fits arrives at the peer as a plain reset of a stream it cannot place, which it drops. Finishing one fails with the new quic::web::send::Error::Unsent variant.
  • None of this shipped (origin/release has no stream_credit). The release cut should drop the poll::Session::stream_credit note at rs/moq-net/MIGRATE.md line 151, which this PR leaves alone per the MIGRATE rule.

Suggested follow-ups

None.

Models

  • Writer: Claude Opus 5.5 (medium)
  • Reviewer: gpt-6.1-sol (high)

🤖 Generated with Claude Code

(Written by Claude Opus 5.5 with medium)

Review

Rewritten by quest review after each round.

  • Reviewer: Codex, gpt-6.1-sol, high effort
  • Session: 01a1ba33-2a6a-7b30-9ece-356cae838ee8, round 2
  • Head: d16adc7
  • Base: origin/main at 8577ed8
  • Score: 10/10, The incremental changes fix silent completion failures in both runtimes, with no additional findings from source review.
  • Findings: none
  • The last round's findings, at 1f0dfe6:
    • Fixed: medium, rs/moq-tokio/src/quic/web/send.rs:207: Finish pending WebTransport headers instead of resetting them; Both adapters now return and retain Error::Unsent when finishing with a blocked header (rs/moq-tokio/src/quic/web/send.rs:161,215 and rs/moq-uring/src/quic/web/send.rs:124,204), and Tokio shutdown propagates that error at rs/moq-tokio/src/quic/web/send.rs:286.

(Written by Claude Opus 5.5 with medium)

kixelated and others added 4 commits November 7, 2026 23:30
Delete transport::StreamCredit, transport::Dir, and the
poll::Session::stream_credit hook, with every implementation and
moq-quic's Streams::credit/blocked accessors that only fed it. The open
line now treats any pending head as a wait for the peer's credit.

To keep that true, moq-tokio's and moq-uring's WebTransport opens return
once QUIC grants the stream; the send stream writes the session header
ahead of its first data instead of the open waiting on it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the session

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resetting it and reporting a clean close hid that the peer never saw the
stream. finish, poll_closed, and shutdown now fail with Error::Unsent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated
kixelated marked this pull request as ready for review November 8, 2026 08:24
@coderabbitai

coderabbitai Bot commented Nov 8, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 1 minute.

Check out review usage here.

View limit details

Limit details: You’ve used all 4 included reviews currently available.

Learn how review limits work.

Review configuration:

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 6adcb705-e3dc-42e4-a850-da85df6b04e5
Commits

Reviewing files that changed from the base of the PR and between 2996eb9 and d16adc7.

Files selected for processing: 19

  • quest/m1/stream-credit/README.md
  • quest/m1/stream-credit/no-credit-hook.md
  • rs/moq-net/src/transport.rs
  • rs/moq-net/src/transport/boxed.rs
  • rs/moq-net/src/transport/memory.rs
  • rs/moq-net/src/transport/open.rs
  • rs/moq-quic/README.md
  • rs/moq-quic/src/connection/streams/mod.rs
  • rs/moq-quic/src/tests/mod.rs
  • rs/moq-tokio/src/quic/driver/connection.rs
  • rs/moq-tokio/src/quic/web/send.rs
  • rs/moq-tokio/src/quic/web/session.rs
  • rs/moq-uring/src/quic/driver/connection.rs
  • rs/moq-uring/src/quic/web.rs
  • rs/moq-uring/src/quic/web/send.rs
  • rs/moq-uring/src/transport/local.rs
  • rs/moq-uring/tests/integration/web.rs
  • rs/qmux/src/session.rs
  • rs/qmux/src/session/tests.rs
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@kixelated kixelated left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review by review (OpenAI)

Reviewed d16adc7. No actionable findings. Transport opens now return after stream credit is granted, with WebTransport headers written ahead of payload across both runtime adapters and Tokio AsyncWrite. Pending headers report unacknowledged, and finishing before header credit is available retains an Unsent failure rather than reporting a clean unseen stream. The removed credit hook and open-line inference match the chosen transport contract.

Verification: GitHub source/diff review of hook removal, open/preemption logic, header/write/reset/finish paths, and credit-exhaustion regressions. No builds, tests, browser interoperability, or benchmarks run.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ Minor suggestions only — the hook removal, the open-line inference, and the lazy WebTransport header all hold up under tracing.

Reviewed changes

Reviewed the full 19-file diff at d16adc7, including both runtime adapters' new header state machine and every in-tree poll::Session implementor.

  • Stream-credit hook removed — moq_net::transport::{StreamCredit, Dir} and poll::Session::stream_credit are gone, with the boxed forwarding, memory, moq-tokio, moq-uring, and qmux implementations; moq-quic::Streams::{credit, blocked} go too, while open_refused still drives STREAMS_BLOCKED.
  • Open line infers the wait — make_room no longer consults the transport and Lines drops its Weak, so a pending head of the line is a credit wait and every pending uni open may preempt a lower-ranked group stream.
  • WebTransport header written lazily — both web::send::Stream types hold unwritten/header/unsent, write the header at open when it fits, and otherwise ahead of the first data; the Opening bridges and the i64::MAX reprioritize step are gone.
  • Error::Unsent — finishing a stream whose header never fit resets it and fails finish, poll_closed, and tokio's shutdown, instead of reporting a clean close the peer never saw.
  • Tests — an_open_waits_only_for_stream_credit in both runtimes, and the uring reset-before-header test rewritten for the new contract.
  • Quest — the child quest is deleted and the epic's decision line records the replacement contract, with MIGRATE.md:151 left to the release cut as AGENTS.md requires.

ℹ️ The "an open pends only for credit" contract is missing from the trait it constrains

The open line now resets a lower-ranked group stream on nothing but a pending open, so a transport that pends for any other reason — the WebTransport header was the one in-tree case — makes the session preempt healthy streams. poll::Session::poll_open_uni and poll_open_bi (rs/moq-net/src/transport.rs:286, :294) still say only that they block "while there are too many concurrent streams"; the new requirement is stated in the PR's Impact but not where an implementer will read it.

Technical details
# State the pending-open invariant on the trait

## Affected sites
- `rs/moq-net/src/transport.rs:286` — `poll_open_uni` doc: "blocks while there are too
  many concurrent streams"; says nothing about other reasons to pend.
- `rs/moq-net/src/transport.rs:294` — the same for `poll_open_bi`.
- `rs/moq-net/src/transport/open.rs:214` — `make_room`, which now trusts the invariant
  unconditionally (no `StreamCredit::blocked` guard remains).

## Required outcome
- A transport implementer reading only the trait docs learns that a `Pending` from these
  two methods means the peer's stream credit, and that any per-stream preamble (a
  WebTransport header, framing, a handshake) must be written as the stream's first data
  instead of blocking the open.
- This is the only thing standing between a third-party adapter and spurious resets of
  healthy group streams, since `make_room` can no longer ask.

(Written by DeepSeek V4.1 Flash 1017)

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using deepseek-v4.1-flash-1017 (free via Pullfrog for OSS) | 𝕏

Comment on lines +216 to +220
if self.poll_header(&kio::Waiter::noop()).is_pending() {
self.reset_mut(0);
self.unsent = true;
return Err(Error::Unsent);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

poll_header can return Ready(Err(..)) — a peer STOP_SENDING, a session close, or ClosedStream — and is_pending() treats that as a written header, so finish still reports the clean close this PR set out to stop reporting. Match the result and return the header's error; the same shape is at rs/moq-uring/src/quic/web/send.rs:205.

Suggested change
if self.poll_header(&kio::Waiter::noop()).is_pending() {
self.reset_mut(0);
self.unsent = true;
return Err(Error::Unsent);
}
match self.poll_header(&kio::Waiter::noop()) {
Poll::Ready(Ok(())) => {}
Poll::Pending => {
self.reset_mut(0);
self.unsent = true;
return Err(Error::Unsent);
}
Poll::Ready(Err(err)) => return Err(err),
}

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant