Skip to content

test(socket.io): drop 200ms guard timers in connection-state-recovery#35132

Merged
Jarred-Sumner merged 1 commit into
mainfrom
farm/8560f1fc/socket-io-recovery-deflake
Jul 22, 2026
Merged

test(socket.io): drop 200ms guard timers in connection-state-recovery#35132
Jarred-Sumner merged 1 commit into
mainfrom
farm/8560f1fc/socket-io-recovery-deflake

Conversation

@robobun

@robobun robobun commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Fixes test/js/third_party/socket.io/socket.io-connection-state-recovery.test.ts going red on the debian-13 x64-asan lane.

Failure

error: timeout
      at <anonymous> (test/js/third_party/socket.io/socket.io-connection-state-recovery.test.ts:46:26)
✗ connection state recovery > should restore session and missed packets [219.96ms]

Seen on builds 77055 (219.84ms) and 77789 (219.96ms), both debian-13 x64-asan.

Cause

Each test in this file carries a setTimeout(() => fail(...), 200) guard that was added when the suite was ported from upstream socket.io in fe74c94. The upstream test has no such timers.

These tests drive the Engine.IO long-polling transport by hand via supertest: the first test performs 8 sequential HTTP round trips. On release that is ~50ms; on release+ASAN it is ~220ms. #34782 moved the x64-asan build lane from amazonlinux-2023 to debian-13 on Jul 21, and the failures started immediately after, with the work landing just past the 200ms guard every time.

The guard is not a behavioral assertion. It races real work against a wall clock and turns a slow-but-correct run into a failure. The test runner already applies its own per-test timeout (90s, tripled under ASAN in CI).

Fix

Drop the guard timers and align with the upstream structure: plain async tests that await the protocol events, with io.close() in a finally block so the server is always released. Every assertion is preserved; the same code paths are exercised.

Verification

# before, bun bd (debug+asan), 5 runs: 0 pass / 7 fail every time
# after, bun bd (debug+asan), 5 runs:
7 pass / 0 fail (×5)

# release bun, 3 runs:
7 pass / 0 fail (×3)

[stamp-90s] gate passed · iteration 0 · 1 files touched

passes on PR (with fix)
Test-only change.

Debug/ASAN (expected pass):
$ bun bd test 'test/js/third_party/socket.io/socket.io-connection-state-recovery.test.ts'
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test test/js/third_party/socket.io/socket.io-connection-state-recovery.test.ts
bun test v1.4.0 (70a1c0502)

test/js/third_party/socket.io/socket.io-connection-state-recovery.test.ts:
(pass) connection state recovery > should restore session and missed packets [2559.28ms]
(pass) connection state recovery > should restore rooms and data attributes [1087.52ms]
(pass) connection state recovery > should not run middlewares upon recovery by default [1125.18ms]
(pass) connection state recovery > should run middlewares even upon recovery [1000.73ms]
(pass) connection state recovery > should fail to restore an unknown session [421.80ms]
(pass) connection state recovery > should be disabled by default [434.45ms]
(pass) connection state recovery > should not call adapter#persistSession or adapter#restoreSession if disabled [405.80ms]

 7 pass
 0 fail
 43 expect() calls
Ran 7 tests across 1 file. [14.50s]
Exit: 0
diff hotspot
.../socket.io-connection-state-recovery.test.ts    | 318 +++++++++------------
 1 file changed, 134 insertions(+), 184 deletions(-)

gate history · 1 passed · 0 rejected · iteration 0

evidence per changed file
file                                                      reads  edits  tests
…y/socket.io/socket.io-connection-state-recovery.test.ts      2      3      0

self-review · no surviving concerns

25 concerns were raised and did not survive verification.

root cause · written by the author bot

The test wrapped its async work in a hardcoded 200ms setTimeout guard that was not a behavioral assertion, and on slow ASAN builds the real protocol steps outran that window, so the guard fired and failed the test even though the code under test behaved correctly. The fix removes the wall-clock guards entirely and converts each test to a plain async function that awaits the protocol steps directly, with server cleanup moved into a try/finally block. Hangs are now bounded by the test runner's per-test timeout rather than an arbitrary 200ms race, and all original assertions are preserved.

The connection-state-recovery tests drive the Engine.IO long-polling
transport by hand via supertest: the heaviest case performs 8
sequential HTTP round trips. When these were ported from upstream
socket.io in fe74c94, each test gained a 200ms setTimeout guard
that calls fail() and closes the server.

