Skip to content

test: make test-fs-read-stream-pos.js deterministic (90s worst case → <100ms) - #36775

Open
robobun wants to merge 2 commits into
mainfrom
farm/483ca7fb/test-fs-read-stream-pos-deterministic
Open

test: make test-fs-read-stream-pos.js deterministic (90s worst case → <100ms)#36775
robobun wants to merge 2 commits into
mainfrom
farm/483ca7fb/test-fs-read-stream-pos-deterministic

Conversation

@robobun

@robobun robobun commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

What

The exit path of the upstream test is a pure timing race: the 1 ms append interval must land between two consecutive ReadStream preads within one stream instance (a short chunk followed by another 'data' before 'end'). Upstream ships a 90 s safety timer for when the race doesn't hit. On that path the assertion block never runs, so the test exits 0 without checking anything.

This PR appends synchronously inside the 'data' handler the moment a short chunk arrives. The stream's next _read is scheduled after the handler returns, so that pread is guaranteed to find the appended bytes. It is exactly the code path the race was trying to reach (short read, file grows, next pread from the same stream), just without the race. The 90 s safety timer and the two runner workarounds from #36478 are then dead and are removed.

The assertion block now also checks that the concatenated chunks are byte-for-byte the file's [streamStart, cur) range. nodejs/node#33940 was a wrong-position pread after a short read; this catches it directly (duplicated or skipped bytes fail the equality) instead of only via the broken-line heuristic.

The 1 ms background writer and the common.mustCallAtLeast wrappers are kept so the test still reads a growing file the way the original does.

Why

Build #87550 spent 90 s on this file on Windows 2019 x64, and #36478 measured 1-40 s over 30 solo Windows runs. When it reaches 90 s the test has asserted nothing. 'data' runs synchronously from the read callback's push, and _read is re-scheduled via process.nextTick after push returns, so a synchronous append inside the handler is ordered strictly before the next pread in both Bun's readStreamPrototype._read and Node's ReadStream.prototype._read. For hwm = 10 and 11/12-byte lines, the running file size is never a multiple of 10 while counter ≤ 105, so the first stream always produces a short tail chunk and the test exits in that stream.

Orthogonal to #36548 / #36479: those address the event-loop ordering that made the race hard to hit; this makes the test independent of the race so neither the 120 s runner ceiling nor the serial scheduling is needed.

Verification

Release (USE_SYSTEM_BUN=1 bun test/js/node/test/parallel/test-fs-read-stream-pos.js):

