fix(net): destroy socket on native close to prevent server.close() hang#28350
fix(net): destroy socket on native close to prevent server.close() hang#28350bilby91 wants to merge 4 commits into
Conversation
When the native Bun socket closes, the close handlers push null to the readable stream but never call destroy(). If the readable is paused or the writable was never ended, autoDestroy never fires, leaving the socket in a zombie state (_handle=null, destroyed=false). This causes server._connections to never decrement, so server.close() hangs forever. Call destroy() via process.nextTick in all three close handlers (SocketHandlers, SocketHandlers2, ServerHandlers) to match Node.js behavior where the native handle's close callback destroys the socket. Fixes oven-sh#13184, fixes oven-sh#19563, fixes oven-sh#23648 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (1)
WalkthroughSchedules a next-tick call to Changes
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. 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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@test/regression/issue/13184.test.ts`:
- Around line 31-45: The test is using fixed setTimeout delays (the
client.destroy() delay, the 500ms propagation delay, and the 5s hang detector)
which makes it flaky; replace these sleeps with event-driven waits: after
triggering client.destroy() await the client's 'close'/'end'/'destroy' event (or
the socket 'close' event) instead of waiting 100ms, replace the 500ms
propagation sleep with awaiting the server-side socket 'close' or connection
'end' event, and replace the 5s reject timeout around server.close() with
resolving/rejecting based on the server.close callback or the server's 'close'
event so server.close() completion is awaited directly (refer to
client.destroy(), server.close(), and the Promise wrappers in this test to find
where to swap timeouts for event listeners).
- Around line 31-34: Add a parallel test variant that exercises a graceful
client close (client.end()) instead of the current abrupt teardown
(client.destroy()): duplicate the existing teardown block that currently calls
client.destroy() inside the setTimeout and replace it with client.end(),
ensuring the test still resolves after the timeout so the server-side cleanup
path for a FIN (half-close) is exercised; update any assertions or awaits that
expect socket state transitions if necessary to account for the half-close
behavior (reference client.end(), client.destroy(), and the existing setTimeout
teardown).
- Around line 108-112: The test "socket that was never read gets destroyed on
native close" incorrectly attaches socket.on('data', ...) which puts the socket
into flowing mode; to properly exercise the "never read" (paused) scenario,
remove the socket.on('data', ...) handler (or explicitly call socket.pause())
inside the callback passed to testServerCloseCompletes so the socket remains
paused; update the code around the anonymous callback used in
testServerCloseCompletes to ensure no data listener is installed and the socket
stays in the paused readable state.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: ad3ac33b-56b9-4894-8437-6f0f72c5f2b0
📒 Files selected for processing (2)
src/js/node/net.tstest/regression/issue/13184.test.ts
Replace all setTimeout-based waits with event-driven awaits using Promise.withResolvers and socket close events. Add client.end() (FIN) test variants alongside existing client.destroy() (RST) tests. Fix "never read" test to not attach a data handler so socket stays paused. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@test/regression/issue/13184.test.ts`:
- Line 38: Replace the string repetition used in the test payload with a
Buffer-based allocation per project guidelines: in the call to client.write(...)
(the line currently using "hello world ".repeat(100)), build the repetitive
payload using Buffer.alloc(count, fill).toString() and pass that string to
client.write; ensure the same repeat count and fill content is used so behavior
is unchanged.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 57a6a0ed-1cce-4af0-a142-484bdacb572a
📒 Files selected for processing (1)
test/regression/issue/13184.test.ts
.repeat() is slow in debug JSC builds; use Buffer.alloc(count, fill) per project test guidelines. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@test/regression/issue/13184.test.ts`:
- Around line 37-42: The test races because client.write(...) is followed
immediately by client[teardown]() so server "data" handlers may not run; change
the test to await a server-side confirmation promise before teardown by
creating/waiting on a waitUntilReady (or waitForServerState) Promise that the
server resolves inside the relevant "data" handlers, then after await
clientConnected and client.write(...) do await waitUntilReady before calling
client[teardown]() and awaiting serverSocketClosed—use the existing symbols
client.write, client[teardown], clientConnected, serverSocketClosed and resolve
the promise from the server-side "data" handlers that establish the test state.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 8eae5341-f6b8-416d-bc7e-4cb2e50f696a
📒 Files selected for processing (1)
test/regression/issue/13184.test.ts
Add a `ready` callback to testServerCloseCompletes so that teardown waits for the server-side data handler to set up its state (pause, unpipe, etc.) before the client destroys/ends the connection. Without this, teardown could race ahead and the test would pass without actually exercising the intended socket state. Ref: oven-sh#28397 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Cross-linking #31654, which overlaps this PR's net-level fix (same three close handlers, also drops that |
Summary
SocketHandlers,SocketHandlers2,ServerHandlers) pushnullto the readable stream but never calldestroy(). If the readable is paused or the writable was never ended,autoDestroynever fires, leaving the socket in a zombie state (_handle=null,destroyed=false). This causesserver._connectionsto never decrement, soserver.close()hangs forever.process.nextTick(destroyNT, self)in all three close handlers to match Node.js behavior where the native handle's close callback destroys the socket directly.Fixes #13184, fixes #19563, fixes #23648
Test plan
test/regression/issue/13184.test.tswith 6 test cases:end()called and paused readable gets destroyeddestroyedflag istrueafter native closeUSE_SYSTEM_BUN=1(unpatched) and pass with debug buildtest/js/node/net/tests pass (155 pass, pre-existing failures unchanged)test/js/bun/http/serve.test.tstests pass (189 pass)🤖 Generated with Claude Code