Skip to content

install: fix ENOENT race on shared cache when RENAME_EXCHANGE is unsupported (NFS) - #36229

Open
robobun wants to merge 9 commits into
mainfrom
farm/eb686b16/install-cache-rename-race
Open

install: fix ENOENT race on shared cache when RENAME_EXCHANGE is unsupported (NFS)#36229
robobun wants to merge 9 commits into
mainfrom
farm/eb686b16/install-cache-rename-race

Conversation

@robobun

@robobun robobun commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Fixes #36227

Problem

Two or more concurrent bun install processes sharing BUN_INSTALL_CACHE_DIR on a filesystem without renameat2(RENAME_EXCHANGE) support (NFS, FUSE) could fail with:

ENOENT: failed copying files from cache to destination for package <name>

When a process lost the RENAME_NOREPLACE race to publish a cache entry, renameat_concurrently_without_fallback tried RENAME_EXCHANGE (always EOPNOTSUPP on NFS) and then fell back to delete_tree(dest) + renameat. That recursive delete unlinked the winner's cache entry files while another process was mid-copy from the cache into node_modules. On local filesystems RENAME_EXCHANGE succeeds and, crucially, the displaced tree is left intact in the temp dir, so readers never see files disappear.

Moving the destination aside before deleting (as suggested in the issue) is not sufficient: a reader holds an open fd on the cache entry directory for the whole copy, and renaming a directory does not invalidate that fd, so deleting the displaced tree under any name still unlinks files out from under the reader. Verified by the new test, which still failed with an aside-then-delete approach.

Fix

Cache entries only ever appear via an atomic rename of a fully extracted tree, so an existing entry at the destination is complete and content-identical to ours. On a rename collision, keep the existing entry and discard the freshly extracted temp copy (which no other process reads). This mirrors what commit_global_store_entry already does for isolated installs. Applied to both cache-population sites:

  • extract_tarball.rs: first-wins, except when the existing entry is incomplete (missing package.json for npm packages, matching package_missing_from_cache). An incomplete entry is still replaced because that is the only repair path for a corrupt entry, preserving the re-extraction repair flow.
  • patch_install.rs: first-wins; the entry name embeds the patch hash, so a same-named entry is equivalent.

The generic renameat_concurrently fallback (still used by the repair path, move_file_z, and patch commit/restore) no longer deletes the live destination in place: it renames the destination aside, renames the source into place, then deletes the old tree under its aside name, restoring the destination if the rename fails. This removes the wide partially-deleted-directory window for any remaining replace-semantics caller.

Test

test/cli/install/bun-install-cache-race.test.ts (Linux): an LD_PRELOAD shim makes renameat2(RENAME_EXCHANGE) fail with EOPNOTSUPP the way NFS does, then runs 4 concurrent bun install --backend=copyfile processes sharing a fresh cache, installing a 600-file package from a local in-process registry, for 3 iterations.

  • Unfixed bun: fails on iteration 1 with ENOENT: failed copying files from cache to destination (the exact failure from the issue).
  • Fixed build: passes, stable across repeated runs.

cargo check passes on all 10 targets (bun run rust:check-all); bun-install-streaming-extract, bun-install-patch, and bun-patch suites pass with the debug build.


no test proof · iteration 1 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/cli/install/bun-install-cache-race.test.ts

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

Cache commit paths now use exclusive atomic renames and preserve existing destinations during concurrent installs. The system fallback renames destinations aside before replacement, and a Linux regression test simulates unsupported rename exchange operations.

Concurrent install cache handling

Layer / File(s) Summary
Rename-aside fallback implementation
src/sys/lib.rs
The fallback displaces existing destinations, retries concurrent recreation, restores entries on failure, and removes aside directories after successful replacement.
Atomic cache commit handling
src/install/extract_tarball.rs, src/install/patch_install.rs
Extraction and patch installation use exclusive renames, preserve complete existing cache entries, and invoke concurrent rename fallback when needed.
Concurrent install regression coverage
test/cli/install/bun-install-cache-race.test.ts
A Linux test builds a failing renameat2 shim, serves a generated package, runs concurrent installs sharing a cache directory, and validates successful results without ENOENT output.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes address #36227 by preserving complete cache entries and hardening the fallback to avoid deleting live readers.
Out of Scope Changes check ✅ Passed All code changes appear directly related to the cache-race fix and its regression test.
Title check ✅ Passed The title clearly matches the PR's main change: fixing an ENOENT race in shared cache installs when RENAME_EXCHANGE is unavailable.
Description check ✅ Passed The description includes the problem, fix, and test verification details, which covers the template's required intent despite different headings.

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

