Skip to content

bun test --isolate: give each test file its own Bun.s3 client - #47493

Open
robobun wants to merge 2 commits into
mainfrom
robobun/af19f6ad/s3-default-client-per-realm
Open

robobun wants to merge 2 commits into
mainfrom
robobun/af19f6ad/s3-default-client-per-realm

Conversation

@robobun

@robobun robobun commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • Under bun test --isolate or --parallel, a call on Bun.s3 never settles from the second test file on. await Bun.s3.file("k").exists() times out after 5000 ms. 1.4.2 passes. 1.4.3, 1.4.4 and main do not.
  • get_s3_default_client (src/runtime/api/BunObject.rs:1374) returns a per-VM Strong, RareData.s3_default_client, to every global. The second file gets the client of the first.
  • The shared client was always wrong. bun test --isolate: fence a finished file's realm against late completions #41831 made it hang: native code now drops calls into the realm of a finished file.

Fix

  • Delete the per-VM Strong. Bun.s3 is a lazy property: JSC calls the getter once for each Bun object and keeps the result there, as for Bun.redis.
  • Each realm builds its own client from the same credentials.
  • Verified: 2 new cases, in test/cli/test/isolation.test.ts and test/js/bun/s3/s3.test.ts. Both fail on main.
  • Self-reviewed with bun:sql: keep the query callbacks on the realm that registers them #47494: 25 concerns raised, 23 addressed (Notes).

Background

  • A realm is a global object with its own Bun object. --isolate makes one for each test file. A ShadowRealm is one.
  • A Strong is a GC root. This one also kept the global of the first file alive for the whole run.
  • Considered clearing the slot at the file swap: a ShadowRealm still gets the wrong client, and nothing else reads the slot.

Downsides

  • Each extra realm that reads Bun.s3 builds a client, one time: 27,553 instructions and 9 mallocs. Before: 30 instructions.
  • Under --isolate, a property that a file sets on Bun.s3 is gone in the next file.
Notes

Repro

// 1.test.ts and 2.test.ts, the same text
import { test } from "bun:test";
test("uses Bun.s3", async () => {
  console.log(await Bun.s3.file("k").exists().then(String, e => e.code));
});

bun test --isolate ./1.test.ts ./2.test.ts, with no S3 credentials in the environment:

Build File 2
1.4.2 ERR_S3_MISSING_CREDENTIALS at once (it has the client of file 1, and the call settles)
1.4.3 times out after 5000 ms
1.4.4 times out after 5000 ms
main (f7607a3d35) times out after 5000 ms
this PR ERR_S3_MISSING_CREDENTIALS at once, from its own client

