Skip to content

fix: limit parallel file reads inside walk to 100 - #446

Open
ethernet8023 wants to merge 1 commit into
electron:mainfrom
ethernet8023:fix/walk-bounded-concurrency
Open

fix: limit parallel file reads inside walk to 100#446
ethernet8023 wants to merge 1 commit into
electron:mainfrom
ethernet8023:fix/walk-bounded-concurrency

Conversation

@ethernet8023

Copy link
Copy Markdown

walk optimistically opens a FD for every file and dir it can see. this hits EMFILE if you have a super deep and/or wide directory tree.

we add a simple promise parallelism limiter that prevents more than 100 FDs from opening at once.

Closes #248

walk optimistically opens a FD for every file and dir it can see.
this hits EMFILE if you have a super deep and/or wide directory tree.

we add a simple promise parallelism limiter that prevents more than
100 FDs from opening at once.

Closes electron#248
@ethernet8023
ethernet8023 requested a review from a team as a code owner August 22, 2026 05:36

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

Working with Erick Zhao

Thanks for taking this on — bounding the individual fs operations (rather than the recursive walk calls) is the right shape, and the deep-tree deadlock test is a nice touch. But the limiter as written doesn't actually limit anything, and two of the new walk tests assert the unlimited behavior, so this needs another pass before it fixes #248.

The Test (22.12.x) CI failure is real and reproduces locally: ConcurrencyLimiter > limits promise parallelism to its passed limit fails with expected 50 to be 3. yarn lint also currently fails on src/util.ts (yarn lint:fix should sort out the formatting), and there are a few typos worth cleaning up while you're in there (numRunningPromsies, "lakge", "redccursive", "beacuse").

Details inline.


Generated by Claude Code

Comment thread src/util.ts
}

async run<T>(promise: () => Promise<T>): Promise<T> {
if(this.numRunningPromsies > this.maxRunningPromises) {

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.

numRunningPromsies is never incremented, so this condition is never true and the limiter is a no-op — every task runs immediately (this is why the new unit test fails with peak 50). You need to increment after acquiring a slot, and the comparison needs to be >= (with >, limit+1 tasks can run). One more subtlety: decrementing and then waking a waiter lets a brand-new run() call steal the slot before the waiter resumes, transiently exceeding the limit. Something like this passes both of your unit tests:

async run<T>(task: () => Promise<T>): Promise<T> {
  if (this.numRunningPromises >= this.maxRunningPromises) {
    await new Promise<void>((resolve) => this.waitingPromises.push(resolve));
  }
  this.numRunningPromises++;
  try {
    return await task();
  } finally {
    this.numRunningPromises--;
    this.waitingPromises.shift()?.();
  }
}

Comment thread spec/util-walk.spec.ts

await walk(testWorkingDir);

expect(binaryCheck.peakOpenFiles).toBe(750);

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.

This asserts unlimited concurrency (750 concurrent reads of 750 files) — it only passes because the limiter is currently a no-op, and it will fail once the limiter works. With 750 files and a working limiter this should saturate, so expect(binaryCheck.peakOpenFiles).toBe(MAX_OPEN_FILE_DESCRIPTORS) matches the test's name.

Comment thread spec/util-walk.spec.ts
const result = await walk(testWorkingDir);

expect(binaryCheck.calls).toBe(fileCount);
expect(binaryCheck.peakOpenFiles).toBeLessThanOrEqual(fileCount);

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.

This is vacuous — peak can never exceed the file count. The test title promises "no more than 100", so this should be toBeLessThanOrEqual(MAX_OPEN_FILE_DESCRIPTORS) (already imported above).

Comment thread src/util.ts
// Every single open call holds a file descriptor. and we quickly hit ulimit,
// thus throwing an EMFILE, if you have a very lakge set of files to sign.
// 100 is a very safe number of files to hold open concurrently.
// It's higher than basically every ulimit.

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.

Inverted — 100 is safe because it's lower than basically every ulimit (macOS defaults to 256).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Code sign failed; please retry manually. Error: EMFILE: too many open files, open

1 participant