Skip to content

fix(qmux): return stopped-stream receive credit, qmux 0.5.4 and @moq/qmux 0.3.5 (backport) - #8808

Open
kixelated wants to merge 1 commit into
releasefrom
backport/qmux-credit
Open

kixelated wants to merge 1 commit into
releasefrom
backport/qmux-credit

Conversation

@kixelated

@kixelated kixelated commented Nov 15, 2026 •

Copy link
Copy Markdown
Collaborator

Backport for quest/m0/release-qmux-credit.md: the next release returns QMux receive credit. In the leaky versions a stream dropped or cancelled with unread data never returns that data's connection credit (MAX_DATA), so once about one 1 MiB connection window of it has piled up the sender can write on no stream of that connection, while the session stays up and logs nothing (moq-dev/web-transport#507). main is unaffected: its in-tree rs/qmux and js/qmux already credit stopped streams.

Changes

  • Cargo.toml, Cargo.lock: qmux 0.5.3 to 0.5.4 (checksum verified against the downloaded crate; dependency set unchanged).
  • js/net/package.json, bun.lock: @moq/net requires @moq/qmux ^0.3.5 (lockfile resolves 0.3.5).
  • js/net/package.json, bun.lock: @moq/net 0.4.7 to 0.4.8, so the requirement ships (see Choices).

Impact

  • Public API: none. qmux 0.5.4 only adds crate-private items (a diff of the src/ of 0.5.3 and 0.5.4 shows credit.rs and session.rs only).
  • Wire: none; credit frames (MAX_DATA) now go out where they were withheld.

Validation