Comment thread src/install/extract_tarball.rs Outdated
Comment thread src/install/patch_install.rs Outdated
Comment thread src/sys/lib.rs
Comment thread src/sys/lib.rs Outdated
Comment thread src/sys/lib.rs Outdated
Comment thread src/sys/lib.rs Outdated
Comment thread src/sys/lib.rs
Comment thread src/sys/lib.rs Outdated
Comment thread src/sys/lib.rs
Comment thread src/install/extract_tarball.rs
Comment thread src/install/patch_install.rs Outdated
Comment thread src/sys/lib.rs
@github-actions

Copy link
Copy Markdown
Contributor

Found 1 issue this PR may fix:

  1. Windows: parallel bun install --no-cache with shared BUN_INSTALL_CACHE_DIR can fail with ENOENT opening cache/package/version dir #28062 - Same concurrent bun install + shared BUN_INSTALL_CACHE_DIR ENOENT race condition, reported on Windows. The "first-wins" semantics and rename-aside pattern in this PR directly address the race window described.

If this is helpful, copy the block below into the PR description to auto-close this issue on merge.

Fixes #28062

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. install: don't leak package extraction temp directories into $TMPDIR #33979 - Fixes the same race condition in renameat_concurrently where the loser's cleanup destroys the winner's cache entry, introducing a "first-writer-wins" approach
  2. install: never delete a concurrently published cache entry on Windows #33884 - Fixes the same logical race (loser deletes winner's cache entry during concurrent install) on the Windows code path

🤖 Generated with Claude Code

@robobun

robobun commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator Author

On the bot findings above:

If #33979 or #33884 lands first I will rebase this onto it; the pieces compose (the fallback hardening and the NFS test remain relevant either way).

@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: 4

🤖 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 `@src/install/patch_install.rs`:
- Around line 613-621: Update the keep-existing branch in the patch installation
flow around rename handling so it only preserves an existing cache entry when
the directory exists and its final bun_hash_tag completeness marker is present.
Replace the bare directory_exists_at check with the appropriate sys::exists_at
check for that marker, allowing incomplete entries to remain replaceable while
preserving complete concurrent installs.

In `@src/sys/lib.rs`:
- Around line 9293-9310: Update the final Err branch of the rename retry logic
to delete the aside tree when the best-effort renameat restoration fails,
including when retries are exhausted and the destination remains occupied.
Preserve the existing successful restore behavior and return the original error
after cleanup.

In `@test/cli/install/bun-install-cache-race.test.ts`:
- Around line 24-67: Update the SHIM_C C source to guard all
SYS_renameat2-dependent logic with an `#ifdef` SYS_renameat2 check, so compilation
succeeds on older headers and the test can self-skip through its existing setup.
Preserve the current failure behavior when SYS_renameat2 is available, including
both syscall interception and renameat2 handling.
- Around line 123-160: Update makeRegistry to use Bun.serve with port: 0 instead
of node:http createServer. Adapt the request handler to Bun’s server API, obtain
the assigned port from the returned server for the tarball URL, and remove the
manual listen promise and createServer-specific lifecycle handling while
preserving the existing responses and async disposal.
🪄 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: 2d942bf1-2476-48dc-88e3-f22c2d0ec531

📥 Commits

Reviewing files that changed from the base of the PR and between be5c92a and 83f0ea2.

📒 Files selected for processing (4)
  • src/install/extract_tarball.rs
  • src/install/patch_install.rs
  • src/sys/lib.rs
  • test/cli/install/bun-install-cache-race.test.ts

Comment thread src/install/patch_install.rs Outdated
Comment thread src/sys/lib.rs
Comment thread test/cli/install/bun-install-cache-race.test.ts
Comment thread test/cli/install/bun-install-cache-race.test.ts Outdated

@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 `@src/sys/lib.rs`:
- Around line 9270-9274: Remove the delete_tree_at call from the no-space branch
in the rename flow around rename_aside_name. Ensure the destination is never
deleted in place: allocate or use sufficient storage to generate an aside name,
and if the path-length limit prevents that, return a catchable path-length error
before any filesystem side effect; otherwise preserve the aside-based rename
behavior.
🪄 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: 30209186-b5b2-496d-ae27-8a42b408b55f

📥 Commits

Reviewing files that changed from the base of the PR and between 83f0ea2 and 03368bf.

📒 Files selected for processing (3)
  • src/install/extract_tarball.rs
  • src/install/patch_install.rs
  • src/sys/lib.rs

