Repository navigation
bun test: close scanner directory fds once their children are opened #40016
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔴 Under
--watch/--hotthis setsresolver.store_fd = true, but the scanner now caches every walked directory withentries.fd = INVALID(Scanner.rs passesstore_fd = false). When the resolver later walks a test file's directory chain indir_info_cached_maybe_log, it finds the cachedDirEntrywith an invalid fd, opens a fresh fd (resolver.rs:4460), tracks it inopen_dirs[], then hitsneeds_iter = false(cachedgeneration >= self.generation) so the fd is never stored intoentries.fd— and thedefer!at resolver.rs:4423 skips the close becausestore_fd = trueandneed_to_close_files()is false. One fd leaks per test-file directory (and its uncached ancestors) on the first watch run. Fix: at resolver.rs:4634-4643, whenneeds_iter = false && self.store_fdand the cachedentries.fdis invalid, storeopen_dirinto it (or close it).Extended reasoning...
What the bug is
The follow-up commit gates
store_fdon hot-reload:store_fd: ctx.debug.hot_reload != HotReload::None(test_command.rs:2279). Underbun test --watch/--hot,resolver.store_fdis thereforetrue. Meanwhile the scanner now unconditionally passesstore_fd = falsetoread_directory_with_iterator(Scanner.rs:235), so every directory the scanner walks is cached in the resolver'sDirEntrymap withentries.fd = Fd::INVALIDandentries.generation = 0(lib.rs:1318 is skipped whenstore_fdis false).That combination — a scanner-cached
DirEntrywith an invalid fd, plusresolver.store_fd = true— trips a latent hole indir_info_cached_maybe_log(resolver.rs:4326+): a freshly opened directory fd is neither stored into the cache nor closed by the cleanup guard.Step-by-step trace
Take a test file at
<cwd>/sub0/probe.test.tsunderbun test --watch, withresolver.generation = 0(resolver.rs:931).sub0/and callsread_directory_with_iterator(path, Some(fd), 0, /*store_fd*/ false, iter). The resolver cachesDirEntry { dir: "<cwd>/sub0", fd: INVALID, generation: 0, data: {…} }.probe.test.tscallsdir_info_cached("<cwd>/sub0"). TheDirInfocache misses (only theDirEntrycache is populated), sodir_info_cached_maybe_logbuilds the ancestor queue. For thesub0slot, resolver.rs:4347-4353 finds the scanner-cachedDirEntryand setsslot.fd = entries.fd = INVALID,slot.safe_path = entries.dir.queue_top.fdis invalid, so a freshopen_dirfd is opened viaopenat.!queue_top.fd.is_valid() && open_dir.is_valid()→open_diris written intobufs!(open_dirs)[open_dir_count++].entries.get_or_put(dir_path)returns the scanner-cached index (dir_pathisentries.dir, the same interned key the scanner stored under).at_index()returns the cachedEntries(entries);entries.generation (0) >= self.generation (0)→needs_iter = false.if needs_iter { … new_entry.fd = if self.store_fd { open_dir } … }block is skipped.open_diris passed to the innerdir_info_uncached()(resolver.rs:6099+) but only used read-side (openat(fd, ".bin"),fstat); nothing there stores or closes it.defer!— the guard closesopen_dirs[0..n]only whenn > 0 && (!close_dirs_store_fd || need_to_close_files()). Hereclose_dirs_store_fd = trueandneed_to_close_files()(lib.rs:1565) returnsfalsewhilefile_limit > 254 && file_limit > (max_fd+1)*2, which holds after Bun raisesRLIMIT_NOFILE. The condition is!true || false = false→ the fd is never closed.bufs!(open_dirs)is a threadlocal scratch buffer whose live prefix is reset byopen_dir_counton the next call, soopen_diris orphaned: opened, never stored anywhere, never closed.Why existing code doesn't prevent it
The
defer!at resolver.rs:4423 assumes that whenstore_fd = true, every fd inopen_dirs[]was written into someDirEntry.fdat line 4698 and will be reused/closed later. That holds whenneeds_iter = true. Whenneeds_iter = false— aDirEntrycache hit — line 4698 never runs, and the assumption is wrong. Before this PR that state was unreachable frombun test: the scanner cached withstore_fd = true, soentries.fdwas valid, so line 4460 took thequeue_top.fdfast path and no fresh fd was opened or tracked. The PR creates the exact combination (DirEntrycached withfd = INVALID+resolver.store_fd = true) that reaches the hole.Impact
One fd leaks per directory that (a) the scanner cached and (b) later appears in a module-resolution
DirInfowalk — i.e. every test-file directory and each of its ancestors down to the first already-DirInfo-cached dir, on the first--watchrun. Theneed_to_close_files()safety valve (~file_limit/2) caps it, so theOPEN_MAXfailure the PR fixes cannot recur, but these are truly untracked fds — an unpaired acquisition per REVIEW.md's "Pair every acquisition with its release" rule — introduced in a mode this PR explicitly special-cases at line 2279 and does not test (the new test does not run--watch).How to fix
Fix the resolver at the layer that owns the fd: at resolver.rs:4634-4643, when
needs_iter = falseandself.store_fdand the cachedentries.fdis invalid, storeopen_dirinto it (so the fd is cached and reused exactly asstore_fdintends). Alternatively, closeopen_diron that branch, or force thedefer!to close by tracking per-slot whether the fd was stored. Storing it is the correct fix forstore_fd = truesemantics.