Skip to content

fix(wasm): resubscribe to ready instead of waiting on closed - #374

Merged
kixelated merged 1 commit into
mainfrom
claude/wasm-datagram-resubscribe
Aug 13, 2026
Merged

fix(wasm): resubscribe to ready instead of waiting on closed#374
kixelated merged 1 commit into
mainfrom
claude/wasm-datagram-resubscribe

Conversation

@kixelated

Copy link
Copy Markdown
Collaborator

What

Session::poll_send_datagram had a fallback that waited on writer.closed() when ready had fulfilled but desiredSize still reported no capacity. This removes it and continues the loop instead, which resubscribes to the writer's current ready promise.

Why

Two separate problems, both fixed by the same one-line change.

It was unreachable. ready and closed share the same Op slot that poll_settled drains at the top of the call, so the loop always starts from an empty slot. A freshly built JsFuture polls Pending on its first poll -- wasm-bindgen attaches then callbacks that fire on a microtask -- so reaching that arm required a fulfilled ready carried over from an earlier call, which the drain already ruled out. This was verified empirically before removal: the branch was instrumented to set a JS global when entered, and it never fired across the full browser harness, including a hand-driven capacity race.

It was also wrong if revived. The natural next refactor -- giving ready and closed their own slots -- would have made it reachable, and then a clone that lost the capacity race would park on closed() against an open writer. That promise never fulfills, so the send hangs for the life of the session. That is exactly the deadlock concurrent_datagram_senders pins, whose doc comment already called it "a deadlock dressed up as backpressure."

So the arm was dead code that also modeled a fix that does not work. Removing it leaves the correct behavior the code already relied on: fall back to an empty slot, resubscribe to the current promise.

Where the closed-writer hazard goes

The arm was originally added to stop a closed writer from spinning on an already-fulfilled ready. That case does not need it: closing the session errors the datagram writable, so ready rejects, and the error leaves through the Poll::Ready(Err(_)) arm. The harness check closed datagram writer terminates poll distinguishes this from a spin -- it fails with "closed writer stayed in a wake loop" on timeout -- and still passes.

Testing

  • just harness (Chromium, real QUIC peer): 14 passed, 0 failed, including the two decisive checks -- closed datagram writer terminates poll ("closed writer returned an error") and concurrent cloned senders all get capacity ("the loser resubscribed and sent").
  • just check: passes.

No behavior change for any path that was actually reachable.

Reviewer notes

The reasoning here was challenged adversarially by Codex, which confirmed the unreachability analysis and caught that an earlier version of this change described the branch as becoming "load-bearing" if the slots were split -- it would in fact deadlock. That correction is why the arm is removed rather than documented.

(written by Opus 5)

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4268e037-d25a-4ae3-b484-8f353ae69ae2

📥 Commits

Reviewing files that changed from the base of the PR and between a13c7b8 and ceea4a0.

📒 Files selected for processing (1)
  • rs/web-transport-wasm/src/session.rs

Walkthrough

poll_send_datagram now retries after the writer readiness promise fulfills. It resubscribes to the writer’s current readiness promise when capacity remains unavailable. Readiness rejection continues to propagate as an error. The previous capacity recheck and session-closure wait handling were removed.

Mergeability Score: ⚪ Minimal · up to ceea4

The change makes the datagram sender resubscribe to the current readiness signal instead of waiting on an open writer to close, with the reported checks passing; no actionable merge-blocking risk remains beyond normal review.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description check ✅ Passed The description accurately explains the code change, its rationale, and the testing results.
Title check ✅ Passed The title clearly and concisely describes the main change: resubscribing to writer readiness instead of waiting for closure.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch claude/wasm-datagram-resubscribe

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

Comment @coderabbitai help to get the list of available commands.

A clone that loses the datagram capacity race holds a `ready` promise that
has already fulfilled and never will again, because the writer swaps in a
fresh one. Only the current promise can wake it.

The `closed` fallback it fell into was unreachable: `ready` and `closed`
shared the `Op` slot that `poll_settled` drains at the top of the call, so
the loop always started from an empty slot, and a freshly built `JsFuture`
polls `Pending` on its first poll. Reaching that arm needed a fulfilled
`ready` carried over from an earlier call, which the drain ruled out.

It was also wrong on its own terms. Had it become reachable -- by giving
the two waits their own slots, the obvious next refactor -- the losing
clone would have parked on `closed()` against an open writer, which never
fulfills, hanging the send for the life of the session. That is the
deadlock `concurrent_datagram_senders` pins.

Falling back to an empty slot is what resubscribes today, so drop the arm
and `continue`. A writer that closed rather than filled rejects `ready`,
so that case still leaves through the error arm.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@kixelated

Copy link
Copy Markdown
Collaborator Author

Adversarially reviewed by Codex against origin/main. It returned HOLD on one [high] finding: a gracefully closed datagram writable fulfills ready with desiredSize == 0 (per the Streams spec), which would wake poll_send_datagram indefinitely without erroring.

The finding is real but does not block this PR, and is tracked separately as #375.

Reasoning: the removed closed() arm was unreachable on main too — ready and closed shared the Op slot that poll_settled drains at the top of every call, so it never executed on any path. It was therefore never protecting against the wake loop. Codex concurs the diff "changes no previously reachable path". The hazard exists identically on main today, so removing dead code neither introduces nor worsens it.

Also worth recording: the obvious repair to that arm — giving ready and closed their own slots — would have made it reachable and then deadlocked an open writer, because a clone losing the capacity race would park on closed(), which never fulfills. That is what concurrent_datagram_senders pins, and it is why the arm is removed here rather than documented.

Verification: browser harness 14/14 (Chromium, real QUIC peer), including closed datagram writer terminates poll and concurrent cloned senders all get capacity; just check passes.

One caveat carried into #375: closed_datagram_writer_terminates closes the session, which errors the writable, so it validates the rejection path rather than spec-compliant clean closure. That coverage gap is genuine — it is just pre-existing.

(written by Opus 5)

@kixelated
kixelated enabled auto-merge (squash) August 13, 2026 00:04
@kixelated
kixelated merged commit 819c46a into main Aug 13, 2026
1 check passed
@kixelated
kixelated deleted the claude/wasm-datagram-resubscribe branch August 13, 2026 00:11
@moq-bot moq-bot Bot mentioned this pull request Aug 12, 2026
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