mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 10:50:44 +00:00
perf(plugins): cache runtime mirror file decisions
(cherry picked from commit 75df09b9ec)
This commit is contained in:
@@ -70,6 +70,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Plugins/inspector: keep bundled plugin runtime capture quiet and config-tolerant for Codex, memory-lancedb, Feishu, Mattermost, QQBot, and Tlon so plugin-inspector JSON checks can validate the full bundled set. Thanks @vincentkoc.
|
||||
- Slack/auto-reply: keep fully consumed text reset triggers such as `new session` out of `BodyForAgent` after directive cleanup, so configured Slack reset phrases do not leak into the fresh model turn. Fixes #73137. Thanks @neeravmakwana.
|
||||
- Plugins/runtime deps: prune stale retained bundled runtime deps and keep doctor/secret channel contract scans on lightweight artifacts, so disabled bundled channels stop preserving old dependency trees or importing heavy plugin surfaces. Thanks @SymbolStar and @vincentkoc.
|
||||
- Plugins/runtime deps: cache unchanged bundled runtime mirror dist-file materialization decisions and close file-lock handles on owner-write failures, reducing repeated startup chunk scans and avoiding FileHandle-GC recovery stalls. Refs #73532. Thanks @oadiazp and @bstanbury.
|
||||
- Auto-reply: bound the post-run pending tool-result delivery drain with a progress-aware idle timeout, so a never-settling tool-result task no longer leaves the session active forever while slow healthy deliveries can keep draining. Fixes #53889; supersedes #64733 and #73434. Thanks @zijunl and @wujiaming88.
|
||||
- Gateway/startup: start chat channels without waiting for primary model prewarm, keeping model warmup bounded in the background so Slack and other channels come online promptly when provider discovery is slow. Supersedes #73420. Thanks @dorukardahan.
|
||||
- Gateway/install: carry env-backed config SecretRefs such as `channels.discord.token` into generated service environments when they are present only in the installing shell, while keeping gateway auth SecretRefs non-persisted. Fixes #67817; supersedes #73426. Thanks @wdimaculangan and @ztexydt-cqh.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
acquireFileLock,
|
||||
drainFileLockStateForTest,
|
||||
@@ -55,4 +55,28 @@ describe("acquireFileLock", () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -183,10 +183,16 @@ export async function acquireFileLock(
|
||||
for (let attempt = 0; attempt <= options.retries.retries; attempt += 1) {
|
||||
try {
|
||||
const handle = await fs.open(lockPath, "wx");
|
||||
await handle.writeFile(
|
||||
JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }, null, 2),
|
||||
"utf8",
|
||||
);
|
||||
try {
|
||||
await handle.writeFile(
|
||||
JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }, null, 2),
|
||||
"utf8",
|
||||
);
|
||||
} catch (writeError) {
|
||||
await handle.close().catch(() => undefined);
|
||||
await fs.rm(lockPath, { force: true }).catch(() => undefined);
|
||||
throw writeError;
|
||||
}
|
||||
HELD_LOCKS.set(normalizedFile, { count: 1, handle, lockPath });
|
||||
return {
|
||||
lockPath,
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
resolveBundledRuntimeDependencyInstallRootPlan,
|
||||
resolveBundledRuntimeDepsNpmRunner,
|
||||
scanBundledPluginRuntimeDeps,
|
||||
shouldMaterializeBundledRuntimeMirrorDistFile,
|
||||
type BundledRuntimeDepsInstallParams,
|
||||
} from "./bundled-runtime-deps.js";
|
||||
|
||||
@@ -99,11 +100,42 @@ afterEach(() => {
|
||||
spawnMock.mockReset();
|
||||
spawnSyncMock.mockReset();
|
||||
bundledRuntimeDepsActivityTesting.resetBundledRuntimeDepsInstallActivity();
|
||||
bundledRuntimeDepsTesting.clearBundledRuntimeMirrorMaterializeCache();
|
||||
for (const dir of tempDirs.splice(0)) {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe("shouldMaterializeBundledRuntimeMirrorDistFile", () => {
|
||||
it("reuses unchanged root dist file decisions without rereading source", () => {
|
||||
const root = makeTempDir();
|
||||
const sourcePath = path.join(root, "shared-runtime.js");
|
||||
fs.writeFileSync(
|
||||
sourcePath,
|
||||
[
|
||||
`//#region extensions/browser/src/runtime.ts`,
|
||||
`export const marker = "shared-runtime";`,
|
||||
`//#endregion`,
|
||||
"",
|
||||
].join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
const realReadFileSync = fs.readFileSync.bind(fs);
|
||||
let sourceReads = 0;
|
||||
vi.spyOn(fs, "readFileSync").mockImplementation(((target, options) => {
|
||||
if (path.resolve(target.toString()) === path.resolve(sourcePath)) {
|
||||
sourceReads += 1;
|
||||
}
|
||||
return realReadFileSync(target, options as never);
|
||||
}) as typeof fs.readFileSync);
|
||||
|
||||
expect(shouldMaterializeBundledRuntimeMirrorDistFile(sourcePath)).toBe(true);
|
||||
expect(shouldMaterializeBundledRuntimeMirrorDistFile(sourcePath)).toBe(true);
|
||||
|
||||
expect(sourceReads).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveBundledRuntimeDepsNpmRunner", () => {
|
||||
it("uses npm_execpath through node on Windows when available", () => {
|
||||
const runner = resolveBundledRuntimeDepsNpmRunner({
|
||||
|
||||
@@ -77,6 +77,10 @@ const BUNDLED_RUNTIME_MIRROR_IMPORT_SPECIFIER_RE =
|
||||
/(?:^|[;\n])\s*(?:import|export)\s+(?:[^'"()]+?\s+from\s+)?["']([^"']+)["']|\bimport\(\s*["']([^"']+)["']\s*\)|\brequire\(\s*["']([^"']+)["']\s*\)/g;
|
||||
|
||||
const registeredBundledRuntimeDepNodePaths = new Set<string>();
|
||||
const bundledRuntimeMirrorMaterializeCache = new Map<
|
||||
string,
|
||||
{ signature: string; materialize: boolean }
|
||||
>();
|
||||
|
||||
export type BundledRuntimeDepsNpmRunner = {
|
||||
command: string;
|
||||
@@ -84,10 +88,15 @@ export type BundledRuntimeDepsNpmRunner = {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
};
|
||||
|
||||
export function shouldMaterializeBundledRuntimeMirrorDistFile(sourcePath: string): boolean {
|
||||
if (!BUNDLED_RUNTIME_MIRROR_MATERIALIZED_EXTENSIONS.has(path.extname(sourcePath))) {
|
||||
return false;
|
||||
}
|
||||
function clearBundledRuntimeMirrorMaterializeCache(): void {
|
||||
bundledRuntimeMirrorMaterializeCache.clear();
|
||||
}
|
||||
|
||||
function statSignature(stat: Pick<fs.Stats, "dev" | "ino" | "size" | "mtimeMs">): string {
|
||||
return `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeMs}`;
|
||||
}
|
||||
|
||||
function computeBundledRuntimeMirrorDistFileMaterialization(sourcePath: string): boolean {
|
||||
let source: string;
|
||||
try {
|
||||
source = fs.readFileSync(sourcePath, "utf8");
|
||||
@@ -112,6 +121,27 @@ export function shouldMaterializeBundledRuntimeMirrorDistFile(sourcePath: string
|
||||
return true;
|
||||
}
|
||||
|
||||
export function shouldMaterializeBundledRuntimeMirrorDistFile(sourcePath: string): boolean {
|
||||
if (!BUNDLED_RUNTIME_MIRROR_MATERIALIZED_EXTENSIONS.has(path.extname(sourcePath))) {
|
||||
return false;
|
||||
}
|
||||
const cacheKey = path.resolve(sourcePath);
|
||||
let signature: string;
|
||||
try {
|
||||
signature = statSignature(fs.statSync(sourcePath));
|
||||
} catch {
|
||||
bundledRuntimeMirrorMaterializeCache.delete(cacheKey);
|
||||
return false;
|
||||
}
|
||||
const cached = bundledRuntimeMirrorMaterializeCache.get(cacheKey);
|
||||
if (cached?.signature === signature) {
|
||||
return cached.materialize;
|
||||
}
|
||||
const materialize = computeBundledRuntimeMirrorDistFileMaterialization(sourcePath);
|
||||
bundledRuntimeMirrorMaterializeCache.set(cacheKey, { signature, materialize });
|
||||
return materialize;
|
||||
}
|
||||
|
||||
export function materializeBundledRuntimeMirrorDistFile(
|
||||
sourcePath: string,
|
||||
targetPath: string,
|
||||
@@ -403,6 +433,7 @@ function formatRuntimeDepsLockTimeoutMessage(params: {
|
||||
}
|
||||
|
||||
export const __testing = {
|
||||
clearBundledRuntimeMirrorMaterializeCache,
|
||||
formatRuntimeDepsLockTimeoutMessage,
|
||||
shouldRemoveRuntimeDepsLock,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user