platform before after
linux x64, 10 runs 129-5730 ms 71-99 ms
windows x64, 5/20 runs 275-10511 ms (90 s on #87550) 67-75 ms
windows x64 under 3 concurrent test-fs-read-stream-fd-leak.js neighbours (failed mustCallAtLeast per #36478) 69-75 ms, exit 0

bun bd test/js/node/test/parallel/test-fs-read-stream-pos.js (linux x64 debug+ASAN): 3.5-3.9 s before, 3.3-4.1 s after. Both are dominated by debug startup and lazy node:stream load; the test body itself is a handful of preads either way.

Node passes the modified test in 60-111 ms on both platforms.

Assertion strength: an instrumented run confirms the isLow branch is reached in the first stream on every run on both platforms, and a createReadStream with a custom options.fs.read that rewinds the position by one byte after a short read (simulating nodejs/node#33940) fails the new deepStrictEqual in both Bun and Node.

bun test test/internal/parallel-allowlist.test.ts: 2/2 pass. node --check scripts/runner.node.mjs: ok.


no test proof · iteration 1 · docs-only change; test-proof not applicable

…safety timer

The exit path of the upstream test is a pure timing race: the 1ms append
interval must land between two consecutive ReadStream preads within a
single stream instance (short chunk followed by another 'data' before
'end'). Upstream ships a 90s safety timer for when that race doesn't
hit; on the 90s path the assertion block never runs and the test exits 0
without checking anything.

Append synchronously in the 'data' handler when a short chunk arrives so
the stream's next pread (scheduled after the handler returns) always has
bytes to read. Same code path as the race, but the follow-up chunk is
guaranteed, so the 90s timer and the runner workarounds from #36478 can
go. Also assert the concatenated chunks are byte-for-byte the file's
[streamStart, cur) range, which catches a wrong-position pread directly
instead of only via broken-line heuristics.
@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

Filesystem stream test parallelization

Layer / File(s) Summary
Deterministic stream validation
test/js/node/test/parallel/test-fs-read-stream-pos.js
The test seeds file contents, tracks stream offsets, validates exact file slices, appends after short reads, and removes its safety timer.
Runner parallel handling
scripts/runner.node.mjs
The test now uses standard timeout handling and runs in the parallel phase.

Possibly related PRs

  • oven-sh/bun#36478: Directly relates to the runner timeout and serial-execution changes for the same test.
  • oven-sh/bun#33622: Modifies the same runner and filesystem stream test for parallel execution and timeout handling.
  • oven-sh/bun#36175: Adjusts related runner parallel scheduling and timeout handling.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the test and its deterministic performance improvement.
Description check ✅ Passed The description explains the change, motivation, implementation, and verification results in sufficient detail.

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

@github-actions github-actions Bot added the claude label Aug 2, 2026

@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 — test-only de-flake that strengthens the assertion instead of weakening it.

Reviewed: the 'data'-handler ordering argument (sync append lands before the next _read schedules its pread), the seed line guaranteeing the mustCallAtLeast on 'data' can no longer fail under I/O-heavy neighbours, and that bufs/streamStart/cur bookkeeping matches the new [streamStart, cur) byte-exact check across stream restarts. The two removed runner.node.mjs special cases have no other references; isParallelSafeTest collapses cleanly to the path check.

Extended reasoning...

Overview

Two files: test/js/node/test/parallel/test-fs-read-stream-pos.js (upstream Node parallel test) and scripts/runner.node.mjs (CI test runner). The test change replaces a 90 s timing-race exit path with a deterministic one — a synchronous append inside the 'data' handler after the first short chunk, so the same stream's next pread is guaranteed to see new bytes and hit the assertion block. It also seeds one line before the first stream and adds a byte-exact deepStrictEqual of the concatenated chunks against the file's [streamStart, cur) slice, which directly targets the nodejs/node#33940 wrong-position bug the test was written for. The runner change removes the 120 s per-file timeout override and the serial-only carve-out added in #36478, both now dead.

Security risks

None. Test-only and CI-runner-only; no runtime, native, or user-facing code touched.

Level of scrutiny

Low-to-moderate. It rewrites an upstream Node test, so the REVIEW.md bar is "when de-flaking, keep asserting the property the original assertion protected." That's met and exceeded: the original brokenLines heuristic is retained verbatim and a strictly stronger byte-exact check is added on top. The 90 s safety-timer path being removed was itself a silent no-op exit (asserted nothing), so nothing of value is lost. The ordering claim — 'data' fires synchronously from push, next _read goes through process.nextTick — is standard Readable semantics in flowing mode and the PR verified it empirically in both Bun and Node.

Other factors

  • bufs is reset only in 'end' (which also nulls stream), and streamStart = cur is captured only when stream is null, so the new read.length === cur - streamStart invariant holds by construction across stream restarts.
  • The seed appendLine() guarantees ≥11 bytes before the first stream, so with hwm = 10 a short tail chunk always exists in stream 1; combined with the in-handler append, the isLow assertion path is reached on the first stream every run (author confirmed on linux+windows).
  • No stale references remain: grepped for test-fs-read-stream-pos — only test/expected-durations.json (94 ms, consistent with the new sub-100 ms runtime) and the test file itself. test/parallel-allowlist.json has no per-file entry for it. isParallelSafeTest reduces to the plain path check with no dead branches.
  • Verification section is thorough: multi-platform release + debug+ASAN runs, Node cross-check, concurrent-neighbour stress, and a negative test simulating the original regression that fails the new assertion.

@robobun

robobun commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 7:07 AM PT - Aug 2nd, 2026

@robobun, your commit b8035fa has 1 failures in Build #87797 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 36775

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

bun-36775 --bun

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/js/node/test/parallel/test-fs-read-stream-pos.js`:
- Around line 18-39: Revert all local modifications to the vendored Node.js test
test-fs-read-stream-pos.js, including the writer changes, byte-range assertions,
short-read timing changes, and endTimer removal, restoring the upstream version.
Do not add fixes to this mirror; submit the deterministic change upstream to
nodejs/node first, then resync the mirror. If interim Bun coverage is required,
add a separate Bun-owned test outside the verbatim Node.js port.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2eb6c9b8-d7ea-49cf-97bc-a0c5c8710d46

📥 Commits

Reviewing files that changed from the base of the PR and between 7354c5e and b8035fa.

📒 Files selected for processing (2)
  • scripts/runner.node.mjs
  • test/js/node/test/parallel/test-fs-read-stream-pos.js
💤 Files with no reviewable changes (1)
  • scripts/runner.node.mjs

Comment thread test/js/node/test/parallel/test-fs-read-stream-pos.js
@robobun

robobun commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator Author

CI status: test-fs-read-stream-pos.js passed on every lane in both builds (#87773, #87797). Remaining reds are unrelated to this diff:

Ready for a maintainer.

@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 — test-only de-flake that strengthens assertions and removes now-dead runner workarounds.

What was reviewed:

  • Traced the deterministic exit path: seeded 11-byte line + hwm=10 guarantees a short tail chunk on the first stream; synchronous appendLine() in the short-chunk handler is ordered before the next _read (scheduled via nextTick), so the follow-up 'data' event is guaranteed and the assertion block always runs.
  • Confirmed the new deepStrictEqual(read, onDisk.subarray(streamStart, cur)) directly catches nodejs/node#33940 (wrong-position pread), strictly stronger than the retained broken-line heuristic; mustCallAtLeast wrappers preserved.
  • Runner changes: the 120s override and serial exclusion in scripts/runner.node.mjs are dead once the 90s safety timer and interval-starvation dependency are gone; default 20s CI timeout is ample for a <100ms test.
Extended reasoning...

Overview

Two files touched, both test infrastructure: test/js/node/test/parallel/test-fs-read-stream-pos.js (Node-parallel test for the nodejs/node#33940 ReadStream position bug) and scripts/runner.node.mjs (removes the 120s per-test timeout override and the parallel-safe exclusion added for this file in #36478). No production code is modified.

The test change replaces a timing race (1ms background writer must land between two consecutive preads) with a deterministic trigger: when a short chunk arrives, append a line synchronously inside the 'data' handler so the stream's next scheduled _read is guaranteed to find bytes. The 90s safety timer — which let the test exit 0 without asserting anything when the race never hit — is removed. The assertion block gains a direct byte-range equality check against the file on disk.

Security risks

None. This is a test file operating on a temp-directory scratch file plus a mechanical removal of two special-case entries from the CI runner script.

Level of scrutiny

Moderate for a test change. REVIEW.md's de-flaking rule ("keep asserting the property the original assertion protected") is the governing bar, and this PR clears it: the original broken-line assertion is retained, a strictly stronger deepStrictEqual on the exact byte range is added, the mustCallAtLeast wrappers are kept, and the previously-reachable "exit at 90s having asserted nothing" path is eliminated. The PR description includes fault-injection verification (a custom options.fs.read that rewinds after a short read fails the new assertion in both Bun and Node), timing measurements on both platforms including under I/O contention, and confirmation that upstream Node passes the modified test.

I checked the determinism argument: the seeded line hello at 1\n is 11 bytes vs hwm=10, so the first stream always produces a 10-byte chunk followed by a short tail; appendLine() runs synchronously in the handler before the next _read (which is nextTick-scheduled after push returns), so the follow-up pread always finds bytes and the isLow assertion block always executes. No hang path remains after removing the 90s timer.

Other factors

The runner deletions are exactly the two workarounds #36478 added for this file's timing dependency; with the dependency removed they are dead code and correctly cleaned up in the same PR. node --check and parallel-allowlist.test.ts were run per the description. The change is self-contained, well-reasoned, and thoroughly verified across platforms and runtimes.

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.

1 participant