From 19727da05add7e1fcd09448aaf345ed27c95fd9b Mon Sep 17 00:00:00 2001 From: Dresing Date: Sun, 8 Mar 2026 12:39:49 +0800 Subject: [PATCH] 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 --- src/infra/json-files.ts | 50 ++++++++++++++++++++++++++++++----------- 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/src/infra/json-files.ts b/src/infra/json-files.ts index 15830e9ad4e..5f4126cc13f 100644 --- a/src/infra/json-files.ts +++ b/src/infra/json-files.ts @@ -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(filePath: string): Promise { 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 => { + 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() {