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(plugin-kit): limit concurrent file descriptors for adapters (DT-1707) #1192

Merged

Conversation

lihbr
Copy link
Member

@lihbr lihbr commented Oct 25, 2023

Context

On projects with high numbers of slices, file descriptors limit can be reached when reading user's types.

Fixes: nuxt-modules/prismic#201, DT-1707

The Solution

This PR limits the amount of concurrent file descriptors allowed to adapters through their shared plugin-kit/fs helpers.

Important

I'm not really happy with this solution because it only addresses the issue by targeting the most file-system intensive part of Slice Machine. Limit could still be reached elsewhere. Also, determining a limit that works across all OS's is hard and there's no handy package available to dynamically fix it based on the current OS limitations.

Impact / Dependencies

/

How to QA

Try using Slice Machine with a large amount of slices. To help with that, here's an archive containing about 400 slices which should be reaching the limit on most systems: 231025_DT-1707.zip

Checklist before requesting a review

  • I hereby declare my code ready for review.
  • If it is a critical feature, I have added tests.
  • The CI is successful.
  • If there could backward compatibility issues, it has been discussed and planned.

@vercel
Copy link

vercel bot commented Oct 25, 2023

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Updated (UTC)
slice-machine ✅ Ready (Inspect) Visit Preview Oct 26, 2023 1:49pm

@angeloashmore angeloashmore self-requested a review October 25, 2023 22:21
Copy link
Member

@angeloashmore angeloashmore left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are some solutions that handle EMFILE, like graceful-fs and fs-extra (which uses graceful-fs). However, graceful-fs doesn't include fs/promises.

Overall, the current solution seems okay to me. We could iterate on this PR with one of the following options:

  1. Create a drop-in replacement module for fs that uses p-limit. Allow users to override the limit with an environment variable (e.g. SM_FS_LIMIT=256).
  2. Create a drop-in replacement module for fs that uses graceful-fs and node:utils's promisify (see this comment).
  3. Use fs-extra.
  4. Abstract the calls to fsLimit() to individual helpers, like lib/readFile.ts and lib/writeFile.ts.

Of these, Option 1 seems the cleanest to me if we are confident a static concurrency amount will work.

If not, Option 2 would work better, keeping in mind the extra dependency and the extra work needed if we move Slice Machine to the browser.

Option 3 is okay, except that it makes us more dependent on other packages.

Option 4 would result in messy code, in my opinion.

I'd go with Option 1:

// lib/fsLimit.ts

import * as fs from "node:fs/promises";
import pLimit from "p-limit";

/**
 * The parsed number value of the SM_FS_LIMIT environment variable.
 */
const SM_FS_LIMIT = Number.isNaN(Number(process.env.SM_FS_LIMIT))
	? undefined
	: Number(process.env.SM_FS_LIMIT);

/**
 * The maximum number of concurrent file descriptors allowed to adapters to
 * minimize issues like `EMFILE: too many open files`.
 *
 * - MacOS default limit: 2560 (recent), 256 (old)
 * - Windows limit (per process): 2048
 */
const CONCURRENT_FILE_DESCRIPTORS_LIMIT = SM_FS_LIMIT ?? 1024;

/**
 * Limit concurrent file descriptors for adapters.
 */
const fsLimit = pLimit(CONCURRENT_FILE_DESCRIPTORS_LIMIT);

/**
 * Wrap a function with `fsLimit()` to limit concurrent calls. All functions
 * called with `wrapWithFSLimit()` share the same queue.
 *
 * @param fn - The function to wrap.
 *
 * @returns The wrapped function.
 */
const wrapWithFSLimit = <
	// eslint-disable-next-line @typescript-eslint/no-explicit-any
	TFn extends (...args: any[]) => any,
>(
	fn: TFn,
): ((...args: Parameters<TFn>) => Promise<Awaited<ReturnType<TFn>>>) => {
	return (...args) => fsLimit(() => fn(...args));
};

export const access = wrapWithFSLimit(fs.access);
export const appendFile = wrapWithFSLimit(fs.appendFile);
export const chmod = wrapWithFSLimit(fs.chmod);
export const chown = wrapWithFSLimit(fs.chown);
export const copyFile = wrapWithFSLimit(fs.copyFile);
export const cp = wrapWithFSLimit(fs.cp);
export const lchmod = wrapWithFSLimit(fs.lchmod);
export const lchown = wrapWithFSLimit(fs.lchown);
export const link = wrapWithFSLimit(fs.link);
export const lstat = wrapWithFSLimit(fs.lstat);
export const lutimes = wrapWithFSLimit(fs.lutimes);
export const mkdir = wrapWithFSLimit(fs.mkdir);
export const mkdtemp = wrapWithFSLimit(fs.mkdtemp);
export const open = wrapWithFSLimit(fs.open);
export const opendir = wrapWithFSLimit(fs.opendir);
export const readFile = wrapWithFSLimit(fs.readFile);
export const readdir = wrapWithFSLimit(fs.readdir);
export const readlink = wrapWithFSLimit(fs.readlink);
export const realpath = wrapWithFSLimit(fs.realpath);
export const rename = wrapWithFSLimit(fs.rename);
export const rm = wrapWithFSLimit(fs.rm);
export const rmdir = wrapWithFSLimit(fs.rmdir);
export const stat = wrapWithFSLimit(fs.stat);
export const statfs = wrapWithFSLimit(fs.statfs);
export const symlink = wrapWithFSLimit(fs.symlink);
export const truncate = wrapWithFSLimit(fs.truncate);
export const unlink = wrapWithFSLimit(fs.unlink);
export const utimes = wrapWithFSLimit(fs.utimes);
export const watch = wrapWithFSLimit(fs.watch);
export const writeFile = wrapWithFSLimit(fs.writeFile);

Listing each export is preferred over some looped reducer to allow for native ESM handling.

Base automatically changed from dev-next-release to master October 26, 2023 12:58
@xrutayisire xrutayisire changed the base branch from master to dev-next-release October 30, 2023 14:16
Base automatically changed from dev-next-release to master November 10, 2023 14:47
@xrutayisire xrutayisire changed the base branch from master to dev-next-release November 13, 2023 10:54
@xrutayisire
Copy link
Collaborator

Hey, following Angelo comment, what's the status of this PR?

@lihbr
Copy link
Member Author

lihbr commented Nov 13, 2023

Hey @xrutayisire, it's ready to be reviewed.

Copy link
Collaborator

@bapmrl bapmrl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work!

@lihbr lihbr merged commit 2e12a00 into dev-next-release Nov 16, 2023
24 checks passed
@lihbr lihbr deleted the lh/limit-concurrent-file-descriptors-for-adapters branch November 16, 2023 16:41
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.

slicemachine Error : "too many open files"
4 participants