fix(File Renames): Retry renames for windows EPERM issue (#7645)

* Retry renames for windows EPERM issue

* use async version and manage temporary errors only

* windows only rename magic
This commit is contained in:
James Gatz 2024-07-03 11:24:18 +02:00 committed by GitHub
parent 9384139867
commit 53421aa72e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 66 additions and 1 deletions

View File

@ -2,6 +2,7 @@ import fs from 'fs/promises';
import path from 'path';
import type { BaseDriver } from './base';
import { gracefulRename } from './graceful-rename';
export default class FileSystemDriver implements BaseDriver {
_directory: string;
@ -43,7 +44,7 @@ export default class FileSystemDriver implements BaseDriver {
// file (non-atomic) then renaming the file to the final value (atomic)
try {
await fs.writeFile(tmpPath, value, 'utf8');
await fs.rename(tmpPath, finalPath);
await gracefulRename(tmpPath, finalPath);
} catch (err) {
console.error(`[FileSystemDriver] Failed to write to ${tmpPath} then rename to ${finalPath}`, err);
throw err;

View File

@ -0,0 +1,64 @@
import fs from 'fs/promises';
import { isWindows } from '../../../common/constants';
// Based on node-graceful-fs and vs-code's take on renaming files in a way that is more resilient to Windows locking renames
// https://github.com/microsoft/vscode/pull/188899/files#diff-2bf233effbb62ea789bb7c4739d222a43ccd97ed9f1219f75bb07e9dee91c1a7R529
// On Windows, A/V software can lock the directory, causing this
// to fail with an EACCES or EPERM if the directory contains newly
// created files.
const WINDOWS_RENAME_TIMEOUT = 60000; // 1 minute
function wait(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function renameWithRetry(source: string, target: string, startTime: number, retryTimeout: number, attempt = 0): Promise<void> {
try {
return await fs.rename(source, target);
} catch (error) {
if (error.code !== 'EACCES' && error.code !== 'EPERM' && error.code !== 'EBUSY') {
// only for errors we think are temporary
throw error;
}
if (Date.now() - startTime >= retryTimeout) {
console.error(`[node.js fs] rename failed after ${attempt} retries with error: ${error}`);
// give up after configurable timeout
throw error;
}
if (attempt === 0) {
let abortRetry = false;
try {
const stat = await fs.stat(target);
if (!stat.isFile()) {
abortRetry = true; // if target is not a file, EPERM error may be raised and we should not attempt to retry
}
} catch (error) {
// Ignore
}
if (abortRetry) {
throw error;
}
}
// Delay with incremental backoff up to 100ms
await wait(Math.min(100, attempt * 10));
// Attempt again
return renameWithRetry(source, target, startTime, retryTimeout, attempt + 1);
}
}
export async function gracefulRename(
from: string,
to: string,
) {
if (isWindows()) {
return renameWithRetry(from, to, Date.now(), WINDOWS_RENAME_TIMEOUT);
}
return fs.rename(from, to);
}