fix: add retry logic for file lock errors (EBUSY, EACCES, EPERM)

Handle file lock errors gracefully when .openclaw directory is synced
via cloud storage services (OneDrive, Dropbox, Google Drive, etc.).
Retry up to 3 times with exponential backoff before failing.

Fixes openclaw/openclaw#39446
This commit is contained in:
Dresing 2026-03-08 12:39:49 +08:00
parent dd8fd98ad4
commit 19727da05a

View File

@ -2,6 +2,10 @@ import { randomUUID } from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
const FILE_LOCK_ERRORS = new Set(["EBUSY", "EACCES", "EPERM"]);
const MAX_RETRIES = 3;
const RETRY_DELAY_MS = 100;
export async function readJsonFile<T>(filePath: string): Promise<T | null> {
try {
const raw = await fs.readFile(filePath, "utf8");
@ -36,24 +40,44 @@ export async function writeTextAtomic(
if (typeof options?.ensureDirMode === "number") {
mkdirOptions.mode = options.ensureDirMode;
}
await fs.mkdir(path.dirname(filePath), mkdirOptions);
const tmp = `${filePath}.${randomUUID()}.tmp`;
try {
await fs.writeFile(tmp, payload, "utf8");
const attemptWrite = async (): Promise<void> => {
await fs.mkdir(path.dirname(filePath), mkdirOptions);
const tmp = `${filePath}.${randomUUID()}.tmp`;
try {
await fs.chmod(tmp, mode);
} catch {
// best-effort; ignore on platforms without chmod
await fs.writeFile(tmp, payload, "utf8");
try {
await fs.chmod(tmp, mode);
} catch {
// best-effort; ignore on platforms without chmod
}
await fs.rename(tmp, filePath);
try {
await fs.chmod(filePath, mode);
} catch {
// best-effort; ignore on platforms without chmod
}
} finally {
await fs.rm(tmp, { force: true }).catch(() => undefined);
}
await fs.rename(tmp, filePath);
};
let lastError: Error | undefined;
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
try {
await fs.chmod(filePath, mode);
} catch {
// best-effort; ignore on platforms without chmod
await attemptWrite();
return;
} catch (err) {
lastError = err as Error;
const errWithCode = err as { code?: string };
if (attempt < MAX_RETRIES - 1 && FILE_LOCK_ERRORS.has(errWithCode.code ?? "")) {
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS * (attempt + 1)));
continue;
}
throw err;
}
} finally {
await fs.rm(tmp, { force: true }).catch(() => undefined);
}
throw lastError;
}
export function createAsyncLock() {