Skip to content

Commit 2fa2e90

Browse files
committed
mitigating the problem with unreachable UNC paths by leaving at least one slot available for local operations #1115
1 parent e73ecb3 commit 2fa2e90

File tree

3 files changed

+51
-18
lines changed

3 files changed

+51
-18
lines changed

src/stat.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { defineConfig } from './config'
2+
import { Stats } from 'node:fs'
3+
import { haveTimeout, pendingPromise } from './cross'
4+
import { stat } from 'fs/promises'
5+
6+
const fileTimeout = defineConfig('file_timeout', 3, x => x * 1000)
7+
8+
// since nodejs' UV_THREADPOOL_SIZE is limited, avoid using multiple slots for the same UNC host, and always leave one free for local operations
9+
const poolSize = Number(process.env.UV_THREADPOOL_SIZE || 4)
10+
const previous = new Map<string, Promise<Stats>>() // wrapped promises with haveTimeout
11+
const working = new Set<Promise<Stats>>() // plain stat's promise
12+
export async function statWithTimeout(path: string) {
13+
const uncHost = /^\\\\([^\\]+)\\/.exec(path)?.[1]
14+
if (!uncHost)
15+
return haveTimeout(fileTimeout.compiled(), stat(path))
16+
const busy = process.env.HFS_PARALLEL_UNC ? null : previous.get(uncHost) // by default we serialize requests on the same UNC host, to keep threadpool usage low
17+
const ret = pendingPromise<Stats>()
18+
previous.set(uncHost, ret) // reserve the slot before starting the operation
19+
const err = await busy?.then(() => false, e => e.message === 'timeout' && e) // only timeout error is shared with pending requests
20+
if (err) {
21+
if (previous.get(uncHost) === ret) // but we don't want to block forever, only involve those that were already waiting
22+
previous.delete(uncHost)
23+
ret.reject(err)
24+
return ret
25+
}
26+
while (working.size >= poolSize - 1) // always leave one slot free for local operations
27+
await Promise.race(working.values()).catch(() => {}) // we are assuming UV_THREADPOOL_SIZE > 1, otherwise race() will deadlock
28+
const op = stat(path)
29+
working.add(op)
30+
try {
31+
ret.resolve(await haveTimeout(fileTimeout.compiled(),
32+
op.finally(() => working.delete(op)) ))
33+
}
34+
catch (e) {
35+
ret.reject(e)
36+
}
37+
finally {
38+
if (previous.get(uncHost) === ret)
39+
previous.delete(uncHost)
40+
}
41+
return ret
42+
}

src/util-files.ts

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,18 @@
11
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <[email protected]> - License https://www.gnu.org/licenses/gpl-3.0.txt
22

3-
import { access, mkdir, readFile, stat } from 'fs/promises'
4-
import { Promisable, try_, wait, isWindowsDrive, haveTimeout, splitAt } from './cross'
5-
import { defineConfig } from './config'
3+
import { access, mkdir, readFile } from 'fs/promises'
4+
import { Promisable, try_, wait, isWindowsDrive } from './cross'
65
import { createWriteStream, mkdirSync, watch, ftruncate } from 'fs'
76
import { basename, dirname } from 'path'
87
import glob from 'fast-glob'
98
import { IS_WINDOWS } from './const'
109
import { once } from 'events'
1110
import { Readable } from 'stream'
11+
import { statWithTimeout } from './stat'
1212
// @ts-ignore
1313
import unzipper from 'unzip-stream'
1414

15-
const fileTimeout = defineConfig('file_timeout', 3, x => x * 1000)
16-
17-
// since nodejs' UV_THREADPOOL_SIZE is 4 by default, avoid using multiple slots for the same UNC host. This doesn't solve the problem but at least mitigates it.
18-
const uncStatWaiting = new Map<string, Promise<any>>()
19-
export async function statWithTimeout(path: string) {
20-
const uncHost = path.startsWith('\\\\') && splitAt('\\', path.slice(2))[0]
21-
if (uncHost)
22-
await uncStatWaiting.get(uncHost)
23-
const op = stat(path)
24-
if (uncHost)
25-
uncStatWaiting.set(uncHost, op.finally(() => uncStatWaiting.delete(uncHost)).catch(() => {}))
26-
return haveTimeout(fileTimeout.compiled(), op)
27-
}
15+
export { statWithTimeout }
2816

2917
export async function isDirectory(path: string) {
3018
try { return (await statWithTimeout(path)).isDirectory() }
@@ -174,4 +162,4 @@ export async function parseFile<T>(path: string, parse: (path: string) => T) {
174162

175163
export async function parseFileContent<T>(path: string, parse: (raw: Buffer) => T) {
176164
return parseFile(path, () => readFile(path).then(parse))
177-
}
165+
}

src/vfs.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,10 @@ export async function getNodeByName(name: string, parent: VfsNode) {
172172
}
173173

174174
export let vfs: VfsNode = {}
175-
defineConfig('vfs', vfs).sub(reviewVfs)
175+
defineConfig('vfs', vfs).sub(async x => {
176+
await reviewVfs(x)
177+
console.log('VFS ready')
178+
})
176179

177180
async function reviewVfs(data=vfs) {
178181
await (async function recur(node) {

0 commit comments

Comments
 (0)