test(inspector): park the inspectee at a second debugger statement before it exits - #39698
test(inspector): park the inspectee at a second debugger statement before it exits#39698robobun wants to merge 1 commit into
Conversation
…fore it exits The fixture calls reportError(), so bun exits with code 1 as soon as the module body finishes. After Debugger.resume the inspectee could exit before the debugger thread wrote the resume response and the Debugger.resumed event to the socket, and the test failed with "WebSocket closed (1006) (inspectee exit: 1)" or "inspectee exited". A second debugger statement keeps the process paused until the test closes the connection.
|
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)
Included review availability: Your plan provides up to 5 included reviews per hour; 1 remains after this review. WalkthroughThe inspector protocol test replaces the interval-based keepalive with a second ChangesInspector lifecycle validation
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
There was a problem hiding this comment.
LGTM — well-diagnosed de-flake that parks the inspectee at a second debugger statement so the debugger thread can finish writing the resume messages before the process exits.
What was reviewed:
- Confirmed
pausedAgainis registered beforeDebugger.resumeis sent, so the secondDebugger.pausedcannot race past the waiter. - Checked that overwriting the
Debugger.pausedentry ineventWaitersis safe — the first waiter already resolved before the new one is set. - Verified the post-close
failWithfrom thecloselistener is harmless (swallowed byfailed.catch), same as before this change.
Extended reasoning...
Overview
This is a test-only change to test/cli/inspect/bun-inspector-protocol.test.ts, currently the flakiest test in CI (221/400 builds). The fixture's setInterval is replaced with a second debugger statement, and the test now awaits the second Debugger.paused event before closing the socket. Two explanatory comments are added. No production code is touched.
Security risks
None. This modifies only a test fixture and the test's await sequence.
Level of scrutiny
Low-to-moderate. It's a de-flake of a single test file with a thoroughly documented root cause: reportError() sets unhandled_error_counter, which makes is_event_loop_alive_excluding_immediates return false, so the inspectee exits immediately after the module body finishes regardless of setInterval. The inspector writes from a separate debugger thread, so under load the process can be gone before the Debugger.resume response and Debugger.resumed event reach the socket. Parking at a second debugger statement guarantees the process is alive until the test has received those messages. The PR author verified 0/480 failures under load after the change vs 123/480 before.
Other factors
- The
waitForEventmap is keyed by method name; I checked that registeringpausedAgainforDebugger.pausedcorrectly overwrites the (already-resolved) first waiter, and that bothresumedandpausedAgainare registered synchronously beforesend("Debugger.resume"), eliminating any registration race. - The removed
setIntervalwas demonstrably not keeping the process alive (per the root-cause analysis), so removing it is correct cleanup rather than a behavior change. - The finally block's
ws.close()triggers thecloselistener →failWith→fail(...), butfailed.catch(() => {})already swallows that and all try-block awaits have completed — unchanged from before. - The change follows the repo's de-flaking guidance: it awaits an actual observable condition (the second pause) rather than adding a sleep or raising a timeout, and the comment explains why no simpler signal exists.
Problem
test/cli/inspect/bun-inspector-protocol.test.tsis the test most often flagged as flaky in CI right now: it failed once and passed on retry in 221 of the last 400 builds, on every Linux lane, almost always in the parallel batch. The failure isWebSocket closed (1006) (inspectee exit: 1)orinspectee exited (inspectee exit: 1)with the fixture'serror: reportedin the inspectee's stderr.reportError(). Bun counts that as an unhandled error (VirtualMachine::uncaught_exception), andis_event_loop_alive_excluding_immediates(src/jsc/VirtualMachine.rs:1206) returns false onceunhandled_error_counteris set, so the inspectee exits with code 1 as soon as the module body finishes. ThesetIntervalin the fixture did not keep it alive.BunInspectorConnection::sendMessageToDebuggerThread,src/jsc/bindings/BunDebugger.cpp). AfterDebugger.resumethe inspectee runs to the end of the module and exits. Under load it is gone before the response toDebugger.resumeand theDebugger.resumedevent reach the socket. An instrumented copy of the test confirmed this: in 58 failures out of 300 runs under load, every one was waiting for one of those two messages.Fix
debuggerstatement. After the test resumes the first pause, the inspectee pauses again at once, so it is still alive while the debugger thread writes the resume messages. The test waits for that secondDebugger.pausedevent, then closes the socket. Closing the last connection resumes the inspectee, which then exits as before.setIntervalline is gone. It had no effect, see above.bun bd test test/cli/inspect/bun-inspector-protocol.test.ts. With the release build and 12 copies of the file running in a loop: 123 failures in 480 runs before this change, 0 in 480 after. The debug build passed 30 of 30 loaded runs.Background
--inspect-waitmakes bun start the script only after a client sendsInspector.initialized. The test enables every domain first, so thedebuggerstatements pause the script.BunInspectorConnection::runWhilePausedon the JS thread. It returns when a client resumes the script or when every connection is closed.LifecycleReporter.preventExitsets a flag that nothing reads. This change only removes the test's dependence on that, it does not change bun.[stamp-90s] gate passed · iteration 0 · 1 files touched
passes on PR (with fix)
diff hotspot
gate history · 1 passed · 0 rejected · iteration 0
evidence per changed file