Sim 进程内缓存实践用 lru-cache、fetchMethod 与 coalesceLocally 构建正确的 TTL 缓存【免费下载链接】simSim is the collaborative workspace to build, deploy, and monitor AI agents and workflows. Used by 100,000 builders.项目地址: https://gitcode.com/GitHub_Trending/sim16/simSim 的.claude/rules/sim-caching.md是一份面向执行路径executor、tools、providers的进程内缓存规范它规定了何时该用缓存、如何用lru-cache的max/ttl约束内存、如何用fetchMethod或coalesceLocally做异步读穿透以及凭据与权限闸门的缓存边界。读完本文你可以掌握在长驻 Node.js 进程中设计 TTL 缓存的完整决策树——从这到底是不是缓存到失效策略并获得 Sim 仓库中每一处规则对应的真实源码佐证。先判断这根本就不是缓存Sim 规范的第一条判断是代码库里大多数模块级Map不是缓存强行把它们改造成缓存比放任不管更糟。Sim 将模块级状态分为两类形态形态Key 何时死亡正确工具生命周期 Map如activeStreams、pendingChildRuns、memoryStreams、handlerRegistry被追踪的对象结束且代码在那里删除它普通Map。无 TTL、无上限TTL 缓存以租户 org id / user id / workspace id 为 key 的远程读取时间流逝LRUCache生命周期 Map 的 key 空间是无界的这没有问题因为每个 key 都有明确的死亡时刻。给它加 TTL 会引入一个与生命周期竞态的过期加上限则会悄悄丢弃存活状态。所以判断标准很简单key 是否有定义明确的删除点。有就是生命周期 Map没有、只能靠时间过期才是 TTL 缓存。TTL 缓存必须同时设置maxlru-cache是apps/sim的直接依赖apps/sim/package.json 中声明lru-cache: 11.3.6它同时负责过期、容量上限以及通过fetchMethod实现的请求合并。用Map加Date.now() - entry.fetchedAt TTL手写这三件事只会把三件事都做坏。规范的典型写法与 lib/auth/session-policy.ts 中真实的policyCache一致const policyCache new LRUCachestring, ResolvedSessionPolicy({ max: 20_000, ttl: SESSION_POLICY_CACHE_TTL_MS, })几个关键结论均有源码佐证ttl本身不约束内存。在没有ttlAutopurge的情况下它本身昂贵——每个 entry 一个定时器过期条目会一直滞留直到有调用触碰到它的 key或被上限逐出。真正给进程封顶的是max。getSessionPolicy的注释里写得很直白这个缓存每次会话创建和刷新都要读、按组织为 key因此一个无界的Map会随进程存活时间一直增长apps/sim/lib/auth/session-policy.ts#L24-L37。上限是内存兜底不是运行阈值。超过它会让 LRU 在 TTL 之内就逐出条目于是每次未命中都变成一次真实读取——答案永远不会错只是退化回没有缓存时的行为但它会让缓存所在路径的命中率出现断崖。条目只有几十字节所以应把上限设得远高于 TTL 窗口内任何合理的单实例工作集让它始终保持兜底姿态。读取用! undefined判断而不是真值判断。当缓存值可能是false、0或null时if (cached)会把缓存的false当成未命中每次调用都重新查询——恰恰是对缓存最想保护的那些租户。lib/api-key/byok-entitlement.ts 的注释专门强调了这一点其实现就是if (cached ! undefined) return cachedapps/sim/lib/api-key/byok-entitlement.ts#L57-L85。异步读穿透优先fetchMethod但有一个例外fetchMethodcache.fetch(key)在一个原语里提供三件事TTL、请求合并并发调用方共享同一个 promise、以及拒绝时逐出noDeleteOnFetchRejection默认为false。规范建议在任何自己组合读穿透逻辑之前先考虑这个原语。唯一需要自己组合的理由挂死的生产者hung producer。fetchMethod没有 settle 截止时间而 Sim 的应用数据库连接池没有设置statement_timeout——从 packages/db/db.ts 的池配置可以看到只设置了idle_timeout和connect_timeout这两者都不约束一个已经执行中的查询。在这种环境下一个卡死的读取会让所有调用方在整个 TTL 期间都被挂住。此时的替代方案是用/lib/concurrency/singleflight的coalesceLocally包住一个读穿透式LRUCache——它会在自己的截止时间上逐出并拒绝。coalesceLocally的实现值得细看apps/sim/lib/concurrency/singleflight.ts进程内用一张inflightMap 按 key 去重第一个调用方执行fn同 key 的并发调用方共享其 promise默认 settle 截止时间为 30 秒DEFAULT_SETTLE_TIMEOUT_MS超时后条目先被逐出、所有等待者收到CoalesceSettleTimeoutError下一个调用方会创建新的生产者而不是加入挂死的那个关键的诚实细节底层fn在超时时不会被取消它继续以分离状态运行只是没有新的调用方会加入它。这一语义直接影响了上层缓存写入的位置——见下文byok-entitlement.ts的注释写缓存必须放在调用方拿到值的这一侧而不是生产者内部否则一个超时后被遗弃的生产者可能在重试已经缓存了更新答案之后才 resolve把新答案覆盖掉整整一个 TTL。规范同时明确警告不要在lru-cache之上再封一层自研 wrapper。因为调用点的需求差异是一个薄助手无法容纳的——providers/client-cache.ts 是同步记忆化updateAgeOnGet: true让 TTL 变成基于空闲时间持续使用的 provider 客户端保持 warm 连接空闲 key 自然老化max: 1_000、TTL 30 分钟lib/auth/security-policy.ts 则按条目设置 TTLmembershipCache.set(userId, value, { ttl: ... })。一个只覆盖常见情况的 wrapper 只会变成第四种模式。缓存闸门绝不缓存凭据这是规范中最有业务语义的一条规则权限、套餐、策略这类闸门可以容忍有界的陈旧——而且陈旧方向是安全的一个已失效的组织在最多一个 TTL 内继续用自己的 provider key只是多花一点计量费不会错收任何人的钱。但 key 材料不行吊销必须立即生效。所以getBYOKKey每次调用都重新读取 key 行只在其外围缓存权限。byok-entitlement.ts 顶部的注释原样记录了这一权衡ORGANIZATION_BYOK_ENTITLEMENT_TTL_MS设为 60 秒与SESSION_POLICY_CACHE_TTL_MS对齐。故障不能被缓存成否定答案。一个把读取失败映射为false的解析器会让故障与真实失效无法区分。解法是给解析器一个onError: throw选项只在成功路径写缓存。resolveOrganizationPlan(organizationId, { onError: throw })正是 byok-entitlement.ts 中这么调用的。有人在等待的地方读新鲜数据。保留两个入口而不是一个带缓存的函数设置页和管理类用例绝不能告诉一个刚升级的组织它还没有套餐而下方的执行路径可以从缓存取isOrganizationBYOKEntitled与isOrganizationBYOKEntitledCached这对函数就是如此拆分的。byok-entitlement.ts 的注释解释了这个拆分的动因getBYOKKey每个 agent block 跑一次、每次可托管工具调用跑一次工作流循环 N 项就要解析 N 次新鲜读取在组织继承场景下要付出三个串行计费查询而缓存版把稳态成本降到零。Reactcache()在 worker 里什么都不是cache()是请求作用域的。Sim 的工作流运行在 Trigger.dev worker 里那里没有 React 请求作用域——所以一个在设置页上看起来免费的cache()包裹闸门在执行路径上其实是未缓存的、逐 block 的。任何从 executor 可达的读取都需要真实缓存即上文LRUCache路线。规范将此归入 app/worker 运行时边界的总原则参见 apps/sim/executor 目录下的执行引擎代码所处的进程模型。失效Invalidation只在同进程读写时才加规范的最后一节回答了什么时候该写invalidate*Cache只有当修改值的代码与读取它的代码跑在同一个进程时才加按 key 的失效器。invalidateSessionPolicyCacheapps/sim/lib/auth/session-policy.ts#L88-L91成立的原因正是写策略的路由apps/sim/app/api/organizations/[id]/session-policy/route.ts与提供读取的是同一进程。反之权限变更通过 Stripe webhook 到达落在某一个进程而读取方分散在各自 worker 中——在那里加失效器只会暗示一种它根本无法兑现的即时性。TTL 才是真正的机制而没有失效器这一事实应当被代码注释明确说出来。byok-entitlement.ts 的resetOrganizationBYOKEntitlementCache注释就是这样自证的它只提供全量清空且仅作为测试接缝并逐句解释了为什么刻意不提供按组织的失效器。小结一条可复用的缓存决策清单把 Sim 这条规则文档压缩成决策树就是key 有明确死亡点 → 普通Map不加 TTL 也不加上限否则用LRUCachemax与ttl同时设置max是内存兜底而非运行阈值读侧用! undefined判断异步读穿透优先fetchMethod存在挂死读取风险且数据库无statement_timeout兜底时用coalesceLocally 读穿透式LRUCache且缓存写放在调用方一侧缓存闸门不缓存凭据故障走onError: throw不落缓存面向人的路径读新鲜数据面向执行的走缓存失效器只在同进程读写时添加跨进程的即时性由 TTL 承担并注释说明。这套模式在 Sim 中不是孤例而是可交叉验证的一族providers/client-cache.ts同步记忆化、lib/auth/session-policy.tsTTL 按 key 失效、lib/api-key/byok-entitlement.tscoalesceLocally 成功路径写入、lib/oauth/credential-service.ts同一coalesceLocally形态与 lib/auth/security-policy.ts按条目 TTL共同构成了这条缓存规则在仓库中的完整证据链。【免费下载链接】simSim is the collaborative workspace to build, deploy, and monitor AI agents and workflows. Used by 100,000 builders.项目地址: https://gitcode.com/GitHub_Trending/sim16/sim创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考