Summary
Session::poll_send_datagram in rs/web-transport-wasm/src/session.rs may never terminate if the datagram writable is ever gracefully closed rather than errored. It would wake repeatedly across polls without making progress and without returning an error.
This is pre-existing and not introduced by #374 — see "Relationship to #374" below.
Mechanism
Per the Streams spec, a closed (not errored) writable fulfills ready and reports desiredSize == 0:
Against such a writer, each call to poll_send_datagram does:
poll_settled at the top drains the fulfilled ready future from the shared send_datagram slot and returns Ok(()).
- The loop reads
desiredSize, sees zero, and arms a fresh writer.ready().
- That
JsFuture returns Pending on its first poll, then fulfills on a microtask and wakes the task.
- Back to step 1, forever.
No single invocation spins — the loop terminates within a call because a freshly built JsFuture always polls Pending first. The repetition is across wakes, so it presents as a task that never completes and never errors while burning CPU.
Why the existing closed() guard never prevented this
Before #374 there was a fallback that waited on writer.closed() when ready had fulfilled but capacity was still zero. That arm was unreachable: ready and closed shared the single Op slot that poll_settled drains at the top of every call, so the loop always began with an empty slot, and reaching the arm required a fulfilled ready carried over from a previous call. It never ran, on any code path, which is why it never protected against this.
Note the naive repair makes things worse rather than better: giving ready and closed separate slots activates the arm, and then a clone that loses the capacity race on an open writer parks on closed() — which never fulfills — hanging the send for the life of the session. That is the deadlock pinned by concurrent_datagram_senders in rs/web-transport-wasm/examples/harness.rs.
Suggested fix
Observe writer.closed() through its own operation slot while continuing to resubscribe to the writer's current ready promise. Clean closure then becomes terminal without ever parking an open-writer race loser exclusively on closed(). Both harness cases must keep passing.
Testing gap
closed_datagram_writer_terminates currently closes the session, which errors the writable in Chromium, so ready rejects and the error exits through the existing error arm. That validates the errored path only. A case is needed that exercises a spec-compliant cleanly-closed datagram writable, rather than relying on one browser's current teardown behavior.
Open question worth settling first: whether a WebTransport datagram writable can reach a cleanly-closed state in practice at all, or whether every teardown path errors it. If the latter, this is latent rather than reachable, and the fix is hardening.
Relationship to #374
#374 removes the unreachable closed() fallback and replaces it with continue. It is behavior-neutral on every reachable path — the arm never executed before or after. The wake loop described here exists identically on main today. Found while adversarially reviewing that PR.
(written by Opus 5)
Summary
Session::poll_send_datagraminrs/web-transport-wasm/src/session.rsmay never terminate if the datagram writable is ever gracefully closed rather than errored. It would wake repeatedly across polls without making progress and without returning an error.This is pre-existing and not introduced by #374 — see "Relationship to #374" below.
Mechanism
Per the Streams spec, a closed (not errored) writable fulfills
readyand reportsdesiredSize == 0:Against such a writer, each call to
poll_send_datagramdoes:poll_settledat the top drains the fulfilledreadyfuture from the sharedsend_datagramslot and returnsOk(()).desiredSize, sees zero, and arms a freshwriter.ready().JsFuturereturnsPendingon its first poll, then fulfills on a microtask and wakes the task.No single invocation spins — the loop terminates within a call because a freshly built
JsFuturealways pollsPendingfirst. The repetition is across wakes, so it presents as a task that never completes and never errors while burning CPU.Why the existing
closed()guard never prevented thisBefore #374 there was a fallback that waited on
writer.closed()whenreadyhad fulfilled but capacity was still zero. That arm was unreachable:readyandclosedshared the singleOpslot thatpoll_settleddrains at the top of every call, so the loop always began with an empty slot, and reaching the arm required a fulfilledreadycarried over from a previous call. It never ran, on any code path, which is why it never protected against this.Note the naive repair makes things worse rather than better: giving
readyandclosedseparate slots activates the arm, and then a clone that loses the capacity race on an open writer parks onclosed()— which never fulfills — hanging the send for the life of the session. That is the deadlock pinned byconcurrent_datagram_sendersinrs/web-transport-wasm/examples/harness.rs.Suggested fix
Observe
writer.closed()through its own operation slot while continuing to resubscribe to the writer's currentreadypromise. Clean closure then becomes terminal without ever parking an open-writer race loser exclusively onclosed(). Both harness cases must keep passing.Testing gap
closed_datagram_writer_terminatescurrently closes the session, which errors the writable in Chromium, soreadyrejects and the error exits through the existing error arm. That validates the errored path only. A case is needed that exercises a spec-compliant cleanly-closed datagram writable, rather than relying on one browser's current teardown behavior.Open question worth settling first: whether a WebTransport datagram writable can reach a cleanly-closed state in practice at all, or whether every teardown path errors it. If the latter, this is latent rather than reachable, and the fix is hardening.
Relationship to #374
#374 removes the unreachable
closed()fallback and replaces it withcontinue. It is behavior-neutral on every reachable path — the arm never executed before or after. The wake loop described here exists identically onmaintoday. Found while adversarially reviewing that PR.(written by Opus 5)