From 414f68fb63f366d02ef5110d7ec7c552555c494a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Wed, 4 Feb 2026 10:47:45 +0100 Subject: [PATCH] fix(workspace-cache): memory leak in deleteFromLocalCache (#17686) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem The `deleteFromLocalCache` method was only setting `lastHashCheckedAt=0` instead of actually deleting the cache entry. This caused old versions to accumulate in the local cache when `invalidateAndRecompute` was called. ### Flow that causes the leak: 1. `invalidateAndRecompute()` is called 2. `flush()` → `deleteFromLocalCache()` — **only sets `lastHashCheckedAt=0`, keeps old data** 3. `recomputeDataFromProvider()` → `setInLocalCache()` — **adds new version with new hash** 4. `cleanupStaleVersions()` — **never called** (only triggered from `getFromLocalCache` path) Result: Each `invalidateAndRecompute` call adds a new version to `entry.versions` without removing the old one. ### Impact For migration commands (like `1-17-migrate-attachment-to-morph-relations`) that process thousands of workspaces, this caused significant memory growth: - Each workspace calls `getOrRecompute` (version 1) - Then calls `invalidateAndRecompute` (version 2 added, version 1 stays) - Memory accumulates as the command processes more workspaces ## Solution Actually delete the local cache entry in `deleteFromLocalCache`, so `recomputeDataFromProvider` starts fresh. ```typescript // Before (memory leak) if (isDefined(entry)) { entry.lastHashCheckedAt = 0; } // After (proper cleanup) this.localCache.delete(localKey); ``` --------- Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --- .../services/workspace-cache.service.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/packages/twenty-server/src/engine/workspace-cache/services/workspace-cache.service.ts b/packages/twenty-server/src/engine/workspace-cache/services/workspace-cache.service.ts index f3610494ed..c5703ce674 100644 --- a/packages/twenty-server/src/engine/workspace-cache/services/workspace-cache.service.ts +++ b/packages/twenty-server/src/engine/workspace-cache/services/workspace-cache.service.ts @@ -33,6 +33,8 @@ const LOCAL_ENTRY_TTL_MS = 30 * 60 * 1000; // 30 minutes const MEMOIZER_TTL_MS = 10_000; // 10 seconds const STALE_VERSION_TTL_MS = 5_000; // 5 seconds const MAX_LOCAL_STALE_VERSIONS = 5; // 5 stale versions +const MAX_LOCAL_CACHE_ENTRIES = 1_000; +const MIN_EVICT_KEYS = 100; type CacheDataType = WorkspaceCacheDataMap[WorkspaceCacheKeyName]; @@ -391,6 +393,28 @@ export class WorkspaceCacheService implements OnModuleInit { entry.versions.set(hash, { data, lastReadAt: Date.now() }); entry.latestHash = hash; entry.lastHashCheckedAt = Date.now(); + + this.cleanupStaleVersions(entry); + this.evictLRUEntriesIfNeeded(); + } + + private evictLRUEntriesIfNeeded(): void { + if (this.localCache.size <= MAX_LOCAL_CACHE_ENTRIES) { + return; + } + + const entries = [...this.localCache.entries()].sort( + (a, b) => a[1].lastHashCheckedAt - b[1].lastHashCheckedAt, + ); + + const toEvict = entries.slice( + 0, + Math.max(MIN_EVICT_KEYS, this.localCache.size - MAX_LOCAL_CACHE_ENTRIES), + ); + + for (const [key] of toEvict) { + this.localCache.delete(key); + } } private cleanupStaleVersions(