技术·MiniCordis:从零实现一个插件化 Runtime · 第 6 篇·2026-08-27·8 分钟

第五篇:失败隔离与 Child Context

这一篇要解决的问题是:当 Runtime 进入更真实的运行环境以后,为什么仅仅有安装、撤销、事件和依赖系统还不够。为了把这个问题讲清楚,我们会先看一个 Plugin 出错时为什么不能把整个系统拖垮,再看为什么一个真正可用的 Runtime 往往需要的不只是单个 Context,而是一棵可以继承与隔离的 Context 树。

1. 这一篇要解决的问题

到上一篇为止,MiniCordis 已经能够安装 Plugin、管理 cleanup、广播事件,并对动态依赖做响应式激活。从教学上看,核心模型其实已经闭合了。但如果想让这套模型再往真实环境靠近一点,还会马上遇到两个问题。

第一个问题是失败隔离。某个 Plugin 在安装过程中抛错、或者某个 cleanup 在卸载时抛错,Runtime 应不应该因此完全停摆?显然不应该。一个模块的失败不应该连坐其他模块。

第二个问题是层次结构。真实系统里经常会有“共享一部分全局能力,但局部又需要自己的覆写”的场景。比如两个 session 共享同一个 telemetry,但各自使用不同的 greeter 或 model。如果只有一个平面的 Context,这种局部配置就很难表达。

所以这一篇真正要解决的问题是:

text
一个 Plugin 出错时,系统怎样继续运行?
Runtime 为什么最终需要一棵 Context 树,而不只是一个全局 Context?

这对应 mini-cordis 里的 v0.10 和 v0.11。

这一篇的主线可以先压缩成下面这条路径:

text
局部出错不能拖垮整体
   ↓
teardown 需要失败隔离
   ↓
局部配置不能污染全局
   ↓
Context 从单点变成一棵树
失败隔离与 Context 树总览图
失败隔离与 Context 树总览图

2. 上一阶段卡在哪里

如果没有失败隔离,那么任何一个 setup 或 cleanup 中抛出的异常,都可能中断整个安装或撤销流程。这意味着系统越是插件化,越容易被局部故障拖垮。一个 Runtime 如果不能保证“出错只影响出错的那一块”,它的生命周期管理就很难真正可靠。

另一方面,如果没有 Child Context,那么所有 Plugin 都只能共享同一张 service 表、同一组监听器和同一个依赖空间。这样一来,只要两个局部场景都想使用同名 service,但配置不同,就会立刻撞在一起。系统只能全局唯一,无法表达局部覆写。

所以这一阶段的问题,本质上是在问:

Runtime 除了要会管理能力,还要不要会隔离风险、隔离作用域、隔离局部配置?

答案显然是要。

3. 这一篇引入什么机制

在失败隔离这一侧,MiniCordis 的思路并不是让错误消失,而是让错误被上报、被观察,同时不阻断其他 cleanup 或其他 Plugin 的运行。它把 teardown 循环统一收敛成一个尽量不向外抛错的内部方法,单个 cleanup 出错时只做两件事:上报错误,继续处理剩余项。

于是系统开始具备一种很重要的性质:

text
单点失败 ≠ 全局失败

在 Child Context 这一侧,MiniCordis 增加的是:

ts
const child = ctx.fork()

第一版规则很简单:

text
Child 可以读取 Parent Service
Child 可以注册自己的 Service
Child Service 优先于 Parent
Dispose Child 不影响 Parent
Parent dispose 时 Child 会被级联处理

这样一来,Runtime 的结构就从一个点变成了一棵树。每个 child 都有自己的本地状态,但又可以沿着 parent 链读取上层能力。局部配置和共享能力第一次被同时表达出来。

4. 代码里真正加了什么

失败隔离这一部分,最关键的是把所有 teardown 逻辑收敛到同一处:

ts
private teardown(scope: Cleanup[]): void {
  while (scope.length) {
    const cleanup = scope.pop()!
    try {
      cleanup()
    } catch (error) {
      this.reportError(error)
    }
  }
}

随后 dispose()、runInScope() 里的回滚,以及其他需要循环 cleanup 的地方都统一复用它。这样做的好处是,隔离策略只需要在一处定义,整个 Runtime 就都能继承这条语义。

Child Context 这一部分,则主要是给 Context 增加一个 parent 指针,并让 get() 和 has() 在本地查不到时继续向上问:

ts
constructor(private readonly parent: Context | null = null) {}

get(name: string): unknown {
  if (this.services.has(name)) {
    return this.services.get(name)
  }
  return this.parent?.get(name)
}

fork() 本身反而并不复杂,它做的事情主要是:创建一个新的 Context,把它的 parent 指向当前节点,并把 child.dispose() 注册进当前 scope,这样父级撤销时就能级联处理子级。

这里有一个很值得留意的点:无论是失败隔离还是 Child Context,MiniCordis 都没有为它们重新发明一整套新模型。前者复用了现有的 teardown 概念,后者复用了现有的 scope 级联概念。也就是说,Runtime 越往前走,设计不是越来越散,而是在不断检验早期抽象是否足够通用。

5. 这一篇的结论

这一篇最重要的结论是:一个真正能长期运行的 Runtime,除了要管理能力的安装、撤销和依赖,还必须能隔离失败,并表达局部作用域。失败隔离回答的是“局部错误如何不拖垮全局”,Child Context 回答的是“局部配置如何建立在共享能力之上”。这两件事都不是锦上添花,而是 Runtime 走向真实场景时迟早会遇到的基础问题。

下一篇会把前面所有机制重新拼回一个完整 Demo。到那时我们就能真正验证:这些机制放在同一个场景里,到底是在互相配合,还是只是在各自单独成立。