mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-08 16:23:57 +00:00
* fix: pin plugin workspace dir for sessions.list to avoid O(rows) memo busting sessions.list was O(rows) slow under concurrent agent/cron load because each row read a process-global active plugin-registry workspace dir that was mutated by other turns between rows. The per-row memo key changed every time, so loadPluginMetadataSnapshot scanned fresh (~100ms per row). Fix: 1. Add AsyncLocalStorage-based workspace dir pinning to runtime-workspace-state.ts — withPinnedActivePluginRegistryWorkspaceDir() snapshots the current workspace dir for the duration of a callback. 2. Wrap listSessionsFromStoreAsync body in the pin so all per-row metadata lookups use a stable memo key. Fixes #90814 * test(plugins): cover request-scoped workspace pins * fix(plugins): pin canonical runtime workspace reads * fix(plugins): preserve workspace pins across reloads --------- Co-authored-by: lsr911 <lsr911@github.com> Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
113 lines
4.0 KiB
TypeScript
113 lines
4.0 KiB
TypeScript
// Verifies request-scoped plugin workspace pins under concurrent registry mutation.
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import { createEmptyPluginRegistry } from "./registry-empty.js";
|
|
import { getActivePluginRegistryWorkspaceDirFromState } from "./runtime-state.js";
|
|
import { withPinnedActivePluginRegistryWorkspaceDir } from "./runtime-workspace-state.js";
|
|
import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "./runtime.js";
|
|
|
|
function setActiveWorkspace(workspaceDir: string): void {
|
|
setActivePluginRegistry(
|
|
createEmptyPluginRegistry(),
|
|
workspaceDir,
|
|
"gateway-bindable",
|
|
workspaceDir,
|
|
);
|
|
}
|
|
|
|
function createDeferred(): { promise: Promise<void>; resolve: () => void } {
|
|
let resolve: (() => void) | undefined;
|
|
const promise = new Promise<void>((done) => {
|
|
resolve = done;
|
|
});
|
|
if (!resolve) {
|
|
throw new Error("Expected deferred resolver");
|
|
}
|
|
return { promise, resolve };
|
|
}
|
|
|
|
describe("runtime workspace state pin", () => {
|
|
afterEach(() => {
|
|
resetPluginRuntimeStateForTest();
|
|
});
|
|
|
|
it("reads the live global workspace dir when no pin is active", () => {
|
|
setActiveWorkspace("/workspace/a");
|
|
expect(getActivePluginRegistryWorkspaceDirFromState()).toBe("/workspace/a");
|
|
|
|
setActiveWorkspace("/workspace/b");
|
|
expect(getActivePluginRegistryWorkspaceDirFromState()).toBe("/workspace/b");
|
|
});
|
|
|
|
it("isolates overlapping pins and restores live state after success", async () => {
|
|
const firstPinned = createDeferred();
|
|
const resumeFirst = createDeferred();
|
|
const firstObserved: Array<string | undefined> = [];
|
|
|
|
setActiveWorkspace("/workspace/a");
|
|
const firstRequest = withPinnedActivePluginRegistryWorkspaceDir(async () => {
|
|
firstObserved.push(getActivePluginRegistryWorkspaceDirFromState());
|
|
firstPinned.resolve();
|
|
await resumeFirst.promise;
|
|
firstObserved.push(getActivePluginRegistryWorkspaceDirFromState());
|
|
});
|
|
await firstPinned.promise;
|
|
|
|
setActiveWorkspace("/workspace/b");
|
|
const secondObserved = await withPinnedActivePluginRegistryWorkspaceDir(async () => {
|
|
const observed = [getActivePluginRegistryWorkspaceDirFromState()];
|
|
setActiveWorkspace("/workspace/c");
|
|
await new Promise<void>((resolve) => {
|
|
setImmediate(resolve);
|
|
});
|
|
observed.push(getActivePluginRegistryWorkspaceDirFromState());
|
|
return observed;
|
|
});
|
|
|
|
resumeFirst.resolve();
|
|
await firstRequest;
|
|
|
|
expect(firstObserved).toEqual(["/workspace/a", "/workspace/a"]);
|
|
expect(secondObserved).toEqual(["/workspace/b", "/workspace/b"]);
|
|
expect(getActivePluginRegistryWorkspaceDirFromState()).toBe("/workspace/c");
|
|
});
|
|
|
|
it("reuses the outer pin for nested scopes", async () => {
|
|
setActiveWorkspace("/workspace/a");
|
|
|
|
await withPinnedActivePluginRegistryWorkspaceDir(async () => {
|
|
setActiveWorkspace("/workspace/b");
|
|
await withPinnedActivePluginRegistryWorkspaceDir(async () => {
|
|
expect(getActivePluginRegistryWorkspaceDirFromState()).toBe("/workspace/a");
|
|
});
|
|
});
|
|
|
|
expect(getActivePluginRegistryWorkspaceDirFromState()).toBe("/workspace/b");
|
|
});
|
|
|
|
it("preserves the pin across module reloads", async () => {
|
|
setActiveWorkspace("/workspace/a");
|
|
|
|
await withPinnedActivePluginRegistryWorkspaceDir(async () => {
|
|
setActiveWorkspace("/workspace/b");
|
|
const reloaded = await import(
|
|
new URL("./runtime-workspace-state.ts?workspace-pin-reload", import.meta.url).href
|
|
);
|
|
|
|
expect(reloaded.getActivePluginRegistryWorkspaceDirFromState()).toBe("/workspace/a");
|
|
});
|
|
});
|
|
|
|
it("propagates rejections without leaking the pin", async () => {
|
|
setActiveWorkspace("/workspace/a");
|
|
|
|
await expect(
|
|
withPinnedActivePluginRegistryWorkspaceDir(async () => {
|
|
setActiveWorkspace("/workspace/b");
|
|
throw new Error("workspace request failed");
|
|
}),
|
|
).rejects.toThrow("workspace request failed");
|
|
|
|
expect(getActivePluginRegistryWorkspaceDirFromState()).toBe("/workspace/b");
|
|
});
|
|
});
|