Skip to content

Commit b7f5526

Browse files
authoredMar 7, 2025··
fix: race condition in RPC filesystem cache. (#7531)
1 parent 34aa322 commit b7f5526

File tree

1 file changed

+39
-3
lines changed
  • packages/vitest/src/node/pools

1 file changed

+39
-3
lines changed
 

‎packages/vitest/src/node/pools/rpc.ts

+39-3
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import type { RuntimeRPC } from '../../types/rpc'
33
import type { TestProject } from '../project'
44
import type { ResolveSnapshotPathHandlerContext } from '../types/config'
55
import { mkdirSync } from 'node:fs'
6-
import { writeFile } from 'node:fs/promises'
7-
import { join } from 'pathe'
6+
import { rename, stat, unlink, writeFile } from 'node:fs/promises'
7+
import { dirname, join } from 'pathe'
88
import { hash } from '../hash'
99

1010
const created = new Set()
@@ -65,7 +65,11 @@ export function createMethodsRPC(project: TestProject, options: MethodsOptions =
6565
}
6666
promises.set(
6767
tmp,
68-
writeFile(tmp, code, 'utf-8').finally(() => promises.delete(tmp)),
68+
69+
atomicWriteFile(tmp, code)
70+
// Fallback to non-atomic write for windows case where file already exists:
71+
.catch(() => writeFile(tmp, code, 'utf-8'))
72+
.finally(() => promises.delete(tmp)),
6973
)
7074
await promises.get(tmp)
7175
Object.assign(result, { id: tmp })
@@ -146,3 +150,35 @@ function handleRollupError(e: unknown): never {
146150
}
147151
throw e
148152
}
153+
154+
/**
155+
* Performs an atomic write operation using the write-then-rename pattern.
156+
*
157+
* Why we need this:
158+
* - Ensures file integrity by never leaving partially written files on disk
159+
* - Prevents other processes from reading incomplete data during writes
160+
* - Particularly important for test files where incomplete writes could cause test failures
161+
*
162+
* The implementation writes to a temporary file first, then renames it to the target path.
163+
* This rename operation is atomic on most filesystems (including POSIX-compliant ones),
164+
* guaranteeing that other processes will only ever see the complete file.
165+
*
166+
* Added in https://github.com/vitest-dev/vitest/pull/7531
167+
*/
168+
async function atomicWriteFile(realFilePath: string, data: string): Promise<void> {
169+
const dir = dirname(realFilePath)
170+
const tmpFilePath = join(dir, `.tmp-${Date.now()}-${Math.random().toString(36).slice(2)}`)
171+
172+
try {
173+
await writeFile(tmpFilePath, data, 'utf-8')
174+
await rename(tmpFilePath, realFilePath)
175+
}
176+
finally {
177+
try {
178+
if (await stat(tmpFilePath)) {
179+
await unlink(tmpFilePath)
180+
}
181+
}
182+
catch {}
183+
}
184+
}

0 commit comments

Comments
 (0)
Please sign in to comment.