Effect 的 Layer 怎么在退出时关闭连接池?

数据库连接池在启动时打开,退出时要关闭;更麻烦的是,中途任何一步失败,已经打开的也要关掉。手写 try/finally 很容易漏掉其中一条路径。

答案是:在 Layer 的构造体里用 Effect.addFinalizer 登记清理动作,它挂在这个 Layer 的生命周期上,Layer 被释放时执行。 释放顺序与构建顺序相反,构建中途失败时已建好的部分也会被释放。

Layer 是「构建一个服务的配方」,Layer<ROut, E, RIn> 写明产物、构建失败和构建所需(概念见 Layer 与依赖图)。资源管理就是这份配方的一部分,下面看清登记位置、顺序,以及长期运行的应用怎么办。

清理动作登记在哪里?

在 Layer.effect 的构造体里。下面的 Pool 打开时记一条日志,并登记关闭时的日志:

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

class Config extends Context.Service<Config, { readonly url: string }>()("app/Config") {
  static readonly layer = Layer.succeed(Config, { url: "db://x" })
}

class Pool extends Context.Service<Pool, {
  readonly query: (sql: string) => Effect.Effect<string>
}>()("app/Pool") {
  static readonly layer: Layer.Layer<Pool, never, Config> = Layer.effect(
    Pool,
    Effect.gen(function* () {
      const cfg = yield* Config
      yield* Effect.log("open pool " + cfg.url)
      yield* Effect.addFinalizer(() => Effect.log("close pool"))
      return { query: (sql) => Effect.succeed("rows:" + sql) }
    })
  )
}

Effect.acquireRelease 是更常用的「获取与释放配对」写法,细节见资源与 Scope。这里只需要知道:Layer 构造体里登记的清理动作,会在 Layer 被释放时执行。

构建与释放按什么顺序发生?

构建发生在程序开始使用之前,释放发生在程序结束之后。把 Pool 接进一个程序:

const PoolLive = Pool.layer.pipe(Layer.provide(Config.layer))

const program = Effect.gen(function* () {
  const pool = yield* Pool
  yield* Effect.log(yield* pool.query("select"))
})

Effect.runPromise(program.pipe(Effect.provide(PoolLive)))

输出的顺序就是生命周期:先 open pool db://x,再 rows:select,最后 close pool。

多个 Layer 时,释放顺序与构建顺序相反,因为后构建的可能还依赖先构建的。中途任何一步构建失败,已经构建成功的部分会被释放。这些由 Layer 保证,你不用自己写 try/finally。

长期运行的应用怎么写?

用 Layer.launch 把整个 Layer 当作程序本体启动。它一直运行直到被中断,中断时 Layer 的释放逻辑随之执行。进程信号与运行时边界的细节见运行时边界。

为什么不能用 Layer.succeed 提供资源?

Layer.succeed 接收的是一个现成的值,它不能表达「打开连接」「关闭连接」。有资源的服务应当用 Layer.effect 加 finalizer。

另一个相关的错误是在 Layer 构造体里 Effect.runPromise:这会把惰性的构建提前执行,丢掉失败类型与释放保证。构造体始终是 Effect,别在里面启动新的运行。

说明:文中的构建与释放顺序、失败时回收、Layer.launch 的行为按原有章节整理,这次没有重新运行,也没有逐条验证。原文没有记录所用的 effect 版本号,Effect v4 的 API 与旧版差异较大,请以你安装的版本为准。