Effect 的 LayerMap 怎么做多租户 Layer 缓存?

我连续三次顺序 provide 同一个 key 的 LayerMap,得到的是三次构建,缓存完全没起作用。

多租户的 Layer 缓存用 LayerMap:get(key) 返回一个 Layer,业务代码照常 Effect.provide;而且必须设置 idleTimeToLive,否则缓存只在借用期间存在。 这是 4.0.5 里的行为,直觉上很容易漏掉。

LayerMap 的底层是 RcMap,也就是「按 key 共享、引用计数释放」的资源;它只是把 key 对应的东西从资源换成了整套 Layer。如果想先弄清五个资源 API 各管什么,看 Effect 的 LayerMap、RcMap、Pool 该怎么选?。下面是写法和三个容易出错的行为。

多租户的 LayerMap 怎么写?

LayerMap 的 rcMap 字段直接暴露了底层的 RcMap,它让业务函数只写 yield* TenantDb,租户在边界处选择:

import { Context, Effect, Layer, LayerMap } from "effect"

class TenantDb extends Context.Service<TenantDb, {
  readonly tenant: string
  readonly query: (sql: string) => Effect.Effect<string>
}>()("app/TenantDb") {
  static readonly layer = (tenant: string) =>
    Layer.effect(
      TenantDb,
      Effect.acquireRelease(
        Effect.sync(() =>
          TenantDb.of({ tenant, query: (sql) => Effect.succeed(`[${tenant}] ${sql}`) })
        ),
        (db) => Effect.log(`release ${db.tenant}`)
      )
    )
}

class Tenants extends LayerMap.Service<Tenants>()("app/Tenants", {
  lookup: (tenant: string) => TenantDb.layer(tenant),
  idleTimeToLive: "1 minute"
}) {}

const listUsers = TenantDb.use((db) => db.query("select * from users"))

const program = Effect.gen(function*() {
  const a = yield* listUsers.pipe(Effect.provide(Tenants.get("acme")))
  const b = yield* listUsers.pipe(Effect.provide(Tenants.get("globex")))
  yield* Tenants.invalidate("acme") // 下次访问重建
  console.log(a, b)
}).pipe(Effect.provide(Tenants.layer))

Effect.runPromise(program)

listUsers 完全不知道租户的存在;用哪个租户,由 Effect.provide(Tenants.get("acme")) 在调用处决定。如果你对 Layer 和服务的关系还不熟,先看 Effect 的 Layer 是什么,依赖图怎么组装?。

同一个 key 会重复构建吗?

不会。同一个 key 连续两次 Effect.provide(Tenants.get("acme")),只构建一次;四个并发的同 key 调用也只构建一次。

为什么不设 idleTimeToLive 就每次都重建?

不写 idleTimeToLive 时,缓存只在借用期间存在。 借用结束,没人持有这个 key 了,缓存项就没了,下一次顺序调用只能重新构建。我连续三次顺序 provide 同一个 key,得到三次构建。

想真正跨请求复用,必须设置 idleTimeToLive,比如上面例子里的 "1 minute":最后一个持有者走后,缓存项再保留这么久。

invalidate 之后,正在用的租户会怎样?

invalidate 会让当前缓存项被移除,下一次访问重新构建;已经借出的 context 在其 scope 关闭前仍可用。所以刷新租户配置时不用担心打断正在处理的请求。

key 空间是开放的还是固定的?

lookup 是函数,key 空间可以是开放的。key 空间固定时用 layers 选项(或 LayerMap.fromRecord),类型上 key 会收窄成字面量联合,写错租户名在编译期就能发现。

另外 contextEffectOption 只读已缓存的项,不会触发构建,适合探测。

还有一件事要记住:LayerMap 在 4.0.5 里是 unstable,小版本里 API 可能变。我建议只在应用层用一处封装,别让它散落在业务代码里。标签含义见 Effect v4 的稳定性标注:@stability 标签取代 unstable 目录。

版本是 effect@4.0.5。同 key 只构建一次、invalidate 后重建、不设 idleTimeToLive 时顺序三次构建三次,这些行为在这个版本上跑过;LayerMap 的稳定性标签来自扫描该版本的 dist/*.d.ts,它是 unstable,后续版本可能变。