refactor(auth): use fs-safe stale lock recovery

This commit is contained in:
Peter Steinberger
2026-05-14 08:00:36 +01:00
parent 04afd114bb
commit e7ae306aa1
5 changed files with 38 additions and 113 deletions

View File

@@ -1766,7 +1766,10 @@
"@lydell/node-pty": "1.2.0-beta.12",
"@modelcontextprotocol/sdk": "1.29.0",
"@mozilla/readability": "0.6.0",
"@openclaw/fs-safe": "0.2.3",
"@openclaw/fs-safe": "0.2.4",
"@slack/bolt": "4.7.2",
"@slack/types": "2.21.1",
"@slack/web-api": "7.15.2",
"ajv": "8.20.0",
"chalk": "5.6.2",
"chokidar": "5.0.0",

19
pnpm-lock.yaml generated
View File

@@ -84,8 +84,17 @@ importers:
specifier: 0.6.0
version: 0.6.0
'@openclaw/fs-safe':
specifier: 0.2.3
version: 0.2.3
specifier: 0.2.4
version: 0.2.4
'@slack/bolt':
specifier: 4.7.2
version: 4.7.2(@types/express@5.0.6)
'@slack/types':
specifier: 2.21.1
version: 2.21.1
'@slack/web-api':
specifier: 7.15.2
version: 7.15.2
ajv:
specifier: 8.20.0
version: 8.20.0
@@ -3325,8 +3334,8 @@ packages:
cpu: [x64]
os: [win32]
'@openclaw/fs-safe@0.2.3':
resolution: {integrity: sha512-O8AJ/ZiLbBQvpxYyXrZuyIFFUG2N2Oz6uhle5Gn1gVGkgT+Qmx7O1A9tbE2pTZs2Cyk75A31H0F8w34CL2X6gg==}
'@openclaw/fs-safe@0.2.4':
resolution: {integrity: sha512-Fo3WTQhxu0asD/rZqIKBqhX6fuZfjyHxSW5yTKfcRx+D9BRAcz0AGoVh+3ur/4XRvZkvsh3Ud8XTw006yRYLgg==}
engines: {node: '>=20.11'}
'@opentelemetry/api-logs@0.217.0':
@@ -10004,7 +10013,7 @@ snapshots:
'@openai/codex@0.130.0-win32-x64':
optional: true
'@openclaw/fs-safe@0.2.3':
'@openclaw/fs-safe@0.2.4':
optionalDependencies:
jszip: 3.10.1
tar: 7.5.15

View File

@@ -7,7 +7,7 @@ packages:
minimumReleaseAge: 2880
minimumReleaseAgeExclude:
- "@openclaw/fs-safe@0.2.3"
- "@openclaw/fs-safe@0.2.4"
- "acpx"
- "tokenjuice"
- "@agentclientprotocol/sdk"

View File

@@ -1,11 +1,5 @@
import fs from "node:fs/promises";
import { isPidDefinitelyDead as defaultIsPidDefinitelyDead } from "../shared/pid-alive.js";
export type LockFileSnapshot = {
raw: string;
payload: Record<string, unknown> | null;
};
export type LockFileOwnerPayload = {
pid?: number;
createdAt?: string;
@@ -23,31 +17,6 @@ export function readLockFileOwnerPayload(
};
}
export async function readLockFileSnapshot(lockPath: string): Promise<LockFileSnapshot | null> {
let raw: string;
try {
raw = await fs.readFile(lockPath, "utf8");
} catch (err) {
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
return null;
}
throw err;
}
try {
const parsed = JSON.parse(raw) as unknown;
return {
raw,
payload:
parsed && typeof parsed === "object" && !Array.isArray(parsed)
? (parsed as Record<string, unknown>)
: null,
};
} catch {
return { raw, payload: null };
}
}
export function shouldRemoveDeadOwnerOrExpiredLock(params: {
payload: Record<string, unknown> | null;
staleMs: number;
@@ -64,40 +33,3 @@ export function shouldRemoveDeadOwnerOrExpiredLock(params: {
}
return false;
}
export async function removeLockFileIfSnapshotMatches(params: {
lockPath: string;
snapshot: LockFileSnapshot;
}): Promise<boolean> {
const current = await readLockFileSnapshot(params.lockPath);
if (!current) {
return true;
}
if (current.raw !== params.snapshot.raw) {
return false;
}
try {
await fs.unlink(params.lockPath);
return true;
} catch (err) {
return (err as NodeJS.ErrnoException).code === "ENOENT";
}
}
export async function removeReportedStaleLockIfStillStale(params: {
lockPath: string;
shouldRemove: (snapshot: LockFileSnapshot) => boolean | Promise<boolean>;
}): Promise<boolean> {
const snapshot = await readLockFileSnapshot(params.lockPath);
if (!snapshot) {
return true;
}
if (!(await params.shouldRemove(snapshot))) {
return false;
}
return await removeLockFileIfSnapshotMatches({
lockPath: params.lockPath,
snapshot,
});
}

View File

@@ -4,10 +4,7 @@ import {
drainFileLockManagerForTest,
resetFileLockManagerForTest,
} from "@openclaw/fs-safe/file-lock";
import {
removeReportedStaleLockIfStillStale,
shouldRemoveDeadOwnerOrExpiredLock,
} from "../infra/stale-lock-file.js";
import { shouldRemoveDeadOwnerOrExpiredLock } from "../infra/stale-lock-file.js";
export type FileLockOptions = {
retries: {
@@ -53,10 +50,6 @@ async function shouldReclaimPluginLock(params: {
});
}
function isFileLockError(error: unknown, code: string): boolean {
return (error as { code?: unknown } | null)?.code === code;
}
function normalizeLockError(err: unknown): never {
if ((err as { code?: unknown }).code === FILE_LOCK_TIMEOUT_ERROR_CODE) {
throw Object.assign(new Error((err as Error).message), {
@@ -86,36 +79,24 @@ export async function acquireFileLock(
filePath: string,
options: FileLockOptions,
): Promise<FileLockHandle> {
while (true) {
try {
const lock = await acquireFsSafeFileLock(filePath, {
managerKey: FILE_LOCK_MANAGER_KEY,
staleMs: options.stale,
retry: options.retries,
allowReentrant: true,
payload: () => ({ pid: process.pid, createdAt: new Date().toISOString() }),
shouldReclaim: shouldReclaimPluginLock,
});
return { lockPath: lock.lockPath, release: lock.release };
} catch (err) {
if (isFileLockError(err, FILE_LOCK_STALE_ERROR_CODE)) {
const lockPath = (err as { lockPath?: string }).lockPath;
if (
lockPath &&
(await removeReportedStaleLockIfStillStale({
lockPath,
shouldRemove: (snapshot) =>
shouldRemoveDeadOwnerOrExpiredLock({
payload: snapshot.payload,
staleMs: options.stale,
}),
}))
) {
continue;
}
}
return normalizeLockError(err);
}
try {
const lock = await acquireFsSafeFileLock(filePath, {
managerKey: FILE_LOCK_MANAGER_KEY,
staleMs: options.stale,
retry: options.retries,
staleRecovery: "remove-if-unchanged",
allowReentrant: true,
payload: () => ({ pid: process.pid, createdAt: new Date().toISOString() }),
shouldReclaim: shouldReclaimPluginLock,
shouldRemoveStaleLock: (snapshot) =>
shouldRemoveDeadOwnerOrExpiredLock({
payload: snapshot.payload,
staleMs: options.stale,
}),
});
return { lockPath: lock.lockPath, release: lock.release };
} catch (err) {
return normalizeLockError(err);
}
}