mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 18:50:42 +00:00
refactor(plugins): share lookup cache eviction
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import { PluginLruCache } from "./plugin-lru-cache.js";
|
||||
|
||||
export class PluginLoadReentryError extends Error {
|
||||
readonly cacheKey: string;
|
||||
|
||||
@@ -9,27 +11,20 @@ export class PluginLoadReentryError extends Error {
|
||||
}
|
||||
|
||||
export class PluginLoaderCacheState<T> {
|
||||
readonly #defaultMaxEntries: number;
|
||||
#maxEntries: number;
|
||||
readonly #registryCache = new Map<string, T>();
|
||||
readonly #registryCache: PluginLruCache<T>;
|
||||
readonly #inFlightLoads = new Set<string>();
|
||||
readonly #openAllowlistWarningCache = new Set<string>();
|
||||
|
||||
constructor(defaultMaxEntries: number) {
|
||||
this.#defaultMaxEntries = Math.max(1, Math.floor(defaultMaxEntries));
|
||||
this.#maxEntries = this.#defaultMaxEntries;
|
||||
this.#registryCache = new PluginLruCache<T>(defaultMaxEntries);
|
||||
}
|
||||
|
||||
get maxEntries(): number {
|
||||
return this.#maxEntries;
|
||||
return this.#registryCache.maxEntries;
|
||||
}
|
||||
|
||||
setMaxEntriesForTest(value?: number): void {
|
||||
this.#maxEntries =
|
||||
typeof value === "number" && Number.isFinite(value) && value > 0
|
||||
? Math.max(1, Math.floor(value))
|
||||
: this.#defaultMaxEntries;
|
||||
this.#evictOldestEntries();
|
||||
this.#registryCache.setMaxEntriesForTest(value);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
@@ -39,21 +34,11 @@ export class PluginLoaderCacheState<T> {
|
||||
}
|
||||
|
||||
get(cacheKey: string): T | undefined {
|
||||
const cached = this.#registryCache.get(cacheKey);
|
||||
if (!cached) {
|
||||
return undefined;
|
||||
}
|
||||
this.#registryCache.delete(cacheKey);
|
||||
this.#registryCache.set(cacheKey, cached);
|
||||
return cached;
|
||||
return this.#registryCache.get(cacheKey);
|
||||
}
|
||||
|
||||
set(cacheKey: string, state: T): void {
|
||||
if (this.#registryCache.has(cacheKey)) {
|
||||
this.#registryCache.delete(cacheKey);
|
||||
}
|
||||
this.#registryCache.set(cacheKey, state);
|
||||
this.#evictOldestEntries();
|
||||
}
|
||||
|
||||
isLoadInFlight(cacheKey: string): boolean {
|
||||
@@ -78,14 +63,4 @@ export class PluginLoaderCacheState<T> {
|
||||
recordOpenAllowlistWarning(cacheKey: string): void {
|
||||
this.#openAllowlistWarningCache.add(cacheKey);
|
||||
}
|
||||
|
||||
#evictOldestEntries(): void {
|
||||
while (this.#registryCache.size > this.#maxEntries) {
|
||||
const oldestEntry = this.#registryCache.keys().next();
|
||||
if (oldestEntry.done) {
|
||||
break;
|
||||
}
|
||||
this.#registryCache.delete(oldestEntry.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
42
src/plugins/plugin-lru-cache.test.ts
Normal file
42
src/plugins/plugin-lru-cache.test.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { PluginLruCache } from "./plugin-lru-cache.js";
|
||||
|
||||
describe("PluginLruCache", () => {
|
||||
it("evicts the least recently used entry", () => {
|
||||
const cache = new PluginLruCache<string>(2);
|
||||
|
||||
cache.set("", "empty");
|
||||
cache.set("a", "alpha");
|
||||
cache.set("b", "bravo");
|
||||
expect(cache.get("a")).toBe("alpha");
|
||||
|
||||
cache.set("c", "charlie");
|
||||
|
||||
expect(cache.get("b")).toBeUndefined();
|
||||
expect(cache.get("a")).toBe("alpha");
|
||||
expect(cache.get("c")).toBe("charlie");
|
||||
});
|
||||
|
||||
it("returns hit state for cached null values", () => {
|
||||
const cache = new PluginLruCache<string | null>(2);
|
||||
|
||||
cache.set("missing", null);
|
||||
|
||||
expect(cache.getResult("missing")).toEqual({ hit: true, value: null });
|
||||
expect(cache.getResult("unknown")).toEqual({ hit: false });
|
||||
});
|
||||
|
||||
it("resizes and falls back to the default max entry count", () => {
|
||||
const cache = new PluginLruCache<string>(2);
|
||||
|
||||
cache.setMaxEntriesForTest(1.9);
|
||||
cache.set("a", "alpha");
|
||||
cache.set("b", "bravo");
|
||||
expect(cache.maxEntries).toBe(1);
|
||||
expect(cache.size).toBe(1);
|
||||
expect(cache.get("a")).toBeUndefined();
|
||||
|
||||
cache.setMaxEntriesForTest();
|
||||
expect(cache.maxEntries).toBe(2);
|
||||
});
|
||||
});
|
||||
72
src/plugins/plugin-lru-cache.ts
Normal file
72
src/plugins/plugin-lru-cache.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
export type PluginLruCacheResult<T> = { hit: true; value: T } | { hit: false };
|
||||
|
||||
export class PluginLruCache<T> {
|
||||
readonly #defaultMaxEntries: number;
|
||||
#maxEntries: number;
|
||||
readonly #entries = new Map<string, T>();
|
||||
|
||||
constructor(defaultMaxEntries: number) {
|
||||
this.#defaultMaxEntries = normalizeMaxEntries(defaultMaxEntries, 1);
|
||||
this.#maxEntries = this.#defaultMaxEntries;
|
||||
}
|
||||
|
||||
get maxEntries(): number {
|
||||
return this.#maxEntries;
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this.#entries.size;
|
||||
}
|
||||
|
||||
setMaxEntriesForTest(value?: number): void {
|
||||
this.#maxEntries =
|
||||
typeof value === "number"
|
||||
? normalizeMaxEntries(value, this.#defaultMaxEntries)
|
||||
: this.#defaultMaxEntries;
|
||||
this.#evictOldestEntries();
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.#entries.clear();
|
||||
}
|
||||
|
||||
get(cacheKey: string): T | undefined {
|
||||
const cached = this.getResult(cacheKey);
|
||||
return cached.hit ? cached.value : undefined;
|
||||
}
|
||||
|
||||
getResult(cacheKey: string): PluginLruCacheResult<T> {
|
||||
if (!this.#entries.has(cacheKey)) {
|
||||
return { hit: false };
|
||||
}
|
||||
const cached = this.#entries.get(cacheKey) as T;
|
||||
this.#entries.delete(cacheKey);
|
||||
this.#entries.set(cacheKey, cached);
|
||||
return { hit: true, value: cached };
|
||||
}
|
||||
|
||||
set(cacheKey: string, value: T): void {
|
||||
if (this.#entries.has(cacheKey)) {
|
||||
this.#entries.delete(cacheKey);
|
||||
}
|
||||
this.#entries.set(cacheKey, value);
|
||||
this.#evictOldestEntries();
|
||||
}
|
||||
|
||||
#evictOldestEntries(): void {
|
||||
while (this.#entries.size > this.#maxEntries) {
|
||||
const oldestEntry = this.#entries.keys().next();
|
||||
if (oldestEntry.done) {
|
||||
break;
|
||||
}
|
||||
this.#entries.delete(oldestEntry.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeMaxEntries(value: number, fallback: number): number {
|
||||
if (!Number.isFinite(value) || value <= 0) {
|
||||
return fallback;
|
||||
}
|
||||
return Math.max(1, Math.floor(value));
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { buildPluginApi } from "./api-builder.js";
|
||||
import { collectPluginConfigContractMatches } from "./config-contracts.js";
|
||||
import { getCachedPluginJitiLoader, type PluginJitiLoaderCache } from "./jiti-loader-cache.js";
|
||||
import type { PluginManifestRecord } from "./manifest-registry.js";
|
||||
import { PluginLruCache, type PluginLruCacheResult } from "./plugin-lru-cache.js";
|
||||
import { loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry.js";
|
||||
import { resolvePluginCacheInputs } from "./roots.js";
|
||||
import type { PluginRuntime } from "./runtime/types.js";
|
||||
@@ -86,20 +87,22 @@ const NOOP_LOGGER: PluginLogger = {
|
||||
const MAX_SETUP_LOOKUP_CACHE_ENTRIES = 128;
|
||||
|
||||
const jitiLoaders: PluginJitiLoaderCache = new Map();
|
||||
const setupRegistryCache = new Map<string, PluginSetupRegistry>();
|
||||
const setupProviderCache = new Map<string, ProviderPlugin | null>();
|
||||
const setupCliBackendCache = new Map<string, SetupCliBackendEntry | null>();
|
||||
let setupLookupCacheEntryCap = MAX_SETUP_LOOKUP_CACHE_ENTRIES;
|
||||
const setupRegistryCache = new PluginLruCache<PluginSetupRegistry>(MAX_SETUP_LOOKUP_CACHE_ENTRIES);
|
||||
const setupProviderCache = new PluginLruCache<ProviderPlugin | null>(
|
||||
MAX_SETUP_LOOKUP_CACHE_ENTRIES,
|
||||
);
|
||||
const setupCliBackendCache = new PluginLruCache<SetupCliBackendEntry | null>(
|
||||
MAX_SETUP_LOOKUP_CACHE_ENTRIES,
|
||||
);
|
||||
|
||||
export const __testing = {
|
||||
get maxSetupLookupCacheEntries() {
|
||||
return setupLookupCacheEntryCap;
|
||||
return setupRegistryCache.maxEntries;
|
||||
},
|
||||
setMaxSetupLookupCacheEntriesForTest(value?: number) {
|
||||
setupLookupCacheEntryCap =
|
||||
typeof value === "number" && Number.isFinite(value) && value > 0
|
||||
? Math.max(1, Math.floor(value))
|
||||
: MAX_SETUP_LOOKUP_CACHE_ENTRIES;
|
||||
setupRegistryCache.setMaxEntriesForTest(value);
|
||||
setupProviderCache.setMaxEntriesForTest(value);
|
||||
setupCliBackendCache.setMaxEntriesForTest(value);
|
||||
},
|
||||
getCacheSizes() {
|
||||
return {
|
||||
@@ -125,31 +128,12 @@ function getJiti(modulePath: string) {
|
||||
});
|
||||
}
|
||||
|
||||
function getCachedSetupValue<T>(
|
||||
cache: Map<string, T>,
|
||||
key: string,
|
||||
): { hit: true; value: T } | { hit: false } {
|
||||
if (!cache.has(key)) {
|
||||
return { hit: false };
|
||||
}
|
||||
const cached = cache.get(key) as T;
|
||||
cache.delete(key);
|
||||
cache.set(key, cached);
|
||||
return { hit: true, value: cached };
|
||||
function getCachedSetupValue<T>(cache: PluginLruCache<T>, key: string): PluginLruCacheResult<T> {
|
||||
return cache.getResult(key);
|
||||
}
|
||||
|
||||
function setCachedSetupValue<T>(cache: Map<string, T>, key: string, value: T): void {
|
||||
if (cache.has(key)) {
|
||||
cache.delete(key);
|
||||
}
|
||||
function setCachedSetupValue<T>(cache: PluginLruCache<T>, key: string, value: T): void {
|
||||
cache.set(key, value);
|
||||
while (cache.size > setupLookupCacheEntryCap) {
|
||||
const oldestKey = cache.keys().next().value;
|
||||
if (typeof oldestKey !== "string") {
|
||||
break;
|
||||
}
|
||||
cache.delete(oldestKey);
|
||||
}
|
||||
}
|
||||
|
||||
function buildSetupRegistryCacheKey(params: {
|
||||
|
||||
Reference in New Issue
Block a user