Repository navigation
fix(quic): a paused stream holds only its stream window - #8794
Conversation
Stream credit ran from the bytes a lease moved out of the assembler, not the bytes its reader took, so a reader that leased and then stopped taking kept granting the peer more, up to the connection window. moq-net's ingest pauses a stream exactly that way to pace a slow reader, assuming it holds at most its stream window, so a paced stream filled the connection window and the stuck window evicted it: a bulk transfer through a relay ended Evicted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
ℹ️ No critical issues — one gap worth a follow-up inline.
Reviewed changes
I reviewed the whole PR: moq-quic now grants stream credit from the bytes a reader takes, not the bytes it leases, and adds tests at the moq-quic and relay-sim levels.
- Stream credit from taken bytes:
Recv::max_stream_datanow starts frombytes_read - Leased::untaken(), and the newlentfield counts bytes that are out in aLease, so a stream that ingest stops reading holds back its stream credit. - Room for a waiting unit: a reader blocked on
lease(want)getsmax(stream_window, want), and theshortrule sends that credit even below the eighth-of-a-window threshold, which fixes the deadlock a few bytes past the limit. - Credit on hand-back:
put_back(restore/release) and theBlockedbranch oflease_innernow queueMAX_STREAM_DATAthrough the newqueue_max_stream_data. - Tests:
leases_hold_one_stream_windowreplaces the test that asserted the old behavior,a_waiting_reader_gets_room_for_its_unitcovers the below-threshold rule, anda_socket_session_carries_a_bulk_transfer_losslesslyreproduces the eviction through a relay over simulated QUIC.
I checked these and found no problem:
assembler.clear()keepsbytes_read, so the subtraction can't underflow after a stop or eviction.- An
arrived()that clearswantbefore the queued frame is written fixes itself when the reader leases again. - Plain
Chunksreads haveuntaken() == wanted() == 0, so their behavior is unchanged.
claude-opus-5-5 | 𝕏
|
No actionable comments were generated in the recent review. WalkthroughReceive-stream lease accounting now tracks bytes held in outstanding leases. Stream-credit calculations use bytes taken and the waiting reader’s requested lease size. Lease operations report when updated credit requires transmission. Unit tests cover lease consumption and requests larger than the stream window. A QUIC simulator test transfers framed data through a relay and checks for complete, uncorrupted frames. Merge Risk:
|
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
✅ No new issues found. The undercount I flagged last round is fixed in moq-net, where ingest knows what it holds.
Reviewed changes
Since the prior review, the new commits charge a paused stream for the part of an unsealed segment it has taken, and the docs now describe what a stopped reader holds.
- Unreleased segment bytes tracked:
coding::Reader::unreleasedcounts the bytes thatpoll_read_framerestores rather than releases, and resets to 0 when the segment seals. Inlining the lease borrows onlyself.stream, so the counter can update while the lease is live. - Larger pause charge:
Pause::pause(taken)flows throughboxed::RecvStream::pausetoShared::reserve. That reservesstream window + taken(capped at the connection window) against half the connection window. The amount is fixed at the first pause, which is correct becauseunreleasedonly changes afterresume(). - Tests:
a_pause_mid_segment_reserves_what_it_tookchecks that the reservation equals the held credit.paused_streams_hold_at_most_half_the_connection_windownow also covers a 25-byte segment hold that refuses a second pause. - Doc corrections: the
Leased,ReceiveWindow, moq-quic README,recv.rs, moq-tokiopoll_leaseand sim-test docs now say that a stopped reader holds at most its stream window of untaken bytes, plus what it took and has not released.
claude-opus-5-5 | 𝕏
|
quest land watch: the merge queue removed this pull request at 3e5aff9 because its The land fixer works on it in the background, and the watcher re-enqueues it at the back of the queue once it is fixed or judged flaky, or hands it back here with what it needs.
… method
|
164 - .fetch_update(Ordering::AcqRel, Ordering::Acquire, |paused| {
164 + .try_update(Ordering::AcqRel, Ordering::Acquire, |paused| {
|
warning: `moq-net` (lib) generated 7 warnings (run `cargo fix --lib -p moq-net` to apply 7 suggestions)
Finished `dev` profile [unoptimized] target(s) in 23.99s
mbx[cache]: object cache: 4 hits, 5 misses; 0 B downloaded, 0 B uploaded, 12.6 MiB stored locally
mbx[cache]: 27.41s session; 2.68s estimated compiler time avoided (summed across compilations)
mbx[cache]: Cargo artifact reuse and CI cache archive transfers are not included above
mbx[savings]: swept up 1.5 TiB logical so far. the broom is content-addressed.
##[endgroup]
##[group]test rs
sh/rs/select.sh test "$LIST"
rs: selecting hang moq-archive moq-audio moq-auth moq-bench moq-boy moq-c moq-cli moq-crypto moq-ffi moq-gst moq-hls moq-json moq-loc moq-mux moq-net moq-net-tests moq-quic moq-relay moq-rtmp moq-rtsp moq-srt moq-stats moq-tokio moq-transcode moq-uring moq-video moq-webrtc qmux
KIO_DENY_HOST_TIMERS=1 CARGO_BUILD_WARNINGS=deny RUSTC_WORKSPACE_WRAPPER='/nix/store/4klbl675argl2irhl89zrh2n14wilzx9-rust-minimal-1.98.1/bin/clippy-driver' bash sh/rs/nextest.sh cargo nextest run --locked --tests "$@"
Compiling kio v0.6.2 (/var/cache/github-runner-work/moq/moq/rs/kio)
Compiling bench-harness v0.0.1 (/var/cache/github-runner-work/moq/moq/rs/bench-harness)
Compiling moq-sock v0.2.0 (/var/cache/github-runner-work/moq/moq/rs/moq-sock)
Compiling moq-video v0.1.10 (/var/cache/github-runner-work/moq/moq/rs/moq-video)
Compiling moq-v4l v0.1.0 (/var/cache/github-runner-work/moq/moq/rs/moq-v4l)
Compiling moq-gst v0.4.12 (/var/cache/github-runner-work/moq/moq/rs/moq-gst)
Compiling moq-c v0.7.0 (/var/cache/github-runner-work/moq/moq/rs/moq-c)
Compiling moq-net v0.3.10 (/var/cache/github-runner-work/moq/moq/rs/moq-net)
Compiling moq-congestion v0.1.0 (/var/cache/github-runner-work/moq/moq/rs/moq-congestion)
Compiling moq-shaper v0.0.1 (/var/cache/github-runner-work/moq/moq/rs/moq-shaper)
error[E0560]: struct `model::group::Info` has no field named `sequence`
--> rs/moq-net/src/coding/reader.rs:1278:40
|
1278 | let mut group = crate::group::Info { sequence: 0 }.produce();
| ^^^^^^^^ `model::group::Info` does not have this field
|
= note: available fields are: `start`
error[E0308]: mismatched types
--> rs/moq-net/src/coding/reader.rs:1286:16
|
1286 | timestamp: Some(crate::Timestamp::ZERO),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `Timestamp`, found `Option<Timestamp>`
|
= note: expected struct `model::time::Timestamp`
found enum `std::option::Option<model::time::Timestamp>`
Compiling moq-crypto v0.0.11 (/var/cache/github-runner-work/moq/moq/rs/moq-crypto)
Compiling moq-json v0.5.8 (/var/cache/github-runner-work/moq/moq/rs/moq-json)
Compiling moq-loc v0.2.19 (/var/cache/github-runner-work/moq/moq/rs/moq-loc)
Compiling moq-archive v0.0.11 (/var/cache/github-runner-work/moq/moq/rs/moq-archive)
Compiling moq-quic v0.1.0 (/var/cache/github-runner-work/moq/moq/rs/moq-quic)
Compiling hang v0.21.11 (/var/cache/github-runner-work/moq/moq/rs/hang)
Compiling moq-auth v0.2.1 (/var/cache/github-runner-work/moq/moq/rs/moq-auth)
Compiling moq-mux v0.10.11 (/var/cache/github-runner-work/moq/moq/rs/moq-mux)
Compiling moq-stats v0.2.12 (/var/cache/github-runner-work/moq/moq/rs/moq-stats)
Compiling qmux v0.7.0 (/var/cache/github-runner-work/moq/moq/rs/qmux)
Compiling moq-uring v0.0.12 (/var/cache/github-runner-work/moq/moq/rs/moq-uring)
Compiling moq-tokio v0.19.23 (/var/cache/github-runner-work/moq/moq/rs/moq-tokio)
Compiling moq-audio v0.1.10 (/var/cache/github-runner-work/moq/moq/rs/moq-audio)
Some errors have detailed explanations: E0308, E0560.
For more information about an error, try `rustc --explain E0308`.
error: could not compile `moq-net` (lib test) due to 2 previous errors
warning: build failed, waiting for other jobs to finish...
Timing report saved to /var/cache/github-runner-work/moq/moq/target/cargo-timings/cargo-timing-20261115T102404772Z-efcac1b57bcf5093.html
error: command `/nix/store/85lxjwg78jw6rf7pqkf3vcq6291nrlfh-cargo-1.98.1-x86_64-unknown-linux-gnu/bin/cargo '--color=auto' test --no-run --message-format json-render-diagnostics --package 'path+file:///var/cache/github-runner-work/moq/moq/rs/hang#0.21.11' --package 'path+file:///var/cache/github-runner-work/moq/moq/rs/moq-archive#0.0.11' --package 'path+file:///var/cache/github-runner-work/moq/moq/rs/moq-audio#0.1.10' --package 'path+file:///var/cache/github-runner-work/moq/moq/rs/moq-auth#0.2.1' --package 'path+file:///var/cache/github-runner-work/moq/moq/rs/moq-bench#0.1.0' --package 'path+file:///var/cache/github-runner-work/moq/moq/rs/moq-boy#0.5.12' --package 'path+file:///var/cache/github-runner-work/moq/moq/rs/moq-c#0.7.0' --package 'path+file:///var/cache/github-runner-work/moq/moq/rs/moq-cli#0.14.2' --package 'path+file:///var/cache/github-runner-work/moq/moq/rs/moq-crypto#0.0.11' --package 'path+file:///var/cache/github-runner-work/moq/moq/rs/moq-ffi#0.4.12' --package 'path+file:///var/cache/github-runner-work/moq/moq/rs/moq-gst#0.4.12' --package 'path+file:///var/cache/github-runner-work/moq/moq/rs/moq-hls#0.5.11' --package 'path+file:///var/cache/github-runner-work/moq/moq/rs/moq-json#0.5.8' --package 'path+file:///var/cache/github-runner-work/moq/moq/rs/moq-loc#0.2.19' --package 'path+file:///var/cache/github-runner-work/moq/moq/rs/moq-mux#0.10.11' --package 'path+file:///var/cache/github-runner-work/moq/moq/rs/moq-net#0.3.10' --package 'path+file:///var/cache/github-runner-work/moq/moq/rs/moq-net-tests#0.0.0' --package 'path+file:///var/cache/github-runner-work/moq/moq/rs/moq-quic#0.1.0' --package 'path+file:///var/cache/github-runner-work/moq/moq/rs/moq-relay#0.17.2' --package 'path+file:///var/cache/github-runner-work/moq/moq/rs/moq-rtmp#0.3.11' --package 'path+file:///var/cache/github-runner-work/moq/moq/rs/moq-rtsp#0.0.1' --package 'path+file:///var/cache/github-runner-work/moq/moq/rs/moq-srt#0.3.11' --package 'path+file:///var/cache/github-runner-work/moq/moq/rs/moq-stats#0.2.12' --package 'path+file:///var/cache/github-runner-work/moq/moq/rs/moq-tokio#0.19.23' --package 'path+file:///var/cache/github-runner-work/moq/moq/rs/moq-transcode#0.1.10' --package 'path+file:///var/cache/github-runner-work/moq/moq/rs/moq-uring#0.0.12' --package 'path+file:///var/cache/github-runner-work/moq/moq/rs/moq-video#0.1.10' --package 'path+file:///var/cache/github-runner-work/moq/moq/rs/moq-webrtc#0.3.12' --package 'path+file:///var/cache/github-runner-work/moq/moq/rs/qmux#0.7.0' --package moq-video --package moq-audio --examples --tests --benches --features 'moq-video/capture moq-audio/capture moq-audio/playback moq-cli/play moq-cli/openh264 moq-relay/io-uring moq-audio/bus moq-auth/fuzz moq-boy/openh264 moq-crypto/e2ee moq-crypto/sign moq-hls/server moq-json/bench moq-mux/fuzz moq-net/async moq-net/fuzz moq-quic/fuzz moq-relay/test-support moq-rtmp/fuzz moq-stats/bench moq-tokio/driver moq-tokio/net moq-uring/_probe moq-uring/fuzz moq-uring/quic moq-video/openh264 qmux/fuzz qmux/tcp qmux/tls qmux/ws qmux/wss' --timings --locked` exited with code 101
mbx[cache]: object cache: 0 hits, 31 misses, 3 bypassed; 0 B downloaded, 0 B uploaded, 340.9 MiB stored locally
mbx[cache]: 44.83s session; 0s estimated compiler time avoided (summed across compilations)
mbx[cache]: bypass reasons: 3 unsupported-crate-type
mbx[cache]: Cargo artifact reuse and CI cache archive transfers are not included above
mbx[savings]: 89824 compilations served. ask rustc how many it remembers doing.
error: recipe `test` failed on line 384 with exit code 101
error: recipe `test-changed` failed on line 104 with exit code 101
##[endgroup]
##[error]ci: test rs failed
error: recipe `ci` failed on line 120 with exit code 101
##[error]Process completed with exit code 101.
##[group]Run actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9
with:
name: cargo-timings
path: target/cargo-timings/cargo-timing-*.html
if-no-files-found: ignore
retention-days: 14
compression-level: 6
overwrite: false
include-hidden-files: false
archive: true
env:
BASE: 11551598ec4cd39be9c44374439d4a298bf30b90
CHECK_FAIL_FAST: 1
NEXTEST_TEST_THREADS: 24
MBX_CACHE_LINKS: 1
MBX_REMOTE_URL:
##[endgroup]
With the provided path, there will be 3 files uploaded
Artifact name is valid!
Root directory input is valid!
Uploading artifact: cargo-timings.zip
Beginning upload of artifact content to blob storage
Uploaded bytes 165660
Finished uploading artifact content to blob storage!
SHA256 digest of uploaded artifact is 9c9918522b9dfbcf9d4102ed03fd9ab5c76317f3dd3dcd712372ba8b69b1cbd6
Finalizing artifact upload
Artifact cargo-timings successfully finalized. Artifact ID 14157706196
Artifact cargo-timings has been successfully uploaded! Final size is 165660 bytes. Artifact ID is 14157706196
Artifact download URL: https://github.com/moq-dev/moq/actions/runs/43703542479/artifacts/14157706196
Post job cleanup.
##[start-action display=Run jdx/mr-boxington-action@88acf2bb50552c7c508cbac13e5f8c576b95b1de;id=__384d5d8a-462c-487a-ba8e-346e39654382.mbx]
##[end-action id=__384d5d8a-462c-487a-ba8e-346e39654382.mbx;outcome=skipped;conclusion=skipped;duration_ms=0]
Post job cleanup.
[command]/nix/store/i2m3w4ly2w6grrmk1j59rb7zp09bssia-git-2.56.0/bin/git version
git version 2.56.0
Temporarily overriding HOME='/var/cache/github-runner-work/_temp/13407f9d-9287-4f0e-8b11-ab9664c86b79' before making global git config changes
Adding repository directory to the temporary git global config as a safe directory
[command]/nix/store/i2m3w4ly2w6grrmk1j59rb7zp09bssia-git-2.56.0/bin/git config --global --add safe.directory /var/cache/github-runner-work/moq/moq
Removing SSH command configuration
[command]/nix/store/i2m3w4ly2w6grrmk1j59rb7zp09bssia-git-2.56.0/bin/git config --local --name-only --get-regexp core\.sshCommand
[command]/nix/store/i2m3w4ly2w6grrmk1j59rb7zp09bssia-git-2.56.0/bin/git submodule foreach --recursive sh -c "git config --local --name-only --get-regexp 'core\.sshCommand' && git config --local --unset-all 'core.sshCommand' || :"
Removing HTTP extra header
[command]/nix/store/i2m3w4ly2w6grrmk1j59rb7zp09bssia-git-2.56.0/bin/git config --local --name-only --get-regexp http\.https\:\/\/github\.com\/\.extraheader
[command]/nix/store/i2m3w4ly2w6grrmk1j59rb7zp09bssia-git-2.56.0/bin/git submodule foreach --recursive sh -c "git config --local --name-only --get-regexp 'http\.https\:\/\/github\.com\/\.extraheader' && git config --local --unset-all 'http.https://github.com/.extraheader' || :"
Removing includeIf entries pointing to credentials config files
[command]/nix/store/i2m3w4ly2w6grrmk1j59rb7zp09bssia-git-2.56.0/bin/git config --local --name-only --get-regexp ^includeIf\.gitdir:
[command]/nix/store/i2m3w4ly2w6grrmk1j59rb7zp09bssia-git-2.56.0/bin/git submodule foreach --recursive git config --local --show-origin --name-only --get-regexp remote.origin.url
A job completed hook has been configured by the self-hosted runner administrator
##[group]Run '/nix/store/qrn6ds64bzvwbm5nwf245ifz2c6jb9xw-ci-cache-gc.sh/bin/ci-cache-gc.sh'
shell: /nix/store/lfm00fnq66xfgbn099vqckabyl5jllz6-bash-interactive-5.3p20/bin/bash --noprofile --norc -e -o pipefail {0}
##[endgroup]
evicted 123 objects and 0 action results (409.2 MiB logical); 50.0 GiB logical remain
dropped 1 stale checkout records
dropped 5 session event streams
removed 0 learned incremental units and 3 checkout directories (3.8 KiB logical); 13 units (2.4 GiB logical) remain
evicted 0 objects and 0 action results (0 B logical); 50.0 GiB logical remain
removed 2 target directories (2.1 MiB logical, 2 abandoned and 0 live); 14.1 GiB logical remain
Cleaning up orphan processes
(written by quest land watch) |
|
codex, gpt-6.1-sol, medium effort land fixer note:
(written by codex, gpt-6.1-sol, medium effort) |
|
quest land watch: armed auto-merge on this pull request at 2d68e88, so it joins the back of the merge queue once its required checks pass, with the land fixer's fix ( (written by quest land watch) |
There was a problem hiding this comment.
✅ No new issues found. The new commits only move the tests to
main's updated APIs. The fix itself is unchanged.
Reviewed changes
Since the prior review, the branch merged main and moved two tests to its current model APIs.
- Receive-credit test migrated:
a_pause_mid_segment_reserves_what_it_tooknow usesgroup::Info { start: 0 }and the non-optionalframe::Info::timestamp. - Bulk-transfer sim test migrated:
a_socket_session_carries_a_bulk_transfer_losslesslynow callsappend_group(0)and writes withTimestamp::unscaled(0). It ends withgroup.close(), which ends the group without a known end and still delivers every frame written. These calls matchmain's definitions inmodel/track.rs,model/time.rsandmodel/group.rs.
claude-opus-5-5 | 𝕏

Problem
A bulk transfer through a relay over QUIC or WebTransport could end
Evictedpartway, even with a writer awaitinggroup::Producer::readyand a single reader that keeps reading (found validatingmoq tcpin #8741, since removed).moq-net's ingest paces a group by its fastest reader: when the reader falls behind, ingest stops reading the stream, so flow control pushes back to the publisher. It charges a paused stream its stream window (1 MiB by default) against half the connection window (4 MiB). moq-quic broke that assumption. It granted
MAX_STREAM_DATAfrom the bytes a lease moved out of the receive buffer, not the bytes the reader took, and every lease moves everything that has arrived. So a stream that ingest stopped reading kept granting credit until it held the whole connection window. Then the stuck-window pass evicted it. The trace: the paced stream held 3.9 MB of lease plus 0.3 MB unread, against a 4 MiB connection window.Approach
In moq-quic, stream credit now starts from what the reader took:
assembler.bytes_read()minus the leased bytes it has not taken yet (Leased::untaken, which includes bytes out in aLease). Handing a lease back queuesMAX_STREAM_DATAfor what was taken. A reader that stops taking now holds its stream window and no more, which is whatReceiveWindowand moq-tokio'sDEFAULT_STREAM_RECEIVE_WINDOWdocs already promised.A reader waiting for a unit larger than the stream window (
lease(want)) gets credit for the whole unit, which the connection window still bounds. The credit is sent even when the increase is below the usual eighth-of-a-window threshold, because nothing the reader takes while it waits would add to it. Without that, a reader waiting on a unit a few bytes past its credit deadlocks.moq-net's charge also misses one more hold.
poll_read_framekeeps the credit of the bytes it took into a frame's unsealed segment (up to 1 MiB) until the segment seals, and a stream paused mid-segment keeps that credit beside its stream window.coding::Readernow counts those bytes, andPause::pause(taken)reservesstream window + taken(Pullfrog's finding).The repro is a moq-relay sim test over simulated QUIC. A
moq-lite-07-wipsocket session sends 40 MB in 8 KiB frames, with the writer awaitingready, and the reader drains at about 64 MB/s. Before the fix it endsEvictedafter 320 KiB.Impact
Validation
quic_sim::a_socket_session_carries_a_bulk_transfer_losslessly: fails onmainwithafter 327680 bytes: evicted, and passes with the fix (2.5 s). I also ran it locally with 5% loss, duplication and reordering on both client paths, and it passed (not committed, since it takes 18 s).StreamsState::evictonmainto find the cause:wmd=5555014 recvd=5555014 held=3895495 rw=4194304 unread=298809, all on the paced stream.moq-quictests:leases_hold_one_stream_windowreplaceslease_spends_connection_window_only, which asserted the old behavior. It checks that leasing without taking stops at one stream window, and that taking returns stream credit but keeps connection credit until release.a_waiting_reader_gets_room_for_its_unitfails without the below-threshold rule.coding::reader::a_pause_mid_segment_reserves_what_it_took, plus atransport::boxedcase for the larger charge.just checkpasses: 12849 tests on the current head (8061 before the segment fix). One earlier run hit an unrelated flake:moq-rtmp server::tests::play_multitrack_to_capable_clientfired a host-clock timer under load. It passes alone.moq tcpwas removed. The sim test runs moq-relay's realConnectionover real moq-quic.Alternatives
Follow-ups
paused-creditquest, alongside the paused-credit cap.moq-rtmphost-timer flake above.Caused by
Models
Size: M
Implementer: Claude Code, claude-opus-5-5, unknown effort (arm A)
Implementer: Claude Code, claude-opus-5-5, unknown effort (iterate, M, arm A)
(Written by Opus 5.5)
🤖 Generated with Claude Code
Review
mechanical fix by codex, gpt-6.1-sol, medium effort:
Merged base and mechanically migrated new tests to its group/start and timestamp APIs; ran nix develop --command just ci base (12832 tests passed) and git diff --check base.(head 2d68e88)Rewritten by
quest reviewafter each round.Head: 3e5aff9
Base: origin/main at 77b8ec7
Risk: medium,
the base has no RISK.md to name what is always high or customer-facing, so every change is mediumClassified: medium by default
Reviewer: Codex, gpt-6.1-sol, high effort
Session:
01a1de3b-898d-71c2-b3b9-c2921f26ae89, round 1Score: 9/10,
Receive-credit accounting appears consistent across leasing and pacing, with no actionable defects found; builds and tests were not run, as instructed.Rounds: 1; first round 0; findings 0: fixed 0, refuted 0, open 0
Findings: none
Reviewer: Codex, gpt-6.1-sol, medium effort, shadow
Session:
01a1de3b-8988-7b01-ba53-d43247d8d25a, round 1Score: 9/10,
Source review found no actionable defects and supports landing, but builds and tests were not run as instructed.Rounds: 1; first round 0; findings 0: fixed 0, refuted 0, open 0
Findings: none