On the debian-13 x64-asan lane (introduced in #34782) the first test
consistently runs at ~220ms, so the 200ms guard fires mid-work and
fails the test with 'error: timeout'. Seen on builds 77055 (219.84ms)
and 77789 (219.96ms).

The guard timers are not part of the upstream test (which is plain
async and simply calls io.close() at the end). They duplicate the test
runner's own timeout and turn a slow-but-correct run into a failure.

Drop the timers and align with upstream: plain async tests that await
the protocol events, with io.close() in a finally block. Same
assertions, same code paths, no wall-clock race.
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 21 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 42dc168a-237b-4b63-be01-05963747ed27

📥 Commits

Reviewing files that changed from the base of the PR and between 47597ab and 70a1c05.

📒 Files selected for processing (1)
  • test/js/third_party/socket.io/socket.io-connection-state-recovery.test.ts

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 9:06 AM PT - Jul 22nd, 2026

@robobun, your commit 70a1c05 has 3 failures in Build #77814 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 35132

That installs a local version of the PR into your bun-35132 executable, so you can run:

bun-35132 --bun

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM — mechanical de-flake that removes wall-clock races without weakening any assertion.

What was reviewed:

  • All 7 tests: every original expect is preserved verbatim; io.close() moves from success()/fail() into finally, so cleanup still runs on every path.
  • Last test's negative contract (persistSession/restoreSession not called) is now an explicit expect(called).toBe("") after all protocol steps — strictly stronger than the old fail()-in-callback which raced the main flow's done().
  • fail/success util exports remain live in 9 sibling test files, so no dead code introduced.
Extended reasoning...

Overview

Test-only change to test/js/third_party/socket.io/socket.io-connection-state-recovery.test.ts. Converts 7 tests from done-callback style wrapped in an async IIFE with a 200ms setTimeout guard to plain async tests with try/finally { io.close() }. No runtime code, no assertion values changed.

Security risks

None. Third-party integration test file; no auth, crypto, or production code paths touched.

Level of scrutiny

Low. This is exactly the anti-pattern REVIEW.md calls out — a literal setTimeout guard that races real work against a wall clock. The 200ms was never a behavioral assertion (upstream socket.io has no such timer); the test-runner's own per-test timeout still bounds hangs. The refactor to async + finally matches the repo's stated conventions (cleanup registered before assertions, no throwing inside event callbacks).

Other factors

  • Cleanup semantics preserved: success()/fail() both called io.close(); now finally does. The finally placement means the server is released even if an expect in the try throws — same as before via the old catch { fail(...) }.
  • The DummyAdapter test's negative contract moved from fail(done, ...) inside override callbacks (which could race the IIFE's success(done, ...)) to a recorded flag asserted after the protocol completes — the correct shape for "X does not happen" per REVIEW.md.
  • io.once("connection", ...) handlers are still registered before the awaits that trigger them; ordering unchanged.
  • PR description shows before/after verification on debug+ASAN (0→7 pass) and release (7 pass), which is the right check for a de-flake.

@robobun

robobun commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator Author

The changed file passes on every lane. Build 77814 reds are unrelated:

  • test/js/node/test/parallel/test-net-connect-memleak.js and test-gc-http-client-connaborted.js are FinalizationRegistry/GC tests broken on main (same failures on build 77789 and other main builds); reported separately.
  • step-failed-outside-runner on darwin 26 aarch64: The number of VMs exceeds the system limit (CI host capacity).
  • test/cli/install/bun-add.test.ts, bun-install.test.ts, bun-install-lifecycle-scripts.test.ts, bun-install-registry.test.ts are GitHub API 504s (GET https://api.github.com/repos/.../tarball/... - 504).
  • Remaining flaky entries (es-module-lexer, spawn.test.ts, no-orphans, test-http-*, test-fs-promises-*) are known intermittents, all marked flaky by the runner.

Self-review: no surviving concerns. Ready for a maintainer.

@Jarred-Sumner
Jarred-Sumner merged commit c36479c into main Jul 22, 2026
51 of 54 checks passed
@Jarred-Sumner
Jarred-Sumner deleted the farm/8560f1fc/socket-io-recovery-deflake branch July 22, 2026 21:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants