Skip to content
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

fix: EMFILE errors #18313

Merged
merged 10 commits into from
Apr 17, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ jobs:
run: node Makefile mocha
- name: Fuzz Test
run: node Makefile fuzz
- name: Test EMFILE Handling
run: npm run test:emfile

test_on_browser:
name: Browser Test
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,6 @@ pnpm-lock.yaml
# Docs site
_site
/docs/src/assets/css

# EMFILE test files
/tests/fixtures/emfile/file*.js
nzakas marked this conversation as resolved.
Show resolved Hide resolved
9 changes: 6 additions & 3 deletions lib/eslint/eslint.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const {
const { pathToFileURL } = require("url");
const { FlatConfigArray } = require("../config/flat-config-array");
const LintResultCache = require("../cli-engine/lint-result-cache");
const { Retrier } = require("@humanwhocodes/retry");

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

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

Expand Down Expand Up @@ -919,7 +922,7 @@ class ESLint {
fixer = message => shouldMessageBeFixed(message, config, fixTypesSet) && originalFix(message);
}

return fs.readFile(filePath, { encoding: "utf8", signal: controller.signal })
return retrier.retry(() => fs.readFile(filePath, { encoding: "utf8", signal: controller.signal })
.then(text => {

// fail immediately if an error occurred in another file
Expand Down Expand Up @@ -949,11 +952,11 @@ class ESLint {
}

return result;
}).catch(error => {
}))
.catch(error => {
controller.abort(error);
throw error;
});

})
);

Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
"test:browser": "node Makefile.js wdio",
"test:cli": "mocha",
"test:fuzz": "node Makefile.js fuzz",
"test:performance": "node Makefile.js perf"
"test:performance": "node Makefile.js perf",
"test:emfile": "node tools/check-emfile-handling.js"
},
"gitHooks": {
"pre-commit": "lint-staged"
Expand Down Expand Up @@ -71,6 +72,7 @@
"@eslint/js": "9.0.0",
"@humanwhocodes/config-array": "^0.12.3",
"@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.2.3",
"@nodelib/fs.walk": "^1.2.8",
"ajv": "^6.12.4",
"chalk": "^4.0.0",
Expand Down
5 changes: 5 additions & 0 deletions tests/fixtures/emfile/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
rules: {
"no-unused-vars": "error"
}
};
106 changes: 106 additions & 0 deletions tools/check-emfile-handling.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/**
* @fileoverview A utility to test that ESLint doesn't crash with EMFILE/ENFILE errors.
* @author Nicholas C. Zakas
*/

"use strict";

//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------

const fs = require("fs");
const { readFile } = require("fs/promises");
const { execSync } = require("child_process");
const os = require("os");

//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------

const OUTPUT_DIRECTORY = "tests/fixtures/emfile";

/*
* Every operating system has a different limit for the number of files that can
* be opened at once. This number is meant to be larger than the default limit
* on most systems.
*
* Linux systems typically start at a count of 1024 and may be increased to 4096.
* MacOS Sonoma v14.4 has a limit of 10496.
* Windows has no hard limit but may be limited by available memory.
*/
const DEFAULT_FILE_COUNT = 15000;
let FILE_COUNT = DEFAULT_FILE_COUNT;

// if the platform isn't windows, get the ulimit to see what the actual limit is
if (os.platform() !== "win32") {
try {
FILE_COUNT = parseInt(execSync("ulimit -n").toString().trim(), 10) + 1;

console.log(`Detected Linux file limit of ${FILE_COUNT}.`);

// if we're on a Mac, make sure the limit isn't high enough to cause a call stack error
if (os.platform() === "darwin") {
FILE_COUNT = Math.min(FILE_COUNT, 100000);
}
} catch {

// ignore error and use default
}
}

/**
* Generates files in a directory.
* @returns {void}
*/
function generateFiles() {

for (let i = 0; i < FILE_COUNT; i++) {
const fileName = `file_${i}.js`;
const fileContent = `// This is file ${i}`;

fs.writeFileSync(`${OUTPUT_DIRECTORY}/${fileName}`, fileContent);
}

}

/**
* Generates an EMFILE error by reading all files in the output directory.
* @returns {Promise<Buffer[]>} A promise that resolves with the contents of all files.
*/
function generateEmFileError() {
return Promise.all(
Array.from({ length: FILE_COUNT }, (_, i) => {
const fileName = `file_${i}.js`;

return readFile(`${OUTPUT_DIRECTORY}/${fileName}`);
})
);
}

//------------------------------------------------------------------------------
// Main
//------------------------------------------------------------------------------

console.log(`Generating ${FILE_COUNT} files in ${OUTPUT_DIRECTORY}...`);
generateFiles();

console.log("Running ESLint...");
execSync(`node bin/eslint.js ${OUTPUT_DIRECTORY} -c ${OUTPUT_DIRECTORY}/eslint.config.js`, { stdio: "inherit" });
console.log("✅ No errors encountered running ESLint.");

console.log("Checking that this number of files would cause an EMFILE error...");
generateEmFileError()
.then(() => {
throw new Error("EMFILE error not encountered.");
})
.catch(error => {
if (error.code === "EMFILE") {
console.log("✅ EMFILE error encountered:", error.message);
} else if (error.code === "ENFILE") {
console.log("✅ ENFILE error encountered:", error.message);
} else {
console.error("❌ Unexpected error encountered:", error.message);
throw error;
}
});