fix(memory-core): harden singleton cache recovery (#70925) · openclaw/openclaw@4d7c4b3
Patrick-Eric
·
2026-04-24
·
via Recent Commits to openclaw:main
| Original file line number | Diff line number | Diff line change |
|---|
@@ -46,6 +46,25 @@ describe("manager cache", () => {
|
46 | 46 | ); |
47 | 47 | }); |
48 | 48 | |
| 49 | +it("repairs an invalid singleton cache shape", async () => { |
| 50 | +const cacheKey = Symbol("openclaw.manager-cache.corrupt-test"); |
| 51 | +(globalThis as Record<PropertyKey, unknown>)[cacheKey] = {}; |
| 52 | + |
| 53 | +const cache = resolveSingletonManagedCache<TestEntry>(cacheKey); |
| 54 | +cachesForCleanup.push(cache); |
| 55 | +const entry = await getOrCreateManagedCacheEntry({ |
| 56 | +cache: cache.cache, |
| 57 | +pending: cache.pending, |
| 58 | +key: "same", |
| 59 | +create: async () => createEntry("repaired"), |
| 60 | +}); |
| 61 | + |
| 62 | +expect(entry.id).toBe("repaired"); |
| 63 | +expect(cache.cache).toBeInstanceOf(Map); |
| 64 | +expect(cache.pending).toBeInstanceOf(Map); |
| 65 | +delete (globalThis as Record<PropertyKey, unknown>)[cacheKey]; |
| 66 | +}); |
| 67 | + |
49 | 68 | it("deduplicates concurrent creation for the same cache key", async () => { |
50 | 69 | const cache = createTestCache(); |
51 | 70 | cachesForCleanup.push(cache); |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -10,10 +10,24 @@ export type ManagedCache<T> = {
|
10 | 10 | }; |
11 | 11 | |
12 | 12 | export function resolveSingletonManagedCache<T>(cacheKey: symbol): ManagedCache<T> { |
13 | | -return resolveGlobalSingleton<ManagedCache<T>>(cacheKey, () => ({ |
| 13 | +const resolved = resolveGlobalSingleton<unknown>(cacheKey, () => ({ |
14 | 14 | cache: new Map<string, T>(), |
15 | 15 | pending: new Map<string, Promise<T>>(), |
16 | 16 | })); |
| 17 | +if ( |
| 18 | +typeof resolved === "object" && |
| 19 | +resolved !== null && |
| 20 | +(resolved as Partial<ManagedCache<T>>).cache instanceof Map && |
| 21 | +(resolved as Partial<ManagedCache<T>>).pending instanceof Map |
| 22 | +) { |
| 23 | +return resolved as ManagedCache<T>; |
| 24 | +} |
| 25 | +const repaired: ManagedCache<T> = { |
| 26 | +cache: new Map<string, T>(), |
| 27 | +pending: new Map<string, Promise<T>>(), |
| 28 | +}; |
| 29 | +(globalThis as Record<PropertyKey, unknown>)[cacheKey] = repaired; |
| 30 | +return repaired; |
17 | 31 | } |
18 | 32 | |
19 | 33 | export async function getOrCreateManagedCacheEntry<T>(params: { |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -214,6 +214,25 @@ beforeEach(async () => {
|
214 | 214 | }); |
215 | 215 | |
216 | 216 | describe("getMemorySearchManager caching", () => { |
| 217 | +it("repairs an invalid shared singleton cache shape before using qmd cache maps", async () => { |
| 218 | +await closeAllMemorySearchManagers(); |
| 219 | +vi.resetModules(); |
| 220 | +const cacheKey = Symbol.for("openclaw.memorySearchManagerCache"); |
| 221 | +(globalThis as Record<PropertyKey, unknown>)[cacheKey] = {}; |
| 222 | + |
| 223 | +const freshModule = await import("./search-manager.js"); |
| 224 | +try { |
| 225 | +const result = await freshModule.getMemorySearchManager({ |
| 226 | +cfg: createQmdCfg("corrupt-cache-agent"), |
| 227 | +agentId: "corrupt-cache-agent", |
| 228 | +}); |
| 229 | +requireManager(result); |
| 230 | +} finally { |
| 231 | +await freshModule.closeAllMemorySearchManagers(); |
| 232 | +delete (globalThis as Record<PropertyKey, unknown>)[cacheKey]; |
| 233 | +} |
| 234 | +}); |
| 235 | + |
217 | 236 | it("reuses the same QMD manager instance for repeated calls", async () => { |
218 | 237 | const cfg = createQmdCfg("main"); |
219 | 238 | |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -42,15 +42,30 @@ type MemorySearchManagerCacheStore = {
|
42 | 42 | pendingQmdManagerCreates: Map<string, PendingQmdManagerCreate>; |
43 | 43 | }; |
44 | 44 | |
| 45 | +function createMemorySearchManagerCacheStore(): MemorySearchManagerCacheStore { |
| 46 | +return { |
| 47 | +qmdManagerCache: new Map<string, CachedQmdManagerEntry>(), |
| 48 | +pendingQmdManagerCreates: new Map<string, PendingQmdManagerCreate>(), |
| 49 | +}; |
| 50 | +} |
| 51 | + |
45 | 52 | function getMemorySearchManagerCacheStore(): MemorySearchManagerCacheStore { |
46 | 53 | // Keep caches reachable across `vi.resetModules()` so later cleanup can close older instances. |
47 | | -return resolveGlobalSingleton<MemorySearchManagerCacheStore>( |
| 54 | +const resolved = resolveGlobalSingleton<unknown>( |
48 | 55 | MEMORY_SEARCH_MANAGER_CACHE_KEY, |
49 | | -() => ({ |
50 | | -qmdManagerCache: new Map<string, CachedQmdManagerEntry>(), |
51 | | -pendingQmdManagerCreates: new Map<string, PendingQmdManagerCreate>(), |
52 | | -}), |
| 56 | +createMemorySearchManagerCacheStore, |
53 | 57 | ); |
| 58 | +if ( |
| 59 | +typeof resolved === "object" && |
| 60 | +resolved !== null && |
| 61 | +(resolved as Partial<MemorySearchManagerCacheStore>).qmdManagerCache instanceof Map && |
| 62 | +(resolved as Partial<MemorySearchManagerCacheStore>).pendingQmdManagerCreates instanceof Map |
| 63 | +) { |
| 64 | +return resolved as MemorySearchManagerCacheStore; |
| 65 | +} |
| 66 | +const repaired = createMemorySearchManagerCacheStore(); |
| 67 | +(globalThis as Record<PropertyKey, unknown>)[MEMORY_SEARCH_MANAGER_CACHE_KEY] = repaired; |
| 68 | +return repaired; |
54 | 69 | } |
55 | 70 | |
56 | 71 | const log = createSubsystemLogger("memory"); |
|
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。