mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-23 16:01:17 +00:00
refactor: convert session manager cache to factory
This commit is contained in:
@@ -1,40 +1,31 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { importFreshModule } from "../../../test/helpers/import-fresh.js";
|
||||
|
||||
type SessionManagerCacheModule = typeof import("./session-manager-cache.js");
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createSessionManagerCache } from "./session-manager-cache.js";
|
||||
|
||||
describe("session manager cache", () => {
|
||||
let savedSessionManagerTtl: string | undefined;
|
||||
it("prunes expired entries during later cache activity even without revisiting them", () => {
|
||||
let now = 1_000;
|
||||
const cache = createSessionManagerCache({
|
||||
clock: () => now,
|
||||
ttlMs: 5_000,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
savedSessionManagerTtl = process.env.OPENCLAW_SESSION_MANAGER_CACHE_TTL_MS;
|
||||
process.env.OPENCLAW_SESSION_MANAGER_CACHE_TTL_MS = "5000";
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-03-22T00:00:00Z"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (savedSessionManagerTtl === undefined) {
|
||||
delete process.env.OPENCLAW_SESSION_MANAGER_CACHE_TTL_MS;
|
||||
} else {
|
||||
process.env.OPENCLAW_SESSION_MANAGER_CACHE_TTL_MS = savedSessionManagerTtl;
|
||||
}
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("prunes expired entries during later cache activity even without revisiting them", async () => {
|
||||
const cache = await importFreshModule<SessionManagerCacheModule>(
|
||||
import.meta.url,
|
||||
"./session-manager-cache.js?session-manager-cache-prune-on-access",
|
||||
);
|
||||
|
||||
cache.__testing.resetSessionManagerCache();
|
||||
cache.trackSessionManagerAccess("/tmp/stale-session.jsonl");
|
||||
expect(cache.__testing.getSessionManagerCacheKeys()).toEqual(["/tmp/stale-session.jsonl"]);
|
||||
expect(cache.keys()).toEqual(["/tmp/stale-session.jsonl"]);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(6_000);
|
||||
now = 7_000;
|
||||
|
||||
cache.trackSessionManagerAccess("/tmp/fresh-session.jsonl");
|
||||
expect(cache.__testing.getSessionManagerCacheKeys()).toEqual(["/tmp/fresh-session.jsonl"]);
|
||||
expect(cache.keys()).toEqual(["/tmp/fresh-session.jsonl"]);
|
||||
});
|
||||
|
||||
it("can disable caching via the injected TTL resolver", () => {
|
||||
const cache = createSessionManagerCache({
|
||||
ttlMs: 0,
|
||||
});
|
||||
|
||||
cache.trackSessionManagerAccess("/tmp/session.jsonl");
|
||||
|
||||
expect(cache.isSessionManagerCached("/tmp/session.jsonl")).toBe(false);
|
||||
expect(cache.keys()).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
import { Buffer } from "node:buffer";
|
||||
import fs from "node:fs/promises";
|
||||
import { isCacheEnabled, resolveCacheTtlMs } from "../../config/cache-utils.js";
|
||||
import {
|
||||
createExpiringMapCache,
|
||||
isCacheEnabled,
|
||||
resolveCacheTtlMs,
|
||||
} from "../../config/cache-utils.js";
|
||||
|
||||
type SessionManagerCacheEntry = {
|
||||
sessionFile: string;
|
||||
loadedAt: number;
|
||||
};
|
||||
|
||||
const SESSION_MANAGER_CACHE = new Map<string, SessionManagerCacheEntry>();
|
||||
const DEFAULT_SESSION_MANAGER_TTL_MS = 45_000; // 45 seconds
|
||||
const MIN_SESSION_MANAGER_CACHE_PRUNE_INTERVAL_MS = 1_000;
|
||||
const MAX_SESSION_MANAGER_CACHE_PRUNE_INTERVAL_MS = 30_000;
|
||||
|
||||
let lastSessionManagerCachePruneAt = 0;
|
||||
|
||||
function getSessionManagerTtl(): number {
|
||||
return resolveCacheTtlMs({
|
||||
envValue: process.env.OPENCLAW_SESSION_MANAGER_CACHE_TTL_MS,
|
||||
@@ -21,10 +17,6 @@ function getSessionManagerTtl(): number {
|
||||
});
|
||||
}
|
||||
|
||||
function isSessionManagerCacheEnabled(): boolean {
|
||||
return isCacheEnabled(getSessionManagerTtl());
|
||||
}
|
||||
|
||||
function resolveSessionManagerCachePruneInterval(ttlMs: number): number {
|
||||
return Math.min(
|
||||
Math.max(ttlMs, MIN_SESSION_MANAGER_CACHE_PRUNE_INTERVAL_MS),
|
||||
@@ -32,74 +24,74 @@ function resolveSessionManagerCachePruneInterval(ttlMs: number): number {
|
||||
);
|
||||
}
|
||||
|
||||
function maybePruneExpiredSessionManagerCache(now: number, ttlMs: number): void {
|
||||
if (now - lastSessionManagerCachePruneAt < resolveSessionManagerCachePruneInterval(ttlMs)) {
|
||||
return;
|
||||
}
|
||||
for (const [sessionFile, entry] of SESSION_MANAGER_CACHE.entries()) {
|
||||
if (now - entry.loadedAt > ttlMs) {
|
||||
SESSION_MANAGER_CACHE.delete(sessionFile);
|
||||
}
|
||||
}
|
||||
lastSessionManagerCachePruneAt = now;
|
||||
export type SessionManagerCache = {
|
||||
clear: () => void;
|
||||
isSessionManagerCached: (sessionFile: string) => boolean;
|
||||
keys: () => string[];
|
||||
prewarmSessionFile: (sessionFile: string) => Promise<void>;
|
||||
trackSessionManagerAccess: (sessionFile: string) => void;
|
||||
};
|
||||
|
||||
export function createSessionManagerCache(options?: {
|
||||
clock?: () => number;
|
||||
fsModule?: Pick<typeof fs, "open">;
|
||||
ttlMs?: number | (() => number);
|
||||
}): SessionManagerCache {
|
||||
const getTtlMs = () =>
|
||||
typeof options?.ttlMs === "function"
|
||||
? options.ttlMs()
|
||||
: (options?.ttlMs ?? getSessionManagerTtl());
|
||||
const cache = createExpiringMapCache<string, true>({
|
||||
ttlMs: getTtlMs,
|
||||
pruneIntervalMs: resolveSessionManagerCachePruneInterval,
|
||||
clock: options?.clock,
|
||||
});
|
||||
const fsModule = options?.fsModule ?? fs;
|
||||
|
||||
return {
|
||||
clear: () => {
|
||||
cache.clear();
|
||||
},
|
||||
isSessionManagerCached: (sessionFile) => cache.get(sessionFile) === true,
|
||||
keys: () => cache.keys(),
|
||||
prewarmSessionFile: async (sessionFile) => {
|
||||
if (!isCacheEnabled(getTtlMs())) {
|
||||
return;
|
||||
}
|
||||
if (cache.get(sessionFile) === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Read a small chunk to encourage OS page cache warmup.
|
||||
const handle = await fsModule.open(sessionFile, "r");
|
||||
try {
|
||||
const buffer = Buffer.alloc(4096);
|
||||
await handle.read(buffer, 0, buffer.length, 0);
|
||||
} finally {
|
||||
await handle.close();
|
||||
}
|
||||
cache.set(sessionFile, true);
|
||||
} catch {
|
||||
// File doesn't exist yet, SessionManager will create it
|
||||
}
|
||||
},
|
||||
trackSessionManagerAccess: (sessionFile) => {
|
||||
cache.set(sessionFile, true);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const sessionManagerCache = createSessionManagerCache();
|
||||
|
||||
export function trackSessionManagerAccess(sessionFile: string): void {
|
||||
const ttl = getSessionManagerTtl();
|
||||
if (!isCacheEnabled(ttl)) {
|
||||
return;
|
||||
}
|
||||
const now = Date.now();
|
||||
maybePruneExpiredSessionManagerCache(now, ttl);
|
||||
SESSION_MANAGER_CACHE.set(sessionFile, {
|
||||
sessionFile,
|
||||
loadedAt: now,
|
||||
});
|
||||
sessionManagerCache.trackSessionManagerAccess(sessionFile);
|
||||
}
|
||||
|
||||
function isSessionManagerCached(sessionFile: string): boolean {
|
||||
const ttl = getSessionManagerTtl();
|
||||
if (!isCacheEnabled(ttl)) {
|
||||
return false;
|
||||
}
|
||||
const now = Date.now();
|
||||
maybePruneExpiredSessionManagerCache(now, ttl);
|
||||
const entry = SESSION_MANAGER_CACHE.get(sessionFile);
|
||||
if (!entry) {
|
||||
return false;
|
||||
}
|
||||
return now - entry.loadedAt <= ttl;
|
||||
export function isSessionManagerCached(sessionFile: string): boolean {
|
||||
return sessionManagerCache.isSessionManagerCached(sessionFile);
|
||||
}
|
||||
|
||||
export async function prewarmSessionFile(sessionFile: string): Promise<void> {
|
||||
if (!isSessionManagerCacheEnabled()) {
|
||||
return;
|
||||
}
|
||||
if (isSessionManagerCached(sessionFile)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Read a small chunk to encourage OS page cache warmup.
|
||||
const handle = await fs.open(sessionFile, "r");
|
||||
try {
|
||||
const buffer = Buffer.alloc(4096);
|
||||
await handle.read(buffer, 0, buffer.length, 0);
|
||||
} finally {
|
||||
await handle.close();
|
||||
}
|
||||
trackSessionManagerAccess(sessionFile);
|
||||
} catch {
|
||||
// File doesn't exist yet, SessionManager will create it
|
||||
}
|
||||
await sessionManagerCache.prewarmSessionFile(sessionFile);
|
||||
}
|
||||
|
||||
export const __testing = {
|
||||
getSessionManagerCacheKeys(): string[] {
|
||||
return [...SESSION_MANAGER_CACHE.keys()];
|
||||
},
|
||||
resetSessionManagerCache(): void {
|
||||
SESSION_MANAGER_CACHE.clear();
|
||||
lastSessionManagerCachePruneAt = 0;
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user