Skip to content

Commit 0d8cf63

Browse files
nzakasfasttime
andauthored
fix: EMFILE errors (#18313)
* fix: EMFILE errors fixes #18301 * Move catch handler * Add intentional EMFILE failure * Use actual limit on Linux systems in test * Adjust emfile test limit * Fix linting error * Fix test for MacOS * Up MacOS limit in test * Move tmp file output directory * Update .gitignore Co-authored-by: Francesco Trotta <github@fasttime.org> --------- Co-authored-by: Francesco Trotta <github@fasttime.org>
1 parent e1ac0b5 commit 0d8cf63

File tree

5 files changed

+125
-4
lines changed

5 files changed

+125
-4
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ jobs:
6565
run: node Makefile mocha
6666
- name: Fuzz Test
6767
run: node Makefile fuzz
68+
- name: Test EMFILE Handling
69+
run: npm run test:emfile
6870

6971
test_on_browser:
7072
name: Browser Test

lib/eslint/eslint.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ const {
4242
const { pathToFileURL } = require("url");
4343
const { FlatConfigArray } = require("../config/flat-config-array");
4444
const LintResultCache = require("../cli-engine/lint-result-cache");
45+
const { Retrier } = require("@humanwhocodes/retry");
4546

4647
/*
4748
* This is necessary to allow overwriting writeFile for testing purposes.
@@ -851,6 +852,8 @@ class ESLint {
851852
errorOnUnmatchedPattern
852853
});
853854
const controller = new AbortController();
855+
const retryCodes = new Set(["ENFILE", "EMFILE"]);
856+
const retrier = new Retrier(error => retryCodes.has(error.code));
854857

855858
debug(`${filePaths.length} files found in: ${Date.now() - startTime}ms`);
856859

@@ -919,7 +922,7 @@ class ESLint {
919922
fixer = message => shouldMessageBeFixed(message, config, fixTypesSet) && originalFix(message);
920923
}
921924

922-
return fs.readFile(filePath, { encoding: "utf8", signal: controller.signal })
925+
return retrier.retry(() => fs.readFile(filePath, { encoding: "utf8", signal: controller.signal })
923926
.then(text => {
924927

925928
// fail immediately if an error occurred in another file
@@ -949,11 +952,11 @@ class ESLint {
949952
}
950953

951954
return result;
952-
}).catch(error => {
955+
}))
956+
.catch(error => {
953957
controller.abort(error);
954958
throw error;
955959
});
956-
957960
})
958961
);
959962

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@
3333
"test:browser": "node Makefile.js wdio",
3434
"test:cli": "mocha",
3535
"test:fuzz": "node Makefile.js fuzz",
36-
"test:performance": "node Makefile.js perf"
36+
"test:performance": "node Makefile.js perf",
37+
"test:emfile": "node tools/check-emfile-handling.js"
3738
},
3839
"gitHooks": {
3940
"pre-commit": "lint-staged"
@@ -71,6 +72,7 @@
7172
"@eslint/js": "9.0.0",
7273
"@humanwhocodes/config-array": "^0.12.3",
7374
"@humanwhocodes/module-importer": "^1.0.1",
75+
"@humanwhocodes/retry": "^0.2.3",
7476
"@nodelib/fs.walk": "^1.2.8",
7577
"ajv": "^6.12.4",
7678
"chalk": "^4.0.0",
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module.exports = {
2+
rules: {
3+
"no-unused-vars": "error"
4+
}
5+
};

tools/check-emfile-handling.js

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/**
2+
* @fileoverview A utility to test that ESLint doesn't crash with EMFILE/ENFILE errors.
3+
* @author Nicholas C. Zakas
4+
*/
5+
6+
"use strict";
7+
8+
//------------------------------------------------------------------------------
9+
// Requirements
10+
//------------------------------------------------------------------------------
11+
12+
const fs = require("fs");
13+
const { readFile } = require("fs/promises");
14+
const { execSync } = require("child_process");
15+
const os = require("os");
16+
17+
//------------------------------------------------------------------------------
18+
// Helpers
19+
//------------------------------------------------------------------------------
20+
21+
const OUTPUT_DIRECTORY = "tmp/emfile-check";
22+
const CONFIG_DIRECTORY = "tests/fixtures/emfile";
23+
24+
/*
25+
* Every operating system has a different limit for the number of files that can
26+
* be opened at once. This number is meant to be larger than the default limit
27+
* on most systems.
28+
*
29+
* Linux systems typically start at a count of 1024 and may be increased to 4096.
30+
* MacOS Sonoma v14.4 has a limit of 10496.
31+
* Windows has no hard limit but may be limited by available memory.
32+
*/
33+
const DEFAULT_FILE_COUNT = 15000;
34+
let FILE_COUNT = DEFAULT_FILE_COUNT;
35+
36+
// if the platform isn't windows, get the ulimit to see what the actual limit is
37+
if (os.platform() !== "win32") {
38+
try {
39+
FILE_COUNT = parseInt(execSync("ulimit -n").toString().trim(), 10) + 1;
40+
41+
console.log(`Detected Linux file limit of ${FILE_COUNT}.`);
42+
43+
// if we're on a Mac, make sure the limit isn't high enough to cause a call stack error
44+
if (os.platform() === "darwin") {
45+
FILE_COUNT = Math.min(FILE_COUNT, 100000);
46+
}
47+
} catch {
48+
49+
// ignore error and use default
50+
}
51+
}
52+
53+
/**
54+
* Generates files in a directory.
55+
* @returns {void}
56+
*/
57+
function generateFiles() {
58+
59+
fs.mkdirSync(OUTPUT_DIRECTORY, { recursive: true });
60+
61+
for (let i = 0; i < FILE_COUNT; i++) {
62+
const fileName = `file_${i}.js`;
63+
const fileContent = `// This is file ${i}`;
64+
65+
fs.writeFileSync(`${OUTPUT_DIRECTORY}/${fileName}`, fileContent);
66+
}
67+
68+
}
69+
70+
/**
71+
* Generates an EMFILE error by reading all files in the output directory.
72+
* @returns {Promise<Buffer[]>} A promise that resolves with the contents of all files.
73+
*/
74+
function generateEmFileError() {
75+
return Promise.all(
76+
Array.from({ length: FILE_COUNT }, (_, i) => {
77+
const fileName = `file_${i}.js`;
78+
79+
return readFile(`${OUTPUT_DIRECTORY}/${fileName}`);
80+
})
81+
);
82+
}
83+
84+
//------------------------------------------------------------------------------
85+
// Main
86+
//------------------------------------------------------------------------------
87+
88+
console.log(`Generating ${FILE_COUNT} files in ${OUTPUT_DIRECTORY}...`);
89+
generateFiles();
90+
91+
console.log("Running ESLint...");
92+
execSync(`node bin/eslint.js ${OUTPUT_DIRECTORY} -c ${CONFIG_DIRECTORY}/eslint.config.js`, { stdio: "inherit" });
93+
console.log("✅ No errors encountered running ESLint.");
94+
95+
console.log("Checking that this number of files would cause an EMFILE error...");
96+
generateEmFileError()
97+
.then(() => {
98+
throw new Error("EMFILE error not encountered.");
99+
})
100+
.catch(error => {
101+
if (error.code === "EMFILE") {
102+
console.log("✅ EMFILE error encountered:", error.message);
103+
} else if (error.code === "ENFILE") {
104+
console.log("✅ ENFILE error encountered:", error.message);
105+
} else {
106+
console.error("❌ Unexpected error encountered:", error.message);
107+
throw error;
108+
}
109+
});

0 commit comments

Comments
 (0)