test(net): drop 100ms setTimeout race from the net.Server listen tests - #36310
Conversation
The 'net.createServer listen' describe block armed setTimeout(closeAndFail, 100) alongside server.listen() as a secondary deadline. Since #36175 reordered the serial phase, this file runs early in the alpine shard while the docker coordinator is still initializing mysql containers in the background; the scheduling contention let the 100ms timer beat the 'listening' event and the test failed with the uninformative 'function should not have been called'. The timer was never exercising listen() itself (Bun.listen binds synchronously and 'listening' is scheduled via setTimeout(...,1)); it was racing the test process's own scheduling latency. The test runner already imposes a per-test timeout, so remove the hand-rolled one and surface the real Error to done() when a listen genuinely fails. Same code paths, same assertions.
|
Warning Review limit reached
Next review available in: 13 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (1)
Comment |
|
Updated 1:06 AM PT - Jul 29th, 2026
❌ @robobun, your commit 6b920f8 has some failures in 🧪 To try this PR locally: bunx bun-pr 36310That installs a local version of the PR into your bun-36310 --bun |
There was a problem hiding this comment.
LGTM — test-only de-flake that removes a redundant 100ms secondary deadline; the assertions (mustCall on the listening callback, server.address() checks) are preserved and the error path now reports the real bind error instead of function should not have been called. One inline nit on the comment block, non-blocking.
Extended reasoning...
Overview
This PR touches a single test file, test/js/node/net/node-net-server.test.ts, removing the setTimeout(closeAndFail, 100) pattern from the nine tests in the net.createServer listen block. The per-test closeAndFail closure is replaced by a shared failOnError(server, done) helper that closes the server and calls done(err), and the now-dead clearTimeout calls inside the listening callbacks are removed. No runtime code is changed.
Security risks
None — test-only change with no new inputs, no new subprocess arguments, no network endpoints beyond the existing port: 0 / unix-socket binds already present.
Level of scrutiny
Low. This is a mechanical de-flake of a test harness pattern. I checked that:
- The property the removed timer protected ("listening fires") is still asserted via
mustCallon every listening callback. - The error path is strictly better:
done(err)surfaces the real bind failure instead of the genericmustNotCall()message, which REVIEW.md's "Never genericize a rich existing message" prefers. - If neither
'listening'nor'error'fires, the test-runner default timeout still bounds the test — the 100 ms timer was duplicating that bound with a value tight enough to race against scheduling latency under CI load, exactly thesetTimeout-as-deadline pattern REVIEW.md flags. - The
eventsblock's 500 ms timers (which guard real client↔server round-trips and haveis_doneguards) are intentionally left alone, andTimeris still referenced there so no dead type usage.
Other factors
The PR description carries concrete CI evidence (build numbers, timing log) and a local reproduction under synthetic load, and explicitly scopes out node-tls-server.test.ts (same pattern, not observed red). The one inline nit — the 7-line comment above failOnError is mostly incident narrative that REVIEW.md says belongs in the PR description — is stylistic and doesn't affect correctness, so I'm approving with it noted rather than blocking on it.
There was a problem hiding this comment.
LGTM — the comment trim in 7ffe330 addresses the earlier nit.
What was reviewed:
- Confirmed the removed 100ms timer only raced scheduling latency (bind is sync,
'listening'is next-tick) and the runner's per-test timeout still bounds a real hang. - Traced
createCallCheckCtx/mustCallin node-harness: dropping themustNotCalldestructure doesn't change the done-slot count, andfailOnError→done(err)still fails the test with a better message than the oldmustNotCall()(). - All
address()assertions andserver.close()cleanup on both success and error paths are preserved; the untouched 500msevents-block timers keep theiris_doneguards.
Extended reasoning...
Overview
Test-only de-flake of test/js/node/net/node-net-server.test.ts. Nine tests in the net.createServer listen block previously armed setTimeout(closeAndFail, 100) alongside server.listen(); this PR removes those timers and replaces the per-test closeAndFail boilerplate with a shared failOnError(server, done) helper that routes server.on('error', ...) to done(err). No runtime code, no assertion changes.
Security risks
None. Test file only; no production code paths, no new external inputs.
Level of scrutiny
Low. This is a mechanical de-flake following the exact pattern REVIEW.md and CLAUDE.md prescribe: don't use setTimeout to wait for a condition, rely on the test runner's per-test timeout, and wire failure events to reject/done with the real error. The PR description root-causes the flake (the 100ms timer measured scheduling contention on alpine while docker prestart ran, not listen() itself) and reproduces it locally under load.
I checked the REVIEW.md rule about not silently weakening a safety net: the property the old timer protected — "listen resolves to 'listening' or 'error' in bounded time" — is still enforced by (a) the runner's default timeout and (b) the 'error' listener now calling done(err). All mustCall wrappers on the listen callbacks and all expect() calls on server.address() are unchanged. Reading createCallCheckCtx in test/js/node/harness.ts confirmed that removing the unused mustNotCall destructure has no side effects (it only creates a done slot when called, not when destructured), and that mustCall's internal closeTimers() never touched the old local timeout variable anyway.
Other factors
The one prior finding (bug-history comment content) was addressed in 7ffe330 with exactly the suggested trim, and the thread is resolved. The events block's 500ms timers are deliberately left alone with a stated reason (real round-trips, is_done-guarded, not observed flaking). node-tls-server.test.ts has the same pattern and is called out in the description as intentionally out of scope. bun bd test passes 21/21 per the PR.
|
Diff is green: The remaining annotations on 84795 are unrelated to this change: Ready for a maintainer to merge. |
There was a problem hiding this comment.
LGTM — the comment-trim nit from the earlier pass is addressed, and the rest is a clean de-flake.
What was reviewed:
- Confirmed the removed 100ms timer was the only thing dropped; every
mustCallwrapper andserver.address()assertion is intact, so the property "listen fires the callback" is still enforced bymustCall+ the runner's per-test bound. failOnErrorroutes'error'→done(err), which is strictly better diagnostics than the oldmustNotCall()()and matches REVIEW.md's "wire failure events to reject" rule.- Checked that
TimerandmustNotCallare still used in the untouchedeventsblock, so no dead imports/destructures were left behind.
Extended reasoning...
Overview
Test-only change to test/js/node/net/node-net-server.test.ts. Nine tests in the net.createServer listen block previously armed setTimeout(closeAndFail, 100) alongside server.listen(); under CI load on the alpine lane the 100ms timer fired before 'listening' (which is scheduled via a 1ms setTimeout), producing a flake. The PR removes those timers, extracts a shared failOnError helper that routes server.on('error', ...) to done(err), and leaves the events block's 500ms round-trip timers untouched. Net: +25 / −92 lines, no runtime code changed.
Security risks
None. Pure test-file edit; no production code paths, no auth/crypto/network-facing surface touched.
Level of scrutiny
Low. This is a targeted de-flake that follows REVIEW.md's own testing guidance ("don't use setTimeout to wait for a condition"; "wire failure events to reject the awaited promise"; "when de-flaking, keep asserting the property the original assertion protected"). The removed timer was measuring test-process scheduling latency rather than listen() behavior, and the test runner's per-test bound already covers the hang case. Every existing assertion (address.address, address.port, address.family, server.listening) is preserved verbatim, and mustCall still enforces that the listen callback actually fires.
Other factors
- The one prior review comment (mine) asked for the CI-history portion of the new comment to be trimmed; commit 7ffe330 applied exactly the suggested wording and the thread is resolved.
- Error reporting is improved: a real bind failure now surfaces the actual error object via
done(err)instead of the opaque "function should not have been called". - Verified no dead code introduced — the
Timertype andmustNotCallare still consumed by the unchangedeventsdescribe block. - The PR description acknowledges
node-tls-server.test.tshas the same pattern and deliberately scopes to the file that was actually flaking, which is a reasonable boundary for a test-only change. - 21/21 tests pass on the debug build per the evidence block.
What does this PR do?
Fixes
test/js/node/net/node-net-server.test.ts > should listen on unix domain socketgoing red on the alpine 3.23 lanes since #36175 (seen on main builds 84293, 84503, 84549, 84601 and ~60 branch builds).Every test in the
net.createServer listenblock armedsetTimeout(closeAndFail, 100)next toserver.listen(). That timer was never testinglisten()itself:Bun.listenbinds synchronously and'listening'is scheduled viasetTimeout(emitListeningNextTick, 1, this), so the 100 ms race was against the test process's own scheduling latency. The runner already bounds each test, so the extra timer only added a flake surface (and hid the real error behindfunction should not have been called).#36175 didn't touch
netor this file, but it moved the allowlisted files into a single batch, so the handful of remaining serial files (this one is inexcludeFiles) now run much earlier in the shard. On alpine that lands while the docker-service coordinator is still bringing up the mysql containers in the background:(from build 84601, alpine 3.23 x64 shard
019fab6d-27b7-4c39)Change
Remove the 100 ms
setTimeout(closeAndFail, ...)from the nine listen tests and routeserver.on('error', ...)todone(err)so a real bind failure reports its actual error. Same assertions, same code paths (listen()→'listening'→server.address()checks); only the hand-rolled deadline that duplicated the test runner's timeout is gone. The 500 ms timers in theeventsblock are untouched; they guard real client↔server round trips, haveis_doneguards, and haven't flaked.How did you verify your code works?
bun bd test test/js/node/net/node-net-server.test.ts→ 21 pass / 0 fail.yes, 2 GBdd): with the old timer the listen block failed 1/5 runs atfunction should not have been called; with this change 5/5 runs pass under the same load (including a 246 ms'listening'that would have tripped the old 100 ms timer).node-tls-server.test.tshas the same 100 ms pattern and is also a serialexcludeFilesentry; happy to fold it in here if preferred, but it hasn't been observed red so I kept this scoped to the reported file.[stamp-90s] gate passed · iteration 1 · 1 files touched
passes on PR (with fix)
diff hotspot
gate history · 2 passed · 0 rejected · iteration 1
evidence per changed file