Comment thread src/sys/lib.rs
Comment thread src/install/patch_install.rs
Comment thread src/install/patch_install.rs
Comment thread src/sys/lib.rs
Comment thread src/sys/lib.rs
Comment thread test/cli/install/bun-install-cache-race.test.ts Outdated
Comment thread src/sys/lib.rs

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/install/patch_install.rs (1)

604-629: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Revalidate completeness inside the concurrent fallback.

Line 615 checks the tag before renameat_concurrently. Two repairers can both see an incomplete entry; after one publishes a complete entry, the other's fallback can move it aside, replace it, and delete it. This breaks the first-wins contract and can again disrupt readers of the winning tree.

Make the fallback re-check the displaced destination's tag after claiming it, restoring/discarding the source when it is complete, or add per-entry coordination that makes this decision atomic.

🤖 Prompt for 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.

In `@src/install/patch_install.rs` around lines 604 - 629, Update the concurrent
fallback around renameat2 and the subsequent renameat_concurrently flow to
re-check the displaced destination’s completion tag after claiming it. If the
destination is complete, restore or discard the source without replacing the
published tree; otherwise proceed with replacement and cleanup. Preserve the
first-wins contract and avoid deleting or disrupting the winning entry.
🤖 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/cli/install/bun-install-cache-race.test.ts`:
- Around line 182-186: Update the concurrent-install test around “concurrent
installs sharing a cache survive a filesystem without RENAME_EXCHANGE” to seed
the shared cache with an incomplete entry before starting installs, ensuring
concurrent repair exercises the rename-aside fallback. Configure the preload
shim to inject and assert at least one EOPNOTSUPP, and verify the test observes
that injected failure while still completing successfully.

---

Outside diff comments:
In `@src/install/patch_install.rs`:
- Around line 604-629: Update the concurrent fallback around renameat2 and the
subsequent renameat_concurrently flow to re-check the displaced destination’s
completion tag after claiming it. If the destination is complete, restore or
discard the source without replacing the published tree; otherwise proceed with
replacement and cleanup. Preserve the first-wins contract and avoid deleting or
disrupting the winning entry.
🪄 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: f9738e46-904e-4868-8dbd-cc9f3265a3b2

📥 Commits

Reviewing files that changed from the base of the PR and between 03368bf and f62bb7f.

📒 Files selected for processing (3)
  • src/install/patch_install.rs
  • src/sys/lib.rs
  • test/cli/install/bun-install-cache-race.test.ts

Comment thread test/cli/install/bun-install-cache-race.test.ts Outdated
@robobun

robobun commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator Author

On the outside-diff review note about re-validating completeness inside the patch fallback: declined as out of scope. The repair path (entry exists but its completeness marker is missing) deliberately keeps last-wins semantics, since replacement is the only repair mechanism without per-entry locking. Two repairers racing over an already-corrupt entry was racy before this PR (in-place delete_tree) and is strictly narrower now (rename-aside). Making that decision atomic requires per-entry coordination (lock files), which the issue discussion notes is its own can of worms on NFS, and the healthy-entry path this PR fixes never touches a published tree.

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

I didn't find any bugs this pass (both of my earlier inline findings — the NAME_MAX guard in rename_aside_name and the isMusl skip on the shim test — were addressed), but this changes concurrency semantics in a shared syscall helper and the cache-population design, so it's worth a human look. Note also the flagged potential-duplicate PRs #33979 / #33884.

What was reviewed:

  • First-writer-wins on rename collision in extract_tarball.rs / patch_install.rs, with completeness gates (package.json / .bun-tag-<hash>) — matches the precedent in commit_global_store_entry.
  • The rename-aside fallback loop in renameat_concurrently_without_fallback: retry bound, ENOENT/EEXIST handling, restore-on-error, and cleanup of the aside tree on every exit path.
  • .is_ok() on the initial renameat2(exclude) swallows non-EEXIST errors, but the fallthrough to renameat_concurrently re-surfaces them, so no error is lost.
  • LD_PRELOAD shim test: hermetic registry, per-process pipe drain, marker-file assertion in the repair test proving the fallback ran.
Extended reasoning...

Overview

Fixes #36227 — concurrent bun install processes sharing a cache on filesystems without RENAME_EXCHANGE (NFS/FUSE) hit ENOENT mid-copy because the loser's fallback deleted the winner's cache entry in place. Touches src/install/extract_tarball.rs and src/install/patch_install.rs (first-writer-wins on collision, gated by a completeness marker so corrupt entries stay repairable), src/sys/lib.rs (renameat_concurrently_without_fallback now renames the destination aside before deleting, with a bounded retry loop and best-effort restore on error), and adds a Linux-only LD_PRELOAD regression test.

Security risks

None identified. Paths are Bun-generated cache/temp names, not untrusted input; no auth, crypto, or network parsing changes.

Level of scrutiny

High. renameat_concurrently_without_fallback is a shared helper reached by move_file_z (bun build --compile, bun upgrade, Tmpfile::finish), the extract repair path, and patch commit/restore, on both POSIX and Windows. The concurrency reasoning (open dir fds surviving rename, first-writer-wins vs. rename-aside) is subtle and cross-process. This is a design decision — the PR chose keep-existing over per-entry lockfiles or delete-in-place — that a maintainer should sign off on, especially given github-actions flagged #33979 and #33884 as potential duplicates addressing the same race.

Other factors

All prior review threads (CodeRabbit and my two inline nits) are resolved: NAME_MAX guard added to rename_aside_name, isMusl added to the test skip, patch keep-existing gated on the .bun-tag marker, aside tree deleted when restore fails, Bun.serve({port:0}) used for the stub registry, and a repair test with a shim-injection marker was added. The PR states rust:check-all and the install/patch suites pass; the new test is Linux-glibc-only and the evidence block notes it was deferred to CI.

robobun and others added 2 commits July 28, 2026 21:01
…ame collision

Concurrent bun install processes sharing BUN_INSTALL_CACHE_DIR raced when
the filesystem does not support renameat2(RENAME_EXCHANGE) (NFS, FUSE):
losing the RENAME_NOREPLACE race fell back to delete_tree + rename, which
unlinked the winner's cache entry files while another process was copying
them into node_modules, failing with ENOENT.

Cache entries only appear via an atomic rename of a complete tree, so on a
rename collision keep the existing entry and discard the freshly extracted
copy. An entry that exists but is missing package.json (a crashed copy) is
still replaced, since replacement is its only repair path.

The generic renameat_concurrently fallback also no longer deletes the live
destination in place: it moves the destination aside, renames the source
into place, then deletes the old tree under its aside name, restoring it
if the rename fails.

Fixes #36227
@robobun
robobun force-pushed the farm/eb686b16/install-cache-rename-race branch from 2504c93 to 5b1d90a Compare July 28, 2026 21:01

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

No issues found this pass; the two nits I raised earlier (NAME_MAX guard in rename_aside_name, isMusl skip for the LD_PRELOAD test) are both addressed, and the repair test now asserts the shim actually injected EOPNOTSUPP via the marker file. That said, this reworks the fallback semantics of a shared syscall helper (renameat_concurrently_without_fallback — every non-Linux caller and every NFS/FUSE Linux caller now goes through the new rename-aside/retry loop) and flips cache publication from last-wins to first-wins, so it's worth a human look.

Checked: the retry loop cleans up the aside tree on every exit (success, EEXIST retry, error-with-restore, restore-failed); rename_aside_name guards both PATH_MAX and NAME_MAX; the .is_ok() on the initial renameat2(NOREPLACE) swallowing non-EEXIST errors is fine because the fallback renameat_concurrently_a re-attempts and surfaces the real error; patch_install.rs buntag-buffer hoist preserves the same bytes/NUL as before.

Extended reasoning...

Overview

Four files: src/install/extract_tarball.rs and src/install/patch_install.rs change cache-entry publication from unconditional renameat_concurrently to renameat2(NOREPLACE) → keep-existing-if-complete → fallback; src/sys/lib.rs replaces the sad-path in renameat_concurrently_without_fallback (delete-tree + rename) with a rename-aside/retry loop and adds rename_aside_name; a new 300-line Linux-only test uses an LD_PRELOAD shim to force RENAME_EXCHANGEEOPNOTSUPP.

Security risks

None identified. Paths are Bun-generated cache names, not untrusted input; no new parsing or privilege boundaries.

Level of scrutiny

High. renameat_concurrently_without_fallback is a shared helper reached from move_file_z (bun build --compile, upgrade, test snapshot commit, Tmpfile::finish), the tarball repair path, and bun patch commit/restore. On Windows and on any Linux filesystem lacking RENAME_EXCHANGE, every one of those callers now takes the new loop instead of the old delete+rename. The first-writer-wins switch also changes an invariant other code may depend on. github-actions flagged two possibly-overlapping open PRs (#33979, #33884) that a human should reconcile.

Other factors

All prior review threads (CodeRabbit, comment-cop, my earlier nits) are resolved in the current diff. The test is hermetic (in-process registry, port: 0, tempDir), skips on musl, and the repair case asserts the shim fired via a marker file. The PR description states rust:check-all and the streaming-extract/patch suites pass; the new test itself is Linux-glibc-only so CI is the proof point.

@robobun

robobun commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 7:11 PM PT - Jul 28th, 2026

@robobun, your commit 2f400c3 has some failures in Build #84403 (All Failures)


🧪   To try this PR locally:

bunx bun-pr 36229

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

bun-36229 --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.

No blocking issues found, but this reworks the shared renameat_concurrently_without_fallback fallback (reached from ~10 call sites including move_file_z, Tmpfile::finish, bun build --compile, and upgrade) and changes cache-publish semantics to first-wins — worth a human look at the concurrency reasoning.

Reviewed: the rename-aside loop (retry bound, restore-on-error, aside cleanup on every exit); rename_aside_name NAME_MAX/PATH_MAX guards (my earlier NAME_MAX note is addressed); the keep-existing completeness checks in both extract_tarball.rs and patch_install.rs; and the LD_PRELOAD test (musl skip applied, marker file proves shim injection in the repair test).

Extended reasoning...

Overview

The PR touches four files: src/sys/lib.rs (rewrites the sad-path fallback in renameat_concurrently_without_fallback from delete-then-rename to rename-aside-then-rename-then-delete, adds rename_aside_name), src/install/extract_tarball.rs and src/install/patch_install.rs (both add a first-wins RENAME_NOREPLACE + completeness check ahead of the existing concurrent-rename call), and a new Linux-only LD_PRELOAD-based regression test.

Security risks

None identified. The paths involved are Bun-controlled cache/tempdir names, not user input; the new helper only appends a fixed-shape suffix and both length limits (buffer and NAME_MAX) are guarded. No auth, crypto, or network-trust surface is touched.

Level of scrutiny

High. renameat_concurrently_without_fallback is a shared syscall helper reached from ~10 call sites (move_file_zStandaloneModuleGraph, upgrade_command, test_command, Tmpfile::finish; patchPackage.rs commit/restore; the extract/patch repair paths). The new fallback introduces a retry loop, a random aside name, and best-effort restore semantics that run on every platform without atomic exchange (NFS/FUSE on Linux, and always on Windows since EXCHANGE is cfg-gated out). Changing the cache publish from last-wins to first-wins is a semantic decision that a maintainer should sign off on, even though the PR's argument (entries only appear via atomic rename of a fully extracted tree, so an existing entry is content-identical) is sound and mirrors commit_global_store_entry.

Other factors

Both nits I raised on the previous revision (NAME_MAX guard on the aside basename; isMusl skip so the shim test doesn't vacuously pass on the static-linked lane) have been addressed, and all inline threads from other reviewers are resolved. The bug-hunting pass on the current head found nothing new. The test is hermetic (in-process registry, port: 0, tempDir), skips correctly on non-Linux/musl/no-cc, and the repair test asserts the shim actually injected EOPNOTSUPP via a marker file. What I did not independently verify: behavior on Windows (where the aside fallback now always runs) and macOS (where renameat2(exclude) maps through renameatx_np); those are covered only by the author's rust:check-all and CI.

@robobun

robobun commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator Author

CI status on the latest run (build 84403): 193 of 196 jobs passed. The only hard failure is the darwin 14 x64 test lane hitting its job timeout; every test failure in the annotations is marked flaky and passed when re-run alone (parallel-batch timeouts and timing assertions in suites this PR does not touch: MySQL 28004, spawn-streaming-stdout, fetch-tls-abortsignal-timeout, bunshell, svelte, and similar). The new cache-race and repair tests pass on the Linux lanes.

The diff is ready for review; I am not going to keep re-rolling CI for unrelated flake.

@robobun

robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

One interaction to be aware of when reviewing this: keeping the existing destination unconditionally is not right for file: and URL tarballs. Their cache folder is named after the hash of the path or URL (@T@<hash>@@@1), not the contents, and every install without a lockfile re-extracts them over that folder. With first-wins, a repacked tarball reinstalled after removing bun.lock keeps installing the previous extraction while the new sha512 is written to the lockfile (on main the fresh copy replaces the folder via RENAME_EXCHANGE; what leaks is the displaced folder).

#38702 fixes that leak for tarballs by deciding before extraction whether the folder already existed (CachePublish::Supersede, replace and delete the displaced folder) or not (KeepExisting, first-wins via a keep_existing_destination option on renameat_concurrently). It leaves npm and GitHub publishing as is, so the two changes are complementary; the first-wins path here should apply to tarballs only in the KeepExisting case.

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.

renameat_concurrently_without_fallback delete+rename fallback causes ENOENT for concurrent readers on NFS

1 participant