Effect 的 Cache 怎么让成功和失败用不同的 TTL?

下游抖了一下,一次失败的查询被缓存了十分钟,之后所有人都读到同一个错误。原因是 Effect 的 Cache 会把查找的整个 Exit 缓存起来,失败也不例外。

用 Cache.makeWith,在 timeToLive 里按结果的 Exit 返回时长:成功返回长 TTL,失败返回短 TTL。 Cache.make 是固定 TTL,makeWith 才能让 TTL 取决于结果。

下面用 TestClock 把时间快进,逐步验证条目什么时候过期、什么时候重新查。TestClock 的更多用法见 Effect 的超时和重试怎么测,才不用真等?。

timeToLive 怎么按结果区分成功和失败?

timeToLive 接收查找结果的 Exit,返回一个 Duration。典型用法是成功缓存久一点,失败缓存短一点。

import { Cache, Duration, Effect, Exit } from "effect"
import { TestClock } from "effect/testing"

const program = Effect.gen(function*() {
  let lookups = 0
  const cache = yield* Cache.makeWith(
    (key: string) =>
      Effect.suspend(() => {
        lookups++
        return key === "bad" ? Effect.fail("boom") : Effect.succeed(key.length)
      }),
    {
      capacity: 100,
      timeToLive: (exit) => (Exit.isSuccess(exit) ? Duration.minutes(10) : Duration.seconds(5))
    }
  )

  yield* Cache.get(cache, "hello")
  yield* Cache.get(cache, "hello")
  console.log(lookups) // 1:第二次命中

  yield* Effect.exit(Cache.get(cache, "bad"))
  yield* Effect.exit(Cache.get(cache, "bad"))
  console.log(lookups) // 2:失败也被缓存,第二次没有重新查

  yield* TestClock.adjust("6 seconds")
  yield* Effect.exit(Cache.get(cache, "bad"))
  console.log(lookups) // 3:失败条目 5 秒后过期,重新查

  yield* Cache.get(cache, "hello")
  console.log(lookups) // 3:成功条目还在

  yield* TestClock.adjust("10 minutes")
  yield* Cache.get(cache, "hello")
  console.log(lookups) // 4:成功条目过期
}).pipe(Effect.provide(TestClock.layer()))

Effect.runPromise(program)

timeToLive 的参数是 (exit, key),所以也可以按 key 区分。

失败也会被缓存吗?

会。上面第二次读 "bad" 没有重新查,lookups 停在 2,就是证据。失败条目要等自己的 TTL 到期才会重新查,所以失败的 TTL 要设短,而不是不设。

另外两个事实来自 JSDoc:对同一个未命中的 key,并发的 Cache.get 共享同一次进行中的查找,不会重复调用 lookup;查找的 Exit 被整体缓存,所以失败也会缓存到过期、失效或刷新为止。

怎么把批处理和带 TTL 的缓存合在一起?

RequestResolver.asCache(resolver, { capacity, timeToLive }) 把 resolver 直接变成一个以 request 为 key 的 Cache,未命中时走 Effect.request,于是你同时得到「批处理合并」与「带 TTL 的缓存、失效、刷新」。我验证了:3 个请求(含一个重复)只触发 1 批,快进 2 分钟后同一请求重新触发 1 批。

批处理本身怎么写,见 Effect 怎么把 N+1 查询合并成一批?。那篇里的 withCache 没有 TTL,需要过期时就换成这里的做法。

缓存的值本身是资源时怎么办?

值需要释放(连接、文件句柄、子进程)时,Cache 不管释放,要用 ScopedCache,见 Effect 的 ScopedCache 过期后,资源什么时候被释放?。

版本是 effect 4.0.5。TTL 的各步行为在 TestClock 下编译并运行过,asCache 的批次数也跑过。「并发 Cache.get 共享同一次查找」与「Exit 整体缓存」两条来自 JSDoc,没有单独验证。