Effect 的 ManagedRuntime 是什么?
你在 Express 里写了一批路由回调,每个回调都要用同一个数据库服务。没有 main 可以包,没有「整个进程」的入口,只有一次又一次零散的调用。
ManagedRuntime 就是为这种场景准备的:它把一个依赖已装配好的 Layer 变成长期存在的运行环境,Layer 只构建一次,每次回调都复用同一份服务。 代价是你要自己负责在合适的时机把它关掉。
背景一句话:Layer 是构建服务的配方,里面用 acquireRelease 获取的资源挂在 Layer 的 Scope 上,Scope 关闭时才释放(见 Layer 与 资源与 Scope)。要弄懂 ManagedRuntime,就是弄懂它什么时候构建这个 Layer、什么时候关闭这个 Scope。至于为什么宿主回调里不能随手 runPromise,见 Effect 程序应该在哪里 run?。
ManagedRuntime.make 做了什么?
ManagedRuntime.make(layer) 接受一个不再缺少依赖的 Layer(R 为 never),返回一个运行时对象。它有两件事要弄清楚:它懒惰地构建 Layer 且只构建一次,并且它拥有 Layer 的 Scope。
import { Context, Effect, Layer, ManagedRuntime } from "effect"
class Db extends Context.Service<Db, {
readonly query: (sql: string) => Effect.Effect<ReadonlyArray<string>>
}>()("app/Db") {
static readonly layer = Layer.effect(
Db,
Effect.gen(function* () {
yield* Effect.acquireRelease(
Effect.log("db open"),
() => Effect.log("db closed")
)
return Db.of({ query: () => Effect.succeed(["row"]) })
})
)
}
// 模块顶层只做「定义」:构造运行时对象,不会立刻构建 Layer
export const runtime = ManagedRuntime.make(Db.layer)
// 框架回调里的每一次调用,都复用同一份 Db
export const handler = (sql: string) =>
runtime.runPromise(Db.use((db) => db.query(sql)))
// 进程退出前的钩子里关闭
export const shutdown = () => runtime.dispose()Layer 是什么时候构建的?
第一次 runtime.runPromise(...) 的时候。ManagedRuntime.make 本身是纯定义,不会触发 I/O;第一次运行时 Layer 被构建,产出的服务被缓存,之后的调用复用同一份。所以数据库连接池只会创建一次。
这点很重要:它让「模块导入」仍然是无副作用的。
为什么必须调用 dispose?
因为 ManagedRuntime 持有 Layer 的 Scope,直到你调用 runtime.dispose() 才会关闭。你必须给它一个关闭的时机,否则它持有的连接永远不会被释放。
测试里尤其明显:创建了 ManagedRuntime 却不 dispose,测试跑完进程不退出,因为资源没释放。把 dispose 放进测试的收尾,或者进程退出钩子里。
能不能每个请求建一个 ManagedRuntime?
不能。运行时是长期对象,存在的意义就是复用。每请求构建,等于绕了一圈回到「每请求重建 Layer」:连接池之类的资源被反复创建、反复销毁,成本和泄漏风险都被放大。
热重载的开发服务器里会出什么问题?
模块被反复重新执行,会不断创建新的 ManagedRuntime,旧的没有被 dispose。
常见的缓解是把 runtime 挂在 globalThis 上复用,并在开发环境下明确这是一个妥协:它是为了应对宿主的模块重复执行,而不是 Effect 本身的要求。
本文整理自本站 Effect 教程中关于运行时边界的一章,没有标注对应的 effect 版本号。代码示例沿用该章原样,本次改写没有重新运行;globalThis 复用 runtime 的做法只是常见的缓解思路,没有逐个框架验证。