mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 22:14:34 +00:00
Outbound: bound directory cache memory growth
This commit is contained in:
45
src/infra/outbound/directory-cache.test.ts
Normal file
45
src/infra/outbound/directory-cache.test.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import type { OpenClawConfig } from "../../config/config.js";
|
||||||
|
import { DirectoryCache } from "./directory-cache.js";
|
||||||
|
|
||||||
|
describe("DirectoryCache", () => {
|
||||||
|
const cfg = {} as OpenClawConfig;
|
||||||
|
|
||||||
|
it("expires entries after ttl", () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
vi.setSystemTime(new Date("2026-01-01T00:00:00.000Z"));
|
||||||
|
const cache = new DirectoryCache<string>(1000, 10);
|
||||||
|
|
||||||
|
cache.set("a", "value-a", cfg);
|
||||||
|
expect(cache.get("a", cfg)).toBe("value-a");
|
||||||
|
|
||||||
|
vi.setSystemTime(new Date("2026-01-01T00:00:02.000Z"));
|
||||||
|
expect(cache.get("a", cfg)).toBeUndefined();
|
||||||
|
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("evicts oldest keys when max size is exceeded", () => {
|
||||||
|
const cache = new DirectoryCache<string>(60_000, 2);
|
||||||
|
cache.set("a", "value-a", cfg);
|
||||||
|
cache.set("b", "value-b", cfg);
|
||||||
|
cache.set("c", "value-c", cfg);
|
||||||
|
|
||||||
|
expect(cache.get("a", cfg)).toBeUndefined();
|
||||||
|
expect(cache.get("b", cfg)).toBe("value-b");
|
||||||
|
expect(cache.get("c", cfg)).toBe("value-c");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("refreshes insertion order on key updates", () => {
|
||||||
|
const cache = new DirectoryCache<string>(60_000, 2);
|
||||||
|
cache.set("a", "value-a", cfg);
|
||||||
|
cache.set("b", "value-b", cfg);
|
||||||
|
cache.set("a", "value-a2", cfg);
|
||||||
|
cache.set("c", "value-c", cfg);
|
||||||
|
|
||||||
|
// Updating "a" should keep it and evict older "b".
|
||||||
|
expect(cache.get("a", cfg)).toBe("value-a2");
|
||||||
|
expect(cache.get("b", cfg)).toBeUndefined();
|
||||||
|
expect(cache.get("c", cfg)).toBe("value-c");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -22,25 +22,35 @@ export function buildDirectoryCacheKey(key: DirectoryCacheKey): string {
|
|||||||
export class DirectoryCache<T> {
|
export class DirectoryCache<T> {
|
||||||
private readonly cache = new Map<string, CacheEntry<T>>();
|
private readonly cache = new Map<string, CacheEntry<T>>();
|
||||||
private lastConfigRef: OpenClawConfig | null = null;
|
private lastConfigRef: OpenClawConfig | null = null;
|
||||||
|
private readonly maxSize: number;
|
||||||
|
|
||||||
constructor(private readonly ttlMs: number) {}
|
constructor(
|
||||||
|
private readonly ttlMs: number,
|
||||||
|
maxSize = 2000,
|
||||||
|
) {
|
||||||
|
this.maxSize = Math.max(1, Math.floor(maxSize));
|
||||||
|
}
|
||||||
|
|
||||||
get(key: string, cfg: OpenClawConfig): T | undefined {
|
get(key: string, cfg: OpenClawConfig): T | undefined {
|
||||||
this.resetIfConfigChanged(cfg);
|
this.resetIfConfigChanged(cfg);
|
||||||
|
this.pruneExpired(Date.now());
|
||||||
const entry = this.cache.get(key);
|
const entry = this.cache.get(key);
|
||||||
if (!entry) {
|
if (!entry) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
if (Date.now() - entry.fetchedAt > this.ttlMs) {
|
|
||||||
this.cache.delete(key);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return entry.value;
|
return entry.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
set(key: string, value: T, cfg: OpenClawConfig): void {
|
set(key: string, value: T, cfg: OpenClawConfig): void {
|
||||||
this.resetIfConfigChanged(cfg);
|
this.resetIfConfigChanged(cfg);
|
||||||
this.cache.set(key, { value, fetchedAt: Date.now() });
|
const now = Date.now();
|
||||||
|
this.pruneExpired(now);
|
||||||
|
// Refresh insertion order so active keys are less likely to be evicted.
|
||||||
|
if (this.cache.has(key)) {
|
||||||
|
this.cache.delete(key);
|
||||||
|
}
|
||||||
|
this.cache.set(key, { value, fetchedAt: now });
|
||||||
|
this.evictToMaxSize();
|
||||||
}
|
}
|
||||||
|
|
||||||
clearMatching(match: (key: string) => boolean): void {
|
clearMatching(match: (key: string) => boolean): void {
|
||||||
@@ -64,4 +74,25 @@ export class DirectoryCache<T> {
|
|||||||
}
|
}
|
||||||
this.lastConfigRef = cfg;
|
this.lastConfigRef = cfg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private pruneExpired(now: number): void {
|
||||||
|
if (this.ttlMs <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const [cacheKey, entry] of this.cache.entries()) {
|
||||||
|
if (now - entry.fetchedAt > this.ttlMs) {
|
||||||
|
this.cache.delete(cacheKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private evictToMaxSize(): void {
|
||||||
|
while (this.cache.size > this.maxSize) {
|
||||||
|
const oldestKey = this.cache.keys().next().value;
|
||||||
|
if (typeof oldestKey !== "string") {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this.cache.delete(oldestKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user