Skip to content

Commit eb074db

Browse files
committed
refactor(cache): address review — idle TTL for client cache, LRUCache for deployed state
- client-cache: add updateAgeOnGet so the TTL is genuinely idle-based (active clients keep their warm keep-alive connections; the JSDoc now matches behavior). - deployed-state: replace the hand-rolled Map + manual FIFO eviction/TTL with LRUCache (real LRU eviction, built-in TTL), matching the effectiveDecryptedEnv and integration-tool-schema caches. TTL stays absolute (not reset on read) so the credential-migration remap still propagates across ECS tasks. Both per review feedback from Greptile.
1 parent a258a38 commit eb074db

2 files changed

Lines changed: 17 additions & 29 deletions

File tree

apps/sim/lib/workflows/persistence/utils.ts

Lines changed: 14 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type { DbOrTx, NormalizedWorkflowData } from '@sim/workflow-persistence/t
1313
import type { BlockState, Loop, Parallel, WorkflowState } from '@sim/workflow-types/workflow'
1414
import type { InferSelectModel } from 'drizzle-orm'
1515
import { and, desc, eq, inArray, lt, sql } from 'drizzle-orm'
16+
import { LRUCache } from 'lru-cache'
1617
import type { Edge } from 'reactflow'
1718
import { remapConditionBlockIds, remapConditionEdgeHandle } from '@/lib/workflows/condition-ids'
1819
import {
@@ -100,35 +101,31 @@ export async function blockExistsInDeployment(
100101
}
101102

102103
/**
103-
* Maximum number of deployed-state entries retained in the process-local cache.
104104
* Each entry is keyed by an immutable `deploymentVersionId` and holds a
105-
* fully-migrated {@link DeployedWorkflowData} snapshot (tens of KB to ~1MB),
106-
* so the bound keeps worst-case memory within a sane envelope.
105+
* fully-migrated {@link DeployedWorkflowData} snapshot (tens of KB to ~1MB);
106+
* the bound keeps worst-case memory within a sane envelope.
107107
*/
108108
const DEPLOYED_STATE_CACHE_MAX_ENTRIES = 500
109109
const DEPLOYED_STATE_CACHE_TTL_MS = 5 * 60 * 1000
110110

111-
interface DeployedStateCacheEntry {
112-
data: DeployedWorkflowData
113-
expiresAt: number
114-
}
115-
116111
/**
117112
* Process-local cache of fully-loaded, post-migration deployed workflow state,
118113
* keyed by the immutable `deploymentVersionId`.
119114
*
120115
* The id is unique per deploy — a redeploy mints a new id and a rollback
121116
* reactivates an existing id — so the active-version lookup naturally selects a
122117
* different (or already-cached) key whenever the active deployment changes,
123-
* making the cache self-invalidating across redeploy/rollback. Insertion order
124-
* is used for oldest-first (LRU-style) eviction once the bound is reached.
118+
* making the cache self-invalidating across redeploy/rollback.
125119
*
126-
* A short TTL bounds the one piece of the cached state that is not strictly
127-
* immutable: `applyBlockMigrations` resolves legacy credential references via a
128-
* live lookup, so the TTL lets a credential change propagate across ECS tasks
129-
* without waiting for redeploy or eviction.
120+
* The TTL is absolute (not reset on read) on purpose: it bounds the one piece
121+
* of the cached state that is not strictly immutable — `applyBlockMigrations`
122+
* resolves legacy credential references via a live lookup — so a credential
123+
* change propagates across ECS tasks even for a continuously-running workflow.
130124
*/
131-
const deployedStateCache = new Map<string, DeployedStateCacheEntry>()
125+
const deployedStateCache = new LRUCache<string, DeployedWorkflowData>({
126+
max: DEPLOYED_STATE_CACHE_MAX_ENTRIES,
127+
ttl: DEPLOYED_STATE_CACHE_TTL_MS,
128+
})
132129

133130
/**
134131
* Drop cached deployed state. Pass a `deploymentVersionId` to evict a single
@@ -171,10 +168,7 @@ export async function loadDeployedWorkflowState(
171168

172169
const cached = deployedStateCache.get(active.id)
173170
if (cached) {
174-
if (cached.expiresAt > Date.now()) {
175-
return structuredClone(cached.data)
176-
}
177-
deployedStateCache.delete(active.id)
171+
return structuredClone(cached)
178172
}
179173

180174
const state = active.state as WorkflowState & { variables?: Record<string, unknown> }
@@ -204,16 +198,7 @@ export async function loadDeployedWorkflowState(
204198
deploymentVersionId: active.id,
205199
}
206200

207-
if (deployedStateCache.size >= DEPLOYED_STATE_CACHE_MAX_ENTRIES) {
208-
const oldestKey = deployedStateCache.keys().next().value
209-
if (oldestKey !== undefined) {
210-
deployedStateCache.delete(oldestKey)
211-
}
212-
}
213-
deployedStateCache.set(active.id, {
214-
data: deployedState,
215-
expiresAt: Date.now() + DEPLOYED_STATE_CACHE_TTL_MS,
216-
})
201+
deployedStateCache.set(active.id, deployedState)
217202

218203
return structuredClone(deployedState)
219204
} catch (error) {

apps/sim/providers/anthropic/client-cache.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ const CLIENT_CACHE_TTL_MS = 30 * 60 * 1_000
2121
const clientCache = new LRUCache<string, Anthropic>({
2222
max: CLIENT_CACHE_MAX_ENTRIES,
2323
ttl: CLIENT_CACHE_TTL_MS,
24+
// Idle expiry: the TTL resets on every hit so a continuously-used client
25+
// (and its warm keep-alive connections) survives, while idle keys age out.
26+
updateAgeOnGet: true,
2427
})
2528

2629
/**

0 commit comments

Comments
 (0)