Skip to content

Fix #743's leak: a client that goes closes the runtime's connection whole - #794

Merged
futurepaul merged 1 commit into
masterfrom
claude/front-door-closes-runtime
Oct 31, 2026
Merged

futurepaul merged 1 commit into
masterfrom
claude/front-door-closes-runtime

Conversation

@futurepaul

Copy link
Copy Markdown
Collaborator

Fixes a leak #743 brought in.

What leaked: each connection a client closed while the user runtime kept its end open: a pass thread and two sockets, held for good, in the node process. workerd keeps a WebSocket connection open after its client closes, whether the client half-closes or sends a Close frame (checked straight against workerd; it closes at once after a plain answer, and after a stream's end). #743's front door relayed the client's close as a half-close of the runtime's connection, then waited on the runtime, which never closed. So every WebSocket a page closed leaked: the shell's own /api/fragments/watch on each page close or reload, and the chats' sockets.

How fast: in a repro of page reloads through the front door, each load closing two WebSockets (4 Chromes, about 12 loads a second), the node process held 28,207 threads and 56,413 fds after about ten minutes, and workerd 58,484 fds. From then on, connections were reset: module loads failed net::ERR_CONNECTION_RESET, so pages never started. An e2e shard leaks far fewer (one or two per page load), but every xtask dev session and e2e run has accumulated them since #743.

Fix: when the client's end is done (closed, or failed), the front door closes the runtime's connection whole (Shutdown::Both), as a proxy does once its client goes. The pass then ends and lets both sockets go. The trade-off, stated beside it: a client that half-closes and waits for its answer gets none. No client here does (no Shutdown::Write anywhere in our code outside front.rs).

Test: a_client_that_goes_ends_its_pass_though_the_runtime_stays. A runtime answers a 101, then keeps the connection open and silent after the client goes (as workerd does). The client takes the 101 and closes. The pass must end; its wait is bounded only so a failure reports instead of hanging.

  • Without the fix: fails (the pass never ends; the 10 s bound reports it).
  • With it: passes.

Also run: devstack's lib tests and clippy with -D warnings, and --test debug_terminal (a real node, started and reloaded through the front door).

🤖 Generated with Claude Code

…on whole

A client's end done, the front door only half-closed the runtime's. workerd
keeps a WebSocket connection whose client closed (half-closed, or with a
Close frame), so each such pass waited on the runtime for good: a thread
and two sockets per closed socket, the shell's own watch among them. A
repro of reloads, each closing two WebSockets, held 28,207 threads and
56,413 fds in ten minutes, and then connections were reset (module loads
failed ERR_CONNECTION_RESET). Now, the client gone, the runtime's
connection is closed whole, as a proxy does once its client goes.

Test: a client that goes, from a runtime that keeps a 101'd connection
open and silent after it: the pass ends (before: it never did).

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

@alexlwn123 alexlwn123 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Pass at a233fcd64fb4d0eee0cf3eb690b60c8f0baa4a0c.

The full runtime shutdown after client EOF or failure wakes the downstream copy and lets the pass finish even when workerd keeps its WebSocket open. The change is appropriately small, and the half-close tradeoff is explicit; I found no repository caller relying on that behavior and no blocking regression.

Validation: cargo test -p fragment-devstack --lib passed all 29 tests, including the new disconnect regression. GitHub check and all four e2e shards are still pending; full e2e was not run locally. The referenced external engineering-style file is unavailable on this machine.

@futurepaul
futurepaul added this pull request to the merge queue Oct 31, 2026
Merged via the queue into master with commit 43aa40f Oct 31, 2026
22 of 24 checks passed
futurepaul added a commit that referenced this pull request Oct 31, 2026
… into the front door's request log

Its pass keeps the log's framing; the runtime's end is closed whole once the client's is done, and #794's test runs through this pass, its log beside it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

2 participants