实现插件热重载与状态迁移
用同一个对象引用验证代码更新、状态保留、迁移和候选版本失败后的可用性。
实现插件热重载与状态迁移
进程没有退出,方法已经更新,旧状态却还在:这才是热重载需要处理的问题。重新 import 文件只能获得新代码,不能回答运行中的对象、旧调用和状态该怎么办。
本文接着上一篇的检查计数插件运行。先把每次加 1 改成加 2,再提升状态版本,最后故意让候选版本加载失败。每一步都有可观察的数值。
示例固定 Chord 1.5.0,目录是 columns/coding-agent/examples/plugin-development/。先理解 contract.ts 与 facet.ts,再运行 npm run reload。
同一个名字匹配同一份实现
重载始终使用 facet 名 lesson/inspection、契约名 lesson.inspection、相同 input 和 cell 名 count。如果每次创建随机名字,宿主看到的是新增对象,无法按原身份接续状态。
inspectionFacet(2) 只改变方法行为,状态版本仍为 1;inspectionFacet(2, 2) 把状态版本提升到 2,并用迁移函数转换旧值。这是教学设定:真实项目应该按业务含义迁移,不能给旧数据随意加一个常数。
完整运行过程
import { createHost } from "@earendil-works/chord"
import { Inspection } from "./contract.js"
import { inspectionFacet } from "./facet.js"
const host = createHost({ name: "server", self: { kind: "server" } })
try {
await host.load([inspectionFacet(1)])
const ref = await host.get(Inspection, {})
try {
await host.root((ctx) => ref.run({}, ctx))
console.log("before", ref.count.value)
const changed = await host.reload([inspectionFacet(2)])
await host.root((ctx) => ref.run({}, ctx))
console.log("after-code", ref.count.value, changed.changed)
const migrated = await host.reload([inspectionFacet(2, 2)])
console.log("after-migration", ref.count.value, migrated.gated)
try {
await host.reload([
{
name: "lesson/inspection",
setup() {
throw new Error("故意制造候选版本失败")
},
},
])
throw new Error("预期重载失败,却意外成功")
} catch (error) {
if (!(error instanceof Error) || !error.message.includes("故意制造")) {
throw error
}
console.log("reload-rejected", error.message)
}
await host.root((ctx) => ref.run({}, ctx))
console.log("after-rejected-reload", ref.count.value)
} finally {
ref.release()
}
} finally {
await host.close()
}关键输出是:
before 1
after-code 3
after-migration 103
reload-rejected ...故意制造候选版本失败...
after-rejected-reload 105第一次调用得到 1。方法更新后同一个引用再次调用,结果是 3,说明代码改变而旧值没有清零。状态版本提升后变成 103,说明迁移消费了已有值。候选版本失败后调用仍能变成 105,说明本例旧实现仍可服务。
changed 和 gated 报告帮助定位发生了什么,但不把内部地址格式当作稳定业务 API;断言具体行为比硬编码打印格式更有价值。
契约版本和状态版本不是一回事
迁移接收 unknown,验证旧版本和值后才能转换。跳过多个版本时,迁移也要明确处理 from,不能永远假定来源只比当前少一版。Schema 发生变化却不提升状态版本,不能期待宿主猜测迁移意图。
运行中的调用如何处理
兼容代码更新可以让既有调用按旧代码完成,新调用使用新代码。需要迁移或替换不兼容资源时,运行时会进入 gate 等待相应调用边界;等待有 deadline,不能保证所有长任务永远都能顺利迁移。
本例刻意在每个 root 结束后发起 reload,没有验证并发长任务、强制退休或集群投票。生产验收应再加入:重载期间取消、未完成调用、迁移抛错、资源打开失败、重复重载,以及远端 host 无响应。方法的外部写入需要独立幂等策略。
失败回退的边界
本例让候选 facet 的 setup 立即抛错,观察到 reload 被拒绝且原实现继续工作。这个结果不能扩大为“所有失败都能撤销”:如果插件在准备阶段直接发了外部请求,那次请求可能已经发生。
应把不可撤销操作从初始化中移出,让资源按运行时生命周期声明,并为外部副作用设计提交、幂等与补偿。热重载保存的是运行时能够管理的对象关系;操作系统、数据库和第三方服务仍有各自的事务边界。
依据与版本
示例依据已发布的 @earendil-works/chord@1.5.0 包内 README、类型声明与本地执行结果。在线主分支已有 v2 设计材料,createPlugins、plugins/runtime 等写法不能直接混入本篇的 1.5 示例。