Each file passes alone on every build. afd78836a7 (#41831) is an ancestor of bun-v1.4.3 and not of bun-v1.4.2.

In a ShadowRealm: after new ShadowRealm().evaluate("Bun.s3.madeIn = 'realm'"), main gives Bun.s3.madeIn === "realm" in the host realm. With this PR it is undefined, in either read order.

Measurements (linux-x64 release builds, f7607a3d35 against this PR)

What main PR How
First Bun.s3 read of a VM 40,832 instructions, 56 mallocs 39,092 instructions, 54 mallocs gdb single-step of BunObject_lazyPropCb_s3
First Bun.s3 read of a second realm 30 instructions, 0 mallocs 27,553 instructions, 9 mallocs same
Protected roots after void Bun.s3 S3Client 1 S3Client 0 heapStats().protectedObjectTypeCounts
Live globals in the third isolated file, after file 1 read Bun.s3 3 2, as with no read heapStats() after two Bun.gc(true), --isolate and --parallel=2
RareData 8 B smaller for each VM one pointer
.text 108,798,697 B 108,797,673 B (-1,024 B) size -A. .rodata does not change. No new host function.

perf, valgrind, strace and bloaty are not installed in the container. The instruction counts come from gdb.

Tests

  • isolation.test.ts: the second file has its own Bun.s3, under --isolate and inside one --parallel worker.
  • s3.test.ts: Bun.s3 in a ShadowRealm is a client of that realm.
  • Also run: all of isolation.test.ts (51 pass) and s3.test.ts (308 pass, 270 skip without R2 credentials).

Not changed

Other values of one realm that the VM or the process still keeps:

Self-review

The review covered this PR and #47494 as one diff. It asked for the split: this half fixes a regression and has no design question. Not done: a fix for the JIT plan that can root a finished file's global for a short time (it is not a value that the VM owns), and a move of the retention tests of #47494 into the test table of another open PR.

`get_s3_default_client` cached the client in a per-VM Strong and returned
it to every global of the VM. Under `bun test --isolate` the next file got
the client of a finished file, and the calls of that client never settled.

`Bun.s3` is a lazy property. JSC calls the getter one time for each `Bun`
object and keeps the result on that object. Delete the per-VM slot.
@robobun

robobun commented Oct 30, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 12:33 PM PT - Oct 30th, 2026

@robobun, your commit 9dac953 is building: #135876

@robobun

robobun commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status

@coderabbitai

coderabbitai Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 5 seconds.

Check out review usage here.

View limit details

Limit details: You’ve used all 10 included reviews currently available.

Learn how review limits work.

Review configuration:

Run configuration
Commits

Reviewing files that changed from the base of the PR and between 9dac953 and b0fa6b1.

Files selected for processing: 2
  • test/cli/test/isolation.test.ts
  • test/js/bun/s3/s3.test.ts

No actionable comments were generated in the recent review.

Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between f7607a3 and 9dac953.

Files selected for processing: 4
  • src/jsc/rare_data.rs
  • src/runtime/api/BunObject.rs
  • test/cli/test/isolation.test.ts
  • test/js/bun/s3/s3.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 3 remain after this review.

Walkthrough

Bun.s3 now creates a client without caching it in RareData. The getter loads credentials through the VM’s mutable environment loader. Tests check client separation across isolated files, parallel workers, and a ShadowRealm.

Changes

S3 client realm isolation

Layer / File(s) Summary
S3 client creation and lifecycle
src/runtime/api/BunObject.rs, src/jsc/rare_data.rs
The getter reads credentials through the VM’s mutable environment loader and returns the created client directly. RareData no longer stores or deinitializes the cached client.
Realm and isolated-file regression tests
test/cli/test/isolation.test.ts, test/js/bun/s3/s3.test.ts
Tests check that clients do not share properties across isolated files or between a ShadowRealm and the current realm.

Possibly related issues

Suggested reviewers: jarred-sumner, alii

Merge Risk: Minimal · previous review 9dac9

No actionable merge-blocking risk is established for the per-realm S3 client change. Merge after normal checks pass.

Pre-merge checks: Passed 4
Check name Status Explanation
Title check Passed The title clearly states the main change: each isolated test file receives its own Bun.s3 client.
Description check Passed The description explains the problem, root cause, fix, trade-offs, and verification results. It does not use the exact template headings, but it provides the required change summary and verification i…
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.
Full details: Description check

Explanation

The description explains the problem, root cause, fix, trade-offs, and verification results. It does not use the exact template headings, but it provides the required change summary and verification information.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

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

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Beyond the inline finding, I checked the deleted Strong/ensure_still_alive against the rooting rules: s3 is registered as PropertyCallback in src/jsc/bindings/BunObject.cpp:1019 (same as redis), so JSC stores the returned client as an own property of each realm's Bun object and get_valkey_default_client already follows this no-cache shape. The error path of get_s3_default_client is byte-for-byte the base's, the raw transpiler.env deref is replaced by the existing env_mut() helper that carries its own SAFETY comment, and no reference to s3_default_client remains anywhere in the repo.

Extended reasoning...

The PR removes the per-VM RareData.s3_default_client Strong and makes the Bun.s3 lazy-property callback build and return a fresh S3Client per realm, plus two tests (isolate/parallel and ShadowRealm). It touches GC rooting of a native wrapper but no auth, injection, or data-exposure surface; the credentials read is unchanged. A test-coverage finding is posted inline, and since the change deletes a GC root a human glance is still reasonable, so this is a defer with a ruled-out note rather than an approve.

Comment thread test/cli/test/isolation.test.ts Resolved
The per-VM slot no longer keeps the client. The lazy property of the
`Bun` object is what makes two reads in one realm give one object.

@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 — the second commit adds the same-realm identity assertions I asked for, and the fix itself is the same shape Bun.redis already uses.

What was reviewed:

  • Confirmed s3 is a PropertyCallback entry on the Bun object (BunObject.cpp:1019), so JSC reifies the returned client onto each realm's own Bun object; get_valkey_default_client relies on the identical mechanism with no VM-wide Strong.
  • Checked the to_js return path: the wrapper goes straight from the Rust callback into JSC's putDirect with nothing allocating in between, so dropping ensure_still_alive/StrongOptional leaves no window.
  • env_mut() is required by get_s3_credentials(&mut self); the loader borrow ends before any other VM access. No stale s3_default_client references remain in the tree.
  • Both new tests fail on the old code with a wrong value (shared madeIn/markedBy) rather than a hang, and the --parallel=2 case uses the 60s scale-up knob so both files share one worker VM.
Extended reasoning...

The PR removes the VM-wide RareData.s3_default_client Strong and lets the Bun.s3 lazy property hold the client per realm, touching src/jsc/rare_data.rs, src/runtime/api/BunObject.rs, and two test files (72 insertions, 31 deletions). It touches no auth, injection, or data-exposure surface: credential reading from the env loader is unchanged, only where the resulting wrapper is cached moved. The deciding facts for approval were that the mechanism is already proven by Bun.redis in the same file, the returned value is stored by JSC immediately with no GC window, the new tests assert identity within a realm and isolation across realms and fail on the unfixed code for the right reason, and the one prior inline comment was addressed by the second commit. No CODEOWNERS entry covers the changed paths and the hunt ran dry rather than hitting a budget bound.

This branch has not been deployed

No deployments
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