Built from a copy of this head outside the checkout (so a nested .cargo/config.toml cannot merge in), --locked.

  • Publisher to relay, over WebSocket, through the real moq-relay stack: a publisher writes 4 x 128 KiB groups every 10 ms; 120 subscribers in turn join, read one frame mid-group, and leave, so the relay cancels its upstream subscription each time. With qmux 0.5.3 the next subscriber times out after 7 to 9 cancels (about 1 MiB of leaked credit). With 0.5.4 all 120 complete and the last still receives groups.
  • Relay to browser: a Bun viewer (@moq/net over @moq/qmux) stays up through 200 groups on 0.3.5 against the relay. It did not reproduce the stall on 0.3.3 through @moq/net: the leak needs data still in flight after the viewer cancels, and on loopback the viewer read each group eagerly, so @moq/net almost never cancelled a stream (9 stop-sending frames in 100 groups with the relay's max age forced to 20 ms, none with late data). The 0.3.3 leak is shown at the qmux layer instead: @moq/qmux 0.3.5's own cancelled receive streams return connection credit tests (late data after cancel, the reset gap, a cancelled stream's slot, and many cancelled streams leaving the session usable) pass on 0.3.5 and all 5 fail on 0.3.3's code.
  • Slots: @moq/qmux 0.3.5 holds a cancelled stream's slot until the peer's FIN or RESET_STREAM. The streams a relay opens toward a browser are group streams, which the relay finishes or resets once cancelled; the viewer's announce and subscribe streams are browser-opened and hold the relay's slots, not the browser's. By reading the code, not by a run, nothing the relay opens toward a browser sits idle.
  • @moq/net bun test src: 1291 pass, 0 fail. moq-relay smoke: 16 pass.
  • Not run here: just check, the Release gate's API diff (no public change expected), and the Windows and macOS lanes; CI covers them.

Release audit

Everything the next release ships since each package's last published version:

  • Rust: moq-relay 0.17.16 is the last tag, and rs/ differs from it only in moq-ffi (0.4.26, already released by moq-dev/moq#8594). The release-plz PR will propose patches for the crates that depend on qmux (moq-tokio, moq-relay, and their dependents).
  • JS: @moq/net 0.4.7 (npm 2026-10-28) predates two merged, unpublished backports: a480bba27 reads a 64-bit Hop ID as unknown (moq-dev/moq#8035) and f823d2ad5 decodes every IPv4 spelling in the GOAWAY local check (moq-dev/moq#8346). Other JS packages (@moq/publish 0.5.6, @moq/hang 0.5.4, @moq/watch 0.6.6) match npm.
  • qmux 0.5.3 to 0.5.4: the audit is in moq-dev/web-transport#543 (merged); release in moq-dev/web-transport#541. Its review found a RecvStream::discard race, fixed in 0.5.4; main's port is web-transport's quest/a0/qmux-discard-race.md.
  • @moq/qmux 0.3.3 to 0.3.5: moq-dev/web-transport#542 (release) and the fix in moq-dev/web-transport#516. My own diff of the two published packages also shows 0.3.5 closes the session (code 1002) on MAX_STREAM_DATA or STOP_SENDING naming a receive-only or unallocated local stream, and rejects MAX_STREAMS above 2^60. A moq relay sends neither, so no moq session is affected.
  • Finding for the maintainer: none that holds the release.

Choices made without asking

  • @moq/net is bumped to 0.4.8 here (recommended): the ^0.3.5 requirement and the two unpublished backports above only reach npm with a bump, and a branch-triggered publish runs on merge to release. Revert the two version lines and the release becomes the maintainer's later bump.
  • The viewer-side 0.3.3 reproduction uses @moq/qmux's own differential tests rather than an end-to-end viewer stall (see Validation).

Caused by

As old as QMux receive handling: moq-dev/web-transport#507, fixed in web-transport#543 and #516.

Review

Rewritten by quest review after each round.

  • Head: b834502

  • Base: origin/release at 24997fe

  • Risk: medium, the base has no RISK.md to name what is always high or customer-facing, so every change is medium

  • Classified: medium by default

  • Reviewer: Codex, gpt-6.1-sol, medium effort

  • Session: 01a1df2c-0562-7e43-8280-1dd51a137e02, round 1

  • Score: 9/10, The dependency requirements and lockfile updates are consistent with no actionable defects found, though builds and tests were not run as instructed.

  • Rounds: 2; first round 3 (medium 3); findings 0: fixed 0, refuted 0, open 0

  • Findings: none

  • Reviewer: Codex, gpt-6.1-sol, low effort, shadow

  • Session: 01a1df2c-0566-7831-a0b8-45c031c92699, round 1

  • Score: 9/10, Dependency and JavaScript package versions are consistent, with no actionable defects found by source inspection, though builds, tests, and upstream dependency behavior remain unverified.

  • Rounds: 1; first round 0; findings 0: fixed 0, refuted 0, open 0

  • Findings: none

Models

Size: S
Implementer: Claude Code, Sonnet 5.5, unknown effort (arm B)

🤖 Generated with Claude Code

(Written by Claude Sonnet 5.5)

Review context

Round 1 above is the first round against origin/release (quest review --base origin/release --new). The "first round 3" tally counts three findings from two earlier runs against origin/main, all in code this diff does not touch (audio ordering, IETF withdrawal, binding floors); --new dropped them unsettled. The validation and the @moq/net 0.4.8 choice stand as written above.

…qmux 0.3.5 (backport)

Moves qmux to 0.5.4 and requires @moq/qmux ^0.3.5. A stream dropped or
cancelled with unread data now returns that data's connection credit
(moq-dev/web-transport#507), so a session no longer stalls after about one
connection window of cancelled groups.

Bumps @moq/net to 0.4.8: it also carries the unpublished Hop ID and GOAWAY
backports merged since 0.4.7.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

@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.

✅ No new issues found.

Reviewed changes: the full diff of the single commit against release.

  • qmux 0.5.3 to 0.5.4: Cargo.toml and Cargo.lock move together, and the dependency set is unchanged.
  • @moq/qmux ^0.3.5: js/net/package.json and bun.lock agree, with the lockfile resolving 0.3.5.
  • @moq/net 0.4.7 to 0.4.8: the package.json and bun.lock versions match, so the new requirement ships.

Pullfrog  | View workflow run | Using claude-sonnet-5-5 | 𝕏

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