Skip to content

Bun.write: keep stdio writes in call order when stdout/stderr is a regular file - #35949

Closed
robobun wants to merge 1 commit into
mainfrom
farm/358e96be/bun-write-stdout-order
Closed

Bun.write: keep stdio writes in call order when stdout/stderr is a regular file#35949
robobun wants to merge 1 commit into
mainfrom
farm/358e96be/bun-write-stdout-order

Conversation

@robobun

@robobun robobun commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator

What's wrong

Un-awaited Bun.write(Bun.stdout, ...) calls land out of order when fd 1 is a regular file:

// bun r.mjs > out.txt && cat out.txt  ->  C0 C4 C3 C2 C1
// bun r.mjs | cat                     ->  C0 C1 C2 C3 C4
for (let i = 0; i < 5; i++) Bun.write(Bun.stdout, "C" + i + "\n");

Mixed with console.log, all console lines come out first and the Bun.write batch after them, reversed. await-ing each write or using Bun.stdout.writer() stays in order; only the fire-and-forget queue on a file destination drains as a stack. Log/report writers that fire-and-forget to a redirected stdout produce a correctly-sized but reordered file with exit 0.

Cause

write_file_internal's synchronous fast path is gated on the destination blob's mode. The condition (added in #7470) excludes fd-backed blobs whose fstat mode is S_IFREG, which is exactly what Bun.stdout / Bun.stderr become under > out.txt. Those writes are instead wrapped in a WriteFileTask and scheduled onto the WorkPool, whose global run_queue is a Treiber stack: the first write is picked up immediately by a worker, the remaining four pile onto the stack while that worker is in write(), and the worker then drains them LIFO. Hence C0 C4 C3 C2 C1.

The guard is inverted relative to its own comment, which says it is meant to keep pipes off the main thread. Pipes, ttys and unknown-mode fds have always taken the synchronous path (and so have always been ordered); only regular-file fds were routed to the thread pool.

Fix

Drop the mode exclusion so every non-S3 fd blob at offset 0 takes write_{string,bytes}_to_file_fast, the same synchronous path pipes and ttys already use. Small writes to a redirected stdout/stderr now complete on the JS thread in call order, with an already-settled promise, matching the pipe case. A pipe that would block still falls through to the async path via needs_async on EAGAIN, which is the existing behaviour.

Verification

New tests in test/js/bun/io/bun-write.test.js spawn a child that issues five Bun.write(Bun.{stdout,stderr}, ...) calls with the stream redirected to a file and to a pipe, and assert both that the output is C0..C4 and that none of the returned promises were still pending when Bun.write() returned. Against main's src/ the file cases fail with pending=5 and reversed output; with this change all four pass.

Windows is skipped: Bun.write() there always goes through uv_fs_write on the libuv thread pool, which can also race writes to a file-backed stdio fd, but via a different mechanism (worker concurrency rather than a LIFO stack) that the POSIX fast path does not touch.


no test proof · iteration 0 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/bun/io/bun-write.test.js

…egular file

Five un-awaited Bun.write(Bun.stdout, ...) calls with stdout redirected to a
file landed as C0 C4 C3 C2 C1. The fast-path eligibility check in
write_file_internal excluded fd-backed blobs whose fstat mode was S_IFREG,
which is the one case Bun.stdout/Bun.stderr hit under shell redirection.
Those writes were instead scheduled onto the WorkPool, whose global run
queue is a Treiber stack: the first write runs immediately and the rest are
drained in reverse push order.

The exclusion dates to the io_uring removal in #7470 and was inverted
relative to its own comment (which intended to keep pipes off the main
thread). Removing it sends every non-S3 fd blob with offset 0 through
write_{string,bytes}_to_file_fast, the same path pipes and ttys already
take, so small stdio writes land in call order regardless of what fd 1/2
points at. A blocking pipe still falls through to the async path via
needs_async on EAGAIN.
@coderabbitai

coderabbitai Bot commented Jul 26, 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: 7 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: 7ad54d2f-cc89-4e39-a3fc-e94e51fbb74e

📥 Commits

Reviewing files that changed from the base of the PR and between 44f6469 and 5a03f8c.

📒 Files selected for processing (2)
  • src/runtime/webcore/Blob.rs
  • test/js/bun/io/bun-write.test.js

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

@robobun

robobun commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 11:53 AM PT - Jul 26th, 2026

@robobun, your commit 5a03f8c has 1 failures in Build #82499 (All Failures):

  • 📦 Binary size — 12 over 0.50 MB
  • targetthis build canary: main #79916
    sizeΔ
    bun-darwin-aarch6458.13 MB57.58 MB+564.9 KB
    bun-darwin-x6463.48 MB62.95 MB+544.5 KB
    bun-linux-aarch6470.98 MB70.42 MB+576.0 KB
    bun-linux-x6472.47 MB71.95 MB+528.0 KB
    bun-linux-aarch64-musl64.88 MB64.32 MB+576.0 KB
    bun-linux-x64-musl66.98 MB66.45 MB+544.0 KB
    bun-linux-aarch64-android78.47 MB77.97 MB+512.0 KB
    bun-linux-x64-android80.62 MB80.10 MB+529.2 KB
    bun-freebsd-x6483.07 MB82.56 MB+528.0 KB
    bun-freebsd-aarch6484.84 MB84.31 MB+544.0 KB
    bun-windows-x6480.26 MB79.70 MB+570.5 KB
    bun-windows-aarch6470.86 MB70.34 MB+533.0 KB

    Add [skip size check] to the commit message if this increase is intentional.


🧪   To try this PR locally:

bunx bun-pr 35949

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

bun-35949 --bun

Comment thread src/runtime/webcore/Blob.rs
@robobun

robobun commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator Author

CI is red on lanes this diff does not touch:

  • binary-size is comparing against canary #79916 (stale baseline) and fails with +540KB on unrelated PR builds 82498 and 82480 as well; this diff removes code.
  • darwin 14 x64 jobs failed on box darwin-flatbread-x64-1 with uv_os_get_passwd returned ENOENT in the Node test runner and Cannot find module '_util/numeric.ts' from test/harness.ts. Build 82498 (different PR) shows the same lane red.
  • napi.test.ts and bun-install-registry.test.ts are flagged flaky and passed on retry.

test/js/bun/io/bun-write.test.js (the new ordering tests) passed on every lane that ran it. Ready for review.

@robobun

robobun commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

Closing: #37128 reworks the stdio sinks and lists this PR as superseded (Bun.write(Bun.stdout | Bun.stderr, ...) goes through the per-fd sink in call order there instead of the thread pool, whatever fd 1/2 points at, so the regular-file reversal fixed here is covered). Its tests exercise Bun.write ordering on a pipe; the file-redirect test from this PR is suggested over there.

If something here turns out not to be covered once #37128 lands, this can be reopened.

@robobun robobun closed this Aug 13, 2026
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