fix: retry file reads when out of file descriptors

Content, include, route-template and search reads now run
concurrently, so large sites can hit EMFILE/ENFILE. Route all async
reads through a shared readFile util that retries with backoff.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pull/5342/head
Divyansh Singh 1 month ago
parent 3372516152
commit da71173afc

@ -1,5 +1,5 @@
import matter from 'gray-matter'
import { readFile, stat } from 'node:fs/promises'
import { stat } from 'node:fs/promises'
import path from 'node:path'
import pMap from 'p-map'
import { normalizePath } from 'vite'
@ -9,6 +9,7 @@ import {
mergeMarkdownLocales
} from './markdown/markdown'
import type { Awaitable, MarkdownEnv } from './shared'
import { readFile } from './utils/fs'
import { glob, normalizeGlob, type GlobOptions } from './utils/glob'
export interface ContentOptions<T = ContentData[]> {
@ -121,7 +122,7 @@ export function createContentLoader<T = ContentData[]>(
if (cached && timestamp === cached.timestamp) return cached.data
const src = await readFile(file, 'utf8')
const src = await readFile(file)
const renderExcerpt = options.excerpt
const { data: frontmatter, excerpt } = matter(

@ -1,5 +1,4 @@
import fs from 'node:fs'
import { readFile } from 'node:fs/promises'
import path from 'node:path'
import c from 'picocolors'
import pm from 'picomatch'
@ -13,6 +12,7 @@ import {
} from 'vite'
import type { Awaitable } from '../shared'
import { type SiteConfig, type UserConfig } from '../siteConfig'
import { readFile } from '../utils/fs'
import { glob, normalizeGlob, type GlobOptions } from '../utils/glob'
import { ModuleGraph } from '../utils/moduleGraph'
import { resolveRewrites } from './rewritesPlugin'
@ -158,7 +158,7 @@ export const dynamicRoutesPlugin = async (
moduleGraph.add(id, [routeFile])
moduleGraph.add(routeFile, [matched.loaderPath])
let baseContent = await readFile(routeFile, 'utf8')
let baseContent = await readFile(routeFile)
// inject raw content
// this is intended for integration with CMS

@ -1,6 +1,5 @@
import { prefixRegex } from '@rolldown/pluginutils'
import MiniSearch from 'minisearch'
import { readFile } from 'node:fs/promises'
import path from 'node:path'
import { createDebug } from 'obug'
import type { Plugin, ViteDevServer } from 'vite'
@ -8,6 +7,7 @@ import type { SiteConfig } from '../config'
import type { DefaultTheme } from '../defaultTheme'
import { createMarkdownRenderer } from '../markdown/markdown'
import { getLocaleForPath, slash, type MarkdownEnv } from '../shared'
import { readFile } from '../utils/fs'
import { processIncludes } from '../utils/processIncludes'
const debug = createDebug('vitepress:local-search')
@ -52,7 +52,7 @@ export async function localSearchPlugin(
const { srcDir, cleanUrls = false } = siteConfig
const relativePath = slash(path.relative(srcDir, file))
const env: MarkdownEnv = { path: file, relativePath, cleanUrls }
const raw = await readFile(file, 'utf8').catch((e) => {
const raw = await readFile(file).catch((e) => {
if (e.code === 'ENOENT') {
debug(`File not found: ${file}`)
return ''

@ -1,9 +1,9 @@
import compression from '@polka/compression'
import { readFile } from 'node:fs/promises'
import path from 'node:path'
import polka, { type IOptions } from 'polka'
import sirv from 'sirv'
import { resolveConfig } from '../config'
import { readFile } from '../utils/fs'
export interface ServeOptions {
base?: string
@ -21,10 +21,7 @@ export async function serve(options: ServeOptions = {}) {
const notAnAsset = (pathname: string) =>
!pathname.includes(`/${config.assetsDir}/`)
const notFound = await readFile(
path.resolve(config.outDir, './404.html'),
'utf8'
)
const notFound = await readFile(path.resolve(config.outDir, './404.html'))
const onNoMatch: IOptions['onNoMatch'] = (req, res) => {
res.statusCode = 404
if (notAnAsset(req.path)) res.write(notFound)

@ -0,0 +1,19 @@
import { readFile as fsReadFile } from 'node:fs/promises'
const retryCodes = new Set(['EMFILE', 'ENFILE'])
/**
* Reads a file as utf8, retrying with backoff when the process is
* temporarily out of file descriptors (EMFILE/ENFILE).
*/
export async function readFile(file: string): Promise<string> {
for (let attempt = 0; ; attempt++) {
try {
return await fsReadFile(file, 'utf8')
} catch (e) {
const code = (e as NodeJS.ErrnoException).code
if (attempt >= 9 || !code || !retryCodes.has(code)) throw e
await new Promise((resolve) => setTimeout(resolve, 2 ** attempt * 10))
}
}
}

@ -1,9 +1,9 @@
import matter from 'gray-matter'
import { replaceAsync, type MarkdownItAsync } from 'markdown-it-async'
import { readFile } from 'node:fs/promises'
import path from 'node:path'
import { findRegion } from '../markdown/plugins/snippet'
import { slash, type MarkdownEnv } from '../shared'
import { readFile } from './fs'
export function processIncludes(
md: MarkdownItAsync,
@ -41,7 +41,7 @@ export function processIncludes(
// chain are cycles, the same file may still be included by siblings
if (includePath === file || ancestors.includes(includePath)) return m
let content = await readFile(includePath, 'utf8')
let content = await readFile(includePath)
if (region) {
const [regionName] = region

Loading…
Cancel
Save