mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:20:43 +00:00
fix(memory-core): harden singleton cache recovery (#70925)
* fix memory cache singleton hardening * refactor(memory-core): simplify singleton cache repair Co-authored-by: BirdSong <88885494+fire-mirror@users.noreply.github.com> --------- Co-authored-by: BirdSong <88885494+fire-mirror@users.noreply.github.com>
This commit is contained in:
@@ -46,6 +46,25 @@ describe("manager cache", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("repairs an invalid singleton cache shape", async () => {
|
||||
const cacheKey = Symbol("openclaw.manager-cache.corrupt-test");
|
||||
(globalThis as Record<PropertyKey, unknown>)[cacheKey] = {};
|
||||
|
||||
const cache = resolveSingletonManagedCache<TestEntry>(cacheKey);
|
||||
cachesForCleanup.push(cache);
|
||||
const entry = await getOrCreateManagedCacheEntry({
|
||||
cache: cache.cache,
|
||||
pending: cache.pending,
|
||||
key: "same",
|
||||
create: async () => createEntry("repaired"),
|
||||
});
|
||||
|
||||
expect(entry.id).toBe("repaired");
|
||||
expect(cache.cache).toBeInstanceOf(Map);
|
||||
expect(cache.pending).toBeInstanceOf(Map);
|
||||
delete (globalThis as Record<PropertyKey, unknown>)[cacheKey];
|
||||
});
|
||||
|
||||
it("deduplicates concurrent creation for the same cache key", async () => {
|
||||
const cache = createTestCache();
|
||||
cachesForCleanup.push(cache);
|
||||
|
||||
@@ -10,10 +10,24 @@ export type ManagedCache<T> = {
|
||||
};
|
||||
|
||||
export function resolveSingletonManagedCache<T>(cacheKey: symbol): ManagedCache<T> {
|
||||
return resolveGlobalSingleton<ManagedCache<T>>(cacheKey, () => ({
|
||||
const resolved = resolveGlobalSingleton<unknown>(cacheKey, () => ({
|
||||
cache: new Map<string, T>(),
|
||||
pending: new Map<string, Promise<T>>(),
|
||||
}));
|
||||
if (
|
||||
typeof resolved === "object" &&
|
||||
resolved !== null &&
|
||||
(resolved as Partial<ManagedCache<T>>).cache instanceof Map &&
|
||||
(resolved as Partial<ManagedCache<T>>).pending instanceof Map
|
||||
) {
|
||||
return resolved as ManagedCache<T>;
|
||||
}
|
||||
const repaired: ManagedCache<T> = {
|
||||
cache: new Map<string, T>(),
|
||||
pending: new Map<string, Promise<T>>(),
|
||||
};
|
||||
(globalThis as Record<PropertyKey, unknown>)[cacheKey] = repaired;
|
||||
return repaired;
|
||||
}
|
||||
|
||||
export async function getOrCreateManagedCacheEntry<T>(params: {
|
||||
|
||||
@@ -214,6 +214,25 @@ beforeEach(async () => {
|
||||
});
|
||||
|
||||
describe("getMemorySearchManager caching", () => {
|
||||
it("repairs an invalid shared singleton cache shape before using qmd cache maps", async () => {
|
||||
await closeAllMemorySearchManagers();
|
||||
vi.resetModules();
|
||||
const cacheKey = Symbol.for("openclaw.memorySearchManagerCache");
|
||||
(globalThis as Record<PropertyKey, unknown>)[cacheKey] = {};
|
||||
|
||||
const freshModule = await import("./search-manager.js");
|
||||
try {
|
||||
const result = await freshModule.getMemorySearchManager({
|
||||
cfg: createQmdCfg("corrupt-cache-agent"),
|
||||
agentId: "corrupt-cache-agent",
|
||||
});
|
||||
requireManager(result);
|
||||
} finally {
|
||||
await freshModule.closeAllMemorySearchManagers();
|
||||
delete (globalThis as Record<PropertyKey, unknown>)[cacheKey];
|
||||
}
|
||||
});
|
||||
|
||||
it("reuses the same QMD manager instance for repeated calls", async () => {
|
||||
const cfg = createQmdCfg("main");
|
||||
|
||||
|
||||
@@ -42,15 +42,30 @@ type MemorySearchManagerCacheStore = {
|
||||
pendingQmdManagerCreates: Map<string, PendingQmdManagerCreate>;
|
||||
};
|
||||
|
||||
function createMemorySearchManagerCacheStore(): MemorySearchManagerCacheStore {
|
||||
return {
|
||||
qmdManagerCache: new Map<string, CachedQmdManagerEntry>(),
|
||||
pendingQmdManagerCreates: new Map<string, PendingQmdManagerCreate>(),
|
||||
};
|
||||
}
|
||||
|
||||
function getMemorySearchManagerCacheStore(): MemorySearchManagerCacheStore {
|
||||
// Keep caches reachable across `vi.resetModules()` so later cleanup can close older instances.
|
||||
return resolveGlobalSingleton<MemorySearchManagerCacheStore>(
|
||||
const resolved = resolveGlobalSingleton<unknown>(
|
||||
MEMORY_SEARCH_MANAGER_CACHE_KEY,
|
||||
() => ({
|
||||
qmdManagerCache: new Map<string, CachedQmdManagerEntry>(),
|
||||
pendingQmdManagerCreates: new Map<string, PendingQmdManagerCreate>(),
|
||||
}),
|
||||
createMemorySearchManagerCacheStore,
|
||||
);
|
||||
if (
|
||||
typeof resolved === "object" &&
|
||||
resolved !== null &&
|
||||
(resolved as Partial<MemorySearchManagerCacheStore>).qmdManagerCache instanceof Map &&
|
||||
(resolved as Partial<MemorySearchManagerCacheStore>).pendingQmdManagerCreates instanceof Map
|
||||
) {
|
||||
return resolved as MemorySearchManagerCacheStore;
|
||||
}
|
||||
const repaired = createMemorySearchManagerCacheStore();
|
||||
(globalThis as Record<PropertyKey, unknown>)[MEMORY_SEARCH_MANAGER_CACHE_KEY] = repaired;
|
||||
return repaired;
|
||||
}
|
||||
|
||||
const log = createSubsystemLogger("memory");
|
||||
|
||||
Reference in New Issue
Block a user