fix(coding-agent): retry sync lockfile acquisition to prevent false auth errors during parallel startup
Add manual retry loop (10 attempts, 20ms delay) around lockfile.lockSync() in FileAuthStorageBackend and FileSettingsStorage. Previously, concurrent pi processes would fail immediately on ELOCKED, causing auth.json to load as empty and surfacing misleading 'No API key found' errors. fixes #1871
This commit is contained in:
@@ -59,13 +59,40 @@ export class FileAuthStorageBackend implements AuthStorageBackend {
|
||||
}
|
||||
}
|
||||
|
||||
private acquireLockSyncWithRetry(path: string): () => void {
|
||||
const maxAttempts = 10;
|
||||
const delayMs = 20;
|
||||
let lastError: unknown;
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
try {
|
||||
return lockfile.lockSync(path, { realpath: false });
|
||||
} catch (error) {
|
||||
const code =
|
||||
typeof error === "object" && error !== null && "code" in error
|
||||
? String((error as { code?: unknown }).code)
|
||||
: undefined;
|
||||
if (code !== "ELOCKED" || attempt === maxAttempts) {
|
||||
throw error;
|
||||
}
|
||||
lastError = error;
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < delayMs) {
|
||||
// Sleep synchronously to avoid changing callers to async.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw (lastError as Error) ?? new Error("Failed to acquire auth storage lock");
|
||||
}
|
||||
|
||||
withLock<T>(fn: (current: string | undefined) => LockResult<T>): T {
|
||||
this.ensureParentDir();
|
||||
this.ensureFileExists();
|
||||
|
||||
let release: (() => void) | undefined;
|
||||
try {
|
||||
release = lockfile.lockSync(this.authPath, { realpath: false });
|
||||
release = this.acquireLockSyncWithRetry(this.authPath);
|
||||
const current = existsSync(this.authPath) ? readFileSync(this.authPath, "utf-8") : undefined;
|
||||
const { result, next } = fn(current);
|
||||
if (next !== undefined) {
|
||||
|
||||
@@ -146,6 +146,33 @@ export class FileSettingsStorage implements SettingsStorage {
|
||||
this.projectSettingsPath = join(cwd, CONFIG_DIR_NAME, "settings.json");
|
||||
}
|
||||
|
||||
private acquireLockSyncWithRetry(path: string): () => void {
|
||||
const maxAttempts = 10;
|
||||
const delayMs = 20;
|
||||
let lastError: unknown;
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
try {
|
||||
return lockfile.lockSync(path, { realpath: false });
|
||||
} catch (error) {
|
||||
const code =
|
||||
typeof error === "object" && error !== null && "code" in error
|
||||
? String((error as { code?: unknown }).code)
|
||||
: undefined;
|
||||
if (code !== "ELOCKED" || attempt === maxAttempts) {
|
||||
throw error;
|
||||
}
|
||||
lastError = error;
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < delayMs) {
|
||||
// Sleep synchronously to avoid changing callers to async.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw (lastError as Error) ?? new Error("Failed to acquire settings lock");
|
||||
}
|
||||
|
||||
withLock(scope: SettingsScope, fn: (current: string | undefined) => string | undefined): void {
|
||||
const path = scope === "global" ? this.globalSettingsPath : this.projectSettingsPath;
|
||||
const dir = dirname(path);
|
||||
@@ -155,7 +182,7 @@ export class FileSettingsStorage implements SettingsStorage {
|
||||
// Only create directory and lock if file exists or we need to write
|
||||
const fileExists = existsSync(path);
|
||||
if (fileExists) {
|
||||
release = lockfile.lockSync(path, { realpath: false });
|
||||
release = this.acquireLockSyncWithRetry(path);
|
||||
}
|
||||
const current = fileExists ? readFileSync(path, "utf-8") : undefined;
|
||||
const next = fn(current);
|
||||
@@ -165,7 +192,7 @@ export class FileSettingsStorage implements SettingsStorage {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
if (!release) {
|
||||
release = lockfile.lockSync(path, { realpath: false });
|
||||
release = this.acquireLockSyncWithRetry(path);
|
||||
}
|
||||
writeFileSync(path, next, "utf-8");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user