Skip to content

Commit cae29a0

Browse files
committed
refactor(agents): share sync storage lock retries
1 parent 8963a89 commit cae29a0

3 files changed

Lines changed: 26 additions & 58 deletions

File tree

src/agents/sessions/auth-storage.ts

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import type {
2323
} from "../../llm/utils/oauth/types.js";
2424
import { getAgentDir } from "../config.js";
2525
import { resolveConfigValue } from "./resolve-config-value.js";
26+
import { acquireLockSyncWithRetry } from "./storage-lock.js";
2627

2728
export type ApiKeyCredential = {
2829
type: "api_key";
@@ -93,40 +94,13 @@ export class FileAuthStorageBackend implements AuthStorageBackend {
9394
});
9495
}
9596

96-
private acquireLockSyncWithRetry(path: string): () => void {
97-
const maxAttempts = 10;
98-
const delayMs = 20;
99-
let lastError: unknown;
100-
101-
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
102-
try {
103-
return lockfile.lockSync(path, { realpath: false });
104-
} catch (error) {
105-
const code =
106-
typeof error === "object" && error !== null && "code" in error
107-
? String((error as { code?: unknown }).code)
108-
: undefined;
109-
if (code !== "ELOCKED" || attempt === maxAttempts) {
110-
throw error;
111-
}
112-
lastError = error;
113-
const start = Date.now();
114-
while (Date.now() - start < delayMs) {
115-
// Sleep synchronously to avoid changing callers to async.
116-
}
117-
}
118-
}
119-
120-
throw (lastError as Error) ?? new Error("Failed to acquire auth storage lock");
121-
}
122-
12397
withLock<T>(fn: (current: string | undefined) => LockResult<T>): T {
12498
this.ensureParentDir();
12599
this.ensureFileExists();
126100

127101
let release: (() => void) | undefined;
128102
try {
129-
release = this.acquireLockSyncWithRetry(this.authPath);
103+
release = acquireLockSyncWithRetry(this.authPath);
130104
const current = existsSync(this.authPath) ? readFileSync(this.authPath, "utf-8") : undefined;
131105
const { result, next } = fn(current);
132106
if (next !== undefined) {

src/agents/sessions/settings-manager.ts

Lines changed: 3 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
77
import { homedir } from "node:os";
88
import { dirname, join } from "node:path";
99
import { resolveIntegerOption } from "@openclaw/normalization-core/number-coercion";
10-
import lockfile from "proper-lockfile";
1110
import { mergeDeep } from "../../infra/deep-merge.js";
1211
import type { Transport } from "../../llm/types.js";
1312
import { CONFIG_DIR_NAME, getAgentDir } from "../config.js";
1413
import { DEFAULT_HTTP_IDLE_TIMEOUT_MS, parseHttpIdleTimeoutMs } from "./http-dispatcher.js";
14+
import { acquireLockSyncWithRetry } from "./storage-lock.js";
1515

1616
interface CompactionSettings {
1717
enabled?: boolean; // default: true
@@ -147,33 +147,6 @@ export class FileSettingsStorage implements SettingsStorage {
147147
this.projectSettingsPath = join(cwd, CONFIG_DIR_NAME, "settings.json");
148148
}
149149

150-
private acquireLockSyncWithRetry(path: string): () => void {
151-
const maxAttempts = 10;
152-
const delayMs = 20;
153-
let lastError: unknown;
154-
155-
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
156-
try {
157-
return lockfile.lockSync(path, { realpath: false });
158-
} catch (error) {
159-
const code =
160-
typeof error === "object" && error !== null && "code" in error
161-
? String((error as { code?: unknown }).code)
162-
: undefined;
163-
if (code !== "ELOCKED" || attempt === maxAttempts) {
164-
throw error;
165-
}
166-
lastError = error;
167-
const start = Date.now();
168-
while (Date.now() - start < delayMs) {
169-
// Sleep synchronously to avoid changing callers to async.
170-
}
171-
}
172-
}
173-
174-
throw (lastError as Error) ?? new Error("Failed to acquire settings lock");
175-
}
176-
177150
withLock(scope: SettingsScope, fn: (current: string | undefined) => string | undefined): void {
178151
const path = scope === "global" ? this.globalSettingsPath : this.projectSettingsPath;
179152
const dir = dirname(path);
@@ -183,7 +156,7 @@ export class FileSettingsStorage implements SettingsStorage {
183156
// Only create directory and lock if file exists or we need to write
184157
const fileExists = existsSync(path);
185158
if (fileExists) {
186-
release = this.acquireLockSyncWithRetry(path);
159+
release = acquireLockSyncWithRetry(path);
187160
}
188161
const current = fileExists ? readFileSync(path, "utf-8") : undefined;
189162
const next = fn(current);
@@ -193,7 +166,7 @@ export class FileSettingsStorage implements SettingsStorage {
193166
mkdirSync(dir, { recursive: true });
194167
}
195168
if (!release) {
196-
release = this.acquireLockSyncWithRetry(path);
169+
release = acquireLockSyncWithRetry(path);
197170
}
198171
writeFileSync(path, next, "utf-8");
199172
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import lockfile from "proper-lockfile";
2+
3+
const MAX_LOCK_ATTEMPTS = 10;
4+
const LOCK_RETRY_DELAY_MS = 20;
5+
6+
export function acquireLockSyncWithRetry(path: string): () => void {
7+
for (let attempt = 1; ; attempt++) {
8+
try {
9+
return lockfile.lockSync(path, { realpath: false });
10+
} catch (error) {
11+
if ((error as NodeJS.ErrnoException).code !== "ELOCKED" || attempt === MAX_LOCK_ATTEMPTS) {
12+
throw error;
13+
}
14+
}
15+
16+
const start = Date.now();
17+
while (Date.now() - start < LOCK_RETRY_DELAY_MS) {
18+
// proper-lockfile rejects sync retries; preserve the bounded sync storage contract.
19+
}
20+
}
21+
}

0 commit comments

Comments
 (0)