mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-13 07:21:52 +00:00
refactor: remove plugin file-lock sdk
This commit is contained in:
@@ -449,7 +449,6 @@ releases.
|
||||
| Bounded async task concurrency | `openclaw/plugin-sdk/concurrency-runtime` |
|
||||
| Numeric coercion | `openclaw/plugin-sdk/number-runtime` |
|
||||
| Process-local async lock | `openclaw/plugin-sdk/async-lock-runtime` |
|
||||
| File locks | `openclaw/plugin-sdk/file-lock` |
|
||||
|
||||
Bundled plugins are scanner-guarded against `infra-runtime`, so repo code
|
||||
cannot regress to the broad barrel.
|
||||
|
||||
@@ -258,7 +258,6 @@ focused channel/runtime subpaths, `config-contracts`, `string-coerce-runtime`,
|
||||
| `plugin-sdk/model-session-runtime` | Model/session override helpers such as `applyModelOverrideToSessionEntry` and `resolveAgentMaxConcurrent` |
|
||||
| `plugin-sdk/talk-config-runtime` | Talk provider config resolution helpers |
|
||||
| `plugin-sdk/json-store` | Small JSON state read/write helpers |
|
||||
| `plugin-sdk/file-lock` | Re-entrant file-lock helpers |
|
||||
| `plugin-sdk/persistent-dedupe` | SQLite-backed dedupe cache helpers |
|
||||
| `plugin-sdk/acp-runtime` | ACP runtime/session and reply-dispatch helpers |
|
||||
| `plugin-sdk/acp-runtime-backend` | Lightweight ACP backend registration and reply-dispatch helpers for startup-loaded plugins |
|
||||
|
||||
@@ -377,8 +377,8 @@ sessionId}` and session key context.
|
||||
SQLite transcript stats instead of filesystem stat calls.
|
||||
- Runtime session locks and the standalone legacy `.jsonl.lock` doctor
|
||||
lane have been removed.
|
||||
- The Microsoft Teams runtime barrel no longer re-exports the old plugin SDK
|
||||
file-lock helper; its durable state paths are SQLite-backed.
|
||||
- The Microsoft Teams runtime barrel and public plugin SDK no longer re-export
|
||||
the old file-lock helper; durable plugin state paths are SQLite-backed.
|
||||
- Session age/count pruning and explicit session cleanup have been removed.
|
||||
Doctor owns legacy import; stale sessions are reset or deleted explicitly.
|
||||
- Doctor no longer treats `agents/<agent>/sessions/` as required runtime
|
||||
|
||||
@@ -875,10 +875,6 @@
|
||||
"types": "./dist/plugin-sdk/context-visibility-runtime.d.ts",
|
||||
"default": "./dist/plugin-sdk/context-visibility-runtime.js"
|
||||
},
|
||||
"./plugin-sdk/file-lock": {
|
||||
"types": "./dist/plugin-sdk/file-lock.d.ts",
|
||||
"default": "./dist/plugin-sdk/file-lock.js"
|
||||
},
|
||||
"./plugin-sdk/fetch-runtime": {
|
||||
"types": "./dist/plugin-sdk/fetch-runtime.d.ts",
|
||||
"default": "./dist/plugin-sdk/fetch-runtime.js"
|
||||
|
||||
@@ -195,7 +195,6 @@
|
||||
"channel-route",
|
||||
"channel-targets",
|
||||
"context-visibility-runtime",
|
||||
"file-lock",
|
||||
"fetch-runtime",
|
||||
"runtime-fetch",
|
||||
"response-limit-runtime",
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
export type {
|
||||
FileLockHandle,
|
||||
FileLockOptions,
|
||||
FileLockTimeoutError,
|
||||
} from "../plugin-sdk/file-lock.js";
|
||||
export {
|
||||
acquireFileLock,
|
||||
drainFileLockStateForTest,
|
||||
FILE_LOCK_TIMEOUT_ERROR_CODE,
|
||||
resetFileLockStateForTest,
|
||||
withFileLock,
|
||||
} from "../plugin-sdk/file-lock.js";
|
||||
@@ -1,78 +0,0 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
acquireFileLock,
|
||||
drainFileLockStateForTest,
|
||||
FILE_LOCK_TIMEOUT_ERROR_CODE,
|
||||
resetFileLockStateForTest,
|
||||
} from "./file-lock.js";
|
||||
|
||||
describe("acquireFileLock", () => {
|
||||
let tempDir = "";
|
||||
|
||||
beforeEach(async () => {
|
||||
resetFileLockStateForTest();
|
||||
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-file-lock-"));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await drainFileLockStateForTest();
|
||||
if (tempDir) {
|
||||
await fs.rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("respects the configured retry budget even when stale windows are much larger", async () => {
|
||||
const filePath = path.join(tempDir, "oauth-refresh");
|
||||
const lockPath = `${filePath}.lock`;
|
||||
const options = {
|
||||
retries: {
|
||||
retries: 1,
|
||||
factor: 1,
|
||||
minTimeout: 20,
|
||||
maxTimeout: 20,
|
||||
},
|
||||
stale: 100,
|
||||
} as const;
|
||||
|
||||
await fs.writeFile(
|
||||
lockPath,
|
||||
JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }, null, 2),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
await expect(acquireFileLock(filePath, options)).rejects.toSatisfy((error) => {
|
||||
expect(error).toMatchObject({
|
||||
code: FILE_LOCK_TIMEOUT_ERROR_CODE,
|
||||
});
|
||||
expect((error as { lockPath?: string }).lockPath).toMatch(/oauth-refresh\.lock$/);
|
||||
return true;
|
||||
});
|
||||
}, 5_000);
|
||||
|
||||
it("closes an opened lock handle when writing the owner payload fails", async () => {
|
||||
const filePath = path.join(tempDir, "write-fails");
|
||||
const writeError = new Error("owner write failed");
|
||||
const close = vi.fn().mockResolvedValue(undefined);
|
||||
vi.spyOn(fs, "open").mockResolvedValue({
|
||||
close,
|
||||
writeFile: vi.fn().mockRejectedValue(writeError),
|
||||
} as unknown as Awaited<ReturnType<typeof fs.open>>);
|
||||
|
||||
await expect(
|
||||
acquireFileLock(filePath, {
|
||||
retries: {
|
||||
retries: 0,
|
||||
factor: 1,
|
||||
minTimeout: 1,
|
||||
maxTimeout: 1,
|
||||
},
|
||||
stale: 100,
|
||||
}),
|
||||
).rejects.toThrow(writeError);
|
||||
|
||||
expect(close).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -1,116 +0,0 @@
|
||||
import "../infra/fs-safe-defaults.js";
|
||||
import {
|
||||
acquireFileLock as acquireFsSafeFileLock,
|
||||
drainFileLockManagerForTest,
|
||||
resetFileLockManagerForTest,
|
||||
} from "@openclaw/fs-safe/file-lock";
|
||||
import { isPidAlive } from "../shared/pid-alive.js";
|
||||
|
||||
export type FileLockOptions = {
|
||||
retries: {
|
||||
retries: number;
|
||||
factor: number;
|
||||
minTimeout: number;
|
||||
maxTimeout: number;
|
||||
randomize?: boolean;
|
||||
};
|
||||
stale: number;
|
||||
};
|
||||
|
||||
type LockFilePayload = {
|
||||
pid?: number;
|
||||
createdAt?: string;
|
||||
};
|
||||
|
||||
export type FileLockHandle = {
|
||||
lockPath: string;
|
||||
release: () => Promise<void>;
|
||||
};
|
||||
|
||||
export const FILE_LOCK_TIMEOUT_ERROR_CODE = "file_lock_timeout";
|
||||
|
||||
export type FileLockTimeoutError = Error & {
|
||||
code: typeof FILE_LOCK_TIMEOUT_ERROR_CODE;
|
||||
lockPath: string;
|
||||
};
|
||||
|
||||
const FILE_LOCK_MANAGER_KEY = "openclaw.plugin-sdk.file-lock";
|
||||
|
||||
function readLockPayload(value: Record<string, unknown> | null): LockFilePayload | null {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
pid: typeof value.pid === "number" ? value.pid : undefined,
|
||||
createdAt: typeof value.createdAt === "string" ? value.createdAt : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async function shouldReclaimPluginLock(params: {
|
||||
lockPath: string;
|
||||
payload: Record<string, unknown> | null;
|
||||
staleMs: number;
|
||||
nowMs: number;
|
||||
}): Promise<boolean> {
|
||||
const payload = readLockPayload(params.payload);
|
||||
if (payload?.pid && !isPidAlive(payload.pid)) {
|
||||
return true;
|
||||
}
|
||||
if (payload?.createdAt) {
|
||||
const createdAt = Date.parse(payload.createdAt);
|
||||
return !Number.isFinite(createdAt) || params.nowMs - createdAt > params.staleMs;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function normalizeTimeoutError(err: unknown): never {
|
||||
if ((err as { code?: unknown }).code === FILE_LOCK_TIMEOUT_ERROR_CODE) {
|
||||
throw Object.assign(new Error((err as Error).message), {
|
||||
code: FILE_LOCK_TIMEOUT_ERROR_CODE,
|
||||
lockPath: (err as { lockPath?: string }).lockPath ?? "",
|
||||
}) as FileLockTimeoutError;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
export function resetFileLockStateForTest(): void {
|
||||
resetFileLockManagerForTest(FILE_LOCK_MANAGER_KEY, FILE_LOCK_MANAGER_KEY);
|
||||
}
|
||||
|
||||
export async function drainFileLockStateForTest(): Promise<void> {
|
||||
await drainFileLockManagerForTest(FILE_LOCK_MANAGER_KEY, FILE_LOCK_MANAGER_KEY);
|
||||
}
|
||||
|
||||
/** Acquire a re-entrant process-local file lock backed by a `.lock` sidecar file. */
|
||||
export async function acquireFileLock(
|
||||
filePath: string,
|
||||
options: FileLockOptions,
|
||||
): Promise<FileLockHandle> {
|
||||
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) {
|
||||
return normalizeTimeoutError(err);
|
||||
}
|
||||
}
|
||||
|
||||
/** Run an async callback while holding a file lock, always releasing the lock afterward. */
|
||||
export async function withFileLock<T>(
|
||||
filePath: string,
|
||||
options: FileLockOptions,
|
||||
fn: () => Promise<T>,
|
||||
): Promise<T> {
|
||||
const lock = await acquireFileLock(filePath, options);
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
await lock.release();
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,6 @@ export * from "../infra/approval-native-runtime.ts";
|
||||
export * from "../infra/approval-display-paths.ts";
|
||||
export * from "../infra/plugin-approvals.ts";
|
||||
export * from "../infra/fetch.js";
|
||||
export * from "../infra/file-lock.js";
|
||||
export * from "../infra/format-time/format-duration.ts";
|
||||
export * from "../infra/fs-safe.ts";
|
||||
export * from "../infra/heartbeat-events.ts";
|
||||
|
||||
@@ -24,9 +24,7 @@ type WorkerPluginRuntimeHelpers = {
|
||||
};
|
||||
type WorkerCleanupHelpers = {
|
||||
closeOpenClawAgentDatabasesForTest: typeof import("../src/state/openclaw-agent-db.js").closeOpenClawAgentDatabasesForTest;
|
||||
drainFileLockStateForTest: typeof import("../src/infra/file-lock.js").drainFileLockStateForTest;
|
||||
resetContextWindowCacheForTest: typeof import("../src/agents/context-runtime-state.js").resetContextWindowCacheForTest;
|
||||
resetFileLockStateForTest: typeof import("../src/infra/file-lock.js").resetFileLockStateForTest;
|
||||
resetModelCatalogReadyCacheForTest: typeof import("../src/agents/models-config-state.js").resetModelCatalogReadyCacheForTest;
|
||||
};
|
||||
|
||||
@@ -74,12 +72,9 @@ function loadWorkerCleanupHelpers(): Promise<WorkerCleanupHelpers> {
|
||||
vi.importActual<typeof import("../src/state/openclaw-agent-db.js")>(
|
||||
"../src/state/openclaw-agent-db.js",
|
||||
),
|
||||
vi.importActual<typeof import("../src/infra/file-lock.js")>("../src/infra/file-lock.js"),
|
||||
]).then(([contextRuntimeState, modelsConfigState, agentDb, fileLock]) => ({
|
||||
]).then(([contextRuntimeState, modelsConfigState, agentDb]) => ({
|
||||
closeOpenClawAgentDatabasesForTest: agentDb.closeOpenClawAgentDatabasesForTest,
|
||||
drainFileLockStateForTest: fileLock.drainFileLockStateForTest,
|
||||
resetContextWindowCacheForTest: contextRuntimeState.resetContextWindowCacheForTest,
|
||||
resetFileLockStateForTest: fileLock.resetFileLockStateForTest,
|
||||
resetModelCatalogReadyCacheForTest: modelsConfigState.resetModelCatalogReadyCacheForTest,
|
||||
}));
|
||||
return globalState[WORKER_CLEANUP_HELPERS];
|
||||
@@ -371,13 +366,9 @@ beforeAll(async () => {
|
||||
afterEach(async () => {
|
||||
const {
|
||||
closeOpenClawAgentDatabasesForTest,
|
||||
drainFileLockStateForTest,
|
||||
resetContextWindowCacheForTest,
|
||||
resetFileLockStateForTest,
|
||||
resetModelCatalogReadyCacheForTest,
|
||||
} = await loadWorkerCleanupHelpers();
|
||||
await drainFileLockStateForTest();
|
||||
resetFileLockStateForTest();
|
||||
closeOpenClawAgentDatabasesForTest();
|
||||
resetContextWindowCacheForTest();
|
||||
resetModelCatalogReadyCacheForTest();
|
||||
@@ -385,8 +376,6 @@ afterEach(async () => {
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
const { closeOpenClawAgentDatabasesForTest, drainFileLockStateForTest } =
|
||||
await loadWorkerCleanupHelpers();
|
||||
await drainFileLockStateForTest();
|
||||
const { closeOpenClawAgentDatabasesForTest } = await loadWorkerCleanupHelpers();
|
||||
closeOpenClawAgentDatabasesForTest();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user