Skip to content

Commit 1d7c775

Browse files
author
nick8799981325
committed
fix: add retry logic for Windows EPERM errors in atomic file writes
1 parent 8bcaf1a commit 1d7c775

File tree

1 file changed

+51
-28
lines changed

1 file changed

+51
-28
lines changed

src/infra/json-files.ts

Lines changed: 51 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { randomUUID } from "node:crypto";
22
import fs from "node:fs/promises";
33
import path from "node:path";
4+
import { setTimeout as sleep } from "node:timers/promises";
5+
6+
const MAX_RETRIES = 5;
7+
const RETRY_BASE_DELAY_MS = 50;
8+
const IS_WINDOWS = process.platform === "win32";
49

510
export async function readJsonFile<T>(filePath: string): Promise<T | null> {
611
try {
@@ -36,40 +41,58 @@ export async function writeTextAtomic(
3641
if (typeof options?.ensureDirMode === "number") {
3742
mkdirOptions.mode = options.ensureDirMode;
3843
}
39-
await fs.mkdir(path.dirname(filePath), mkdirOptions);
4044
const parentDir = path.dirname(filePath);
41-
const tmp = `${filePath}.${randomUUID()}.tmp`;
42-
try {
43-
const tmpHandle = await fs.open(tmp, "w", mode);
44-
try {
45-
await tmpHandle.writeFile(payload, { encoding: "utf8" });
46-
await tmpHandle.sync();
47-
} finally {
48-
await tmpHandle.close().catch(() => undefined);
49-
}
45+
for (let attempt = 1; attempt <= MAX_RETRIES + 1; attempt++) {
46+
await fs.mkdir(parentDir, mkdirOptions);
47+
const tmp = `${filePath}.${randomUUID()}.tmp`;
5048
try {
51-
await fs.chmod(tmp, mode);
52-
} catch {
53-
// best-effort; ignore on platforms without chmod
54-
}
55-
await fs.rename(tmp, filePath);
56-
try {
57-
const dirHandle = await fs.open(parentDir, "r");
49+
const tmpHandle = await fs.open(tmp, "w", mode);
5850
try {
59-
await dirHandle.sync();
51+
await tmpHandle.writeFile(payload, { encoding: "utf8" });
52+
await tmpHandle.sync();
6053
} finally {
61-
await dirHandle.close().catch(() => undefined);
54+
await tmpHandle.close().catch(() => undefined);
6255
}
63-
} catch {
64-
// best-effort; some platforms/filesystems do not support syncing directories.
65-
}
66-
try {
67-
await fs.chmod(filePath, mode);
68-
} catch {
69-
// best-effort; ignore on platforms without chmod
56+
try {
57+
await fs.chmod(tmp, mode);
58+
} catch {
59+
// best-effort; ignore on platforms without chmod
60+
}
61+
await fs.rename(tmp, filePath);
62+
try {
63+
const dirHandle = await fs.open(parentDir, "r");
64+
try {
65+
await dirHandle.sync();
66+
} finally {
67+
await dirHandle.close().catch(() => undefined);
68+
}
69+
} catch {
70+
// best-effort; some platforms/filesystems do not support syncing directories.
71+
}
72+
try {
73+
await fs.chmod(filePath, mode);
74+
} catch {
75+
// best-effort; ignore on platforms without chmod
76+
}
77+
return;
78+
} catch (err) {
79+
// Only retry on Windows; other platforms should fail fast
80+
if (
81+
IS_WINDOWS &&
82+
err &&
83+
typeof err === "object" &&
84+
"code" in err &&
85+
err.code === "EPERM" &&
86+
attempt < MAX_RETRIES + 1
87+
) {
88+
const delay = 2 ** attempt * RETRY_BASE_DELAY_MS;
89+
await sleep(delay);
90+
continue;
91+
}
92+
throw err;
93+
} finally {
94+
await fs.rm(tmp, { force: true }).catch(() => undefined);
7095
}
71-
} finally {
72-
await fs.rm(tmp, { force: true }).catch(() => undefined);
7396
}
7497
}
7598

0 commit comments

Comments
 (0)