|
|
ca956f69bf
|
fix(update): 升级后第一次启动被误回滚(真机 OTA 实测)+ 关闭期 nil 解引用
## 一、★★★ 升级永远无法生效(rollback.go)
真机跑 OTA 闭环时暴露:
-apply 输出:✅ 升级完成:1.4.0dev (commit ca5b1b1)
重启后日志:ROLLED BACK kernel 1.4.0dev → 1.4.0dev(上一次升级未能确认启动)
原因:pending 标记是在**替换之前**写的(这是对的,见下),所以升级后的
**第一次启动必然看到它**。而原实现"只要标记在就回滚",从不比对当前运行的
是哪个版本 ⇒ 每次升级都在第一次启动时被自己撤销。
正确语义(两段式确认的本意)是比对版本:
当前版本 == 标记里的 To ⇒ 新版本**已经跑起来了** ⇒ 确认、清标记、继续
当前版本 != To(或读不到) ⇒ 新版本没能成为运行版本 ⇒ 回滚到备份
标记仍然必须写在替换**之前**:反过来(替换后才写)会在"替换成功但没来得及
写标记就崩了"时丢标记,那时就不知道该回滚了。
两条判据(用真能执行 `--version` 的脚本做假二进制 —— 原来那些用例写死字节、
根本读不出版本,只覆盖了"读不到 ⇒ 回滚"这一条分支,所以抓不到本缺陷):
TestRollback_ConfirmsWhenRunningTargetVersion 跑的是 To ⇒ 不回滚 + 清标记
TestRollback_RollsBackWhenRunningOldVersion 跑的是 From ⇒ 必须回滚
## 二、每次重启必现两条 panic(proc/host.go)
[stage] tool xxx handler panic: invalid memory address or nil pointer dereference
github.com/.../proc.(*unifiedRegion).generation(...)
关机/重启时 SharedRef 统一段先被拆掉,而在途的工具调用(stage handler /
cleaner proxy)仍会走到 Host.Generation() ⇒ 解引用 nil。panic 虽被 recover,
但把"正常关闭"伪装成故障,并刷屏掩盖真问题。加 nil 守卫(连 h 一起判)。
## 三、下载客户端 20s 总超时(release.go)
`DownloadLatestRelease` 复用了查询用的 kernelHTTPClient(Timeout=20s,
那是**整条请求**上限,含读完 body)。而发行包近 900MB ⇒ 必然
`context deadline exceeded ... while reading body`,"从 Release 自动更新"
永久失败,且错误看起来像网络问题。
改为独立下载客户端:建连/TLS/响应头仍分层设超时(卡住能失败),
读 body 不设总时长。
|
2026-10-26 14:03:05 +08:00 |
|
|
|
5b759e80d7
|
feat(update): 更新进度走 EventBus + SSE(穿越进程重启的通知通道)
## 为什么必须走事件总线(用户口径:为什么不走 eventbus)
升级是**长耗时且会让进程消失**的动作。此前进度只往 log.Printf 写,
WebUI 要看得自己轮询 /kernel/update —— 而轮询在重启瞬间必然断线,
断线那一刻恰恰是用户最需要知道"升到哪一步/失败了/已回滚"的时刻。
EventBus 是内核既有的发布订阅机制(同步投递、handler panic 隔离),
用它不需要新造一条通知通道。
## 三个新事件类型
update_check 检查结果(只读,不改动)
update_progress 升级步骤:verify/backup/replace/restart/rollback/done/failed
update_result 最终结果(含回滚轨迹)
## ★ 顺序约束:先发事件,再动作
升级尾部几步(replace → restart)会让进程消失。顺序一反,最后几步的
事件永远发不出去。所以每个 publish* 都紧贴在其描述的动作**之前**。
## 直接 import events,不搞接口垫层
第一版我写了 EventPublisher 接口 + map[string]interface{} payload 想
"避免依赖",那是自欺欺人:events 包**不依赖任何内部包**(已验证),
import 它不会成环;而垫层会把事件降级成无类型 map,丢掉编译期检查。
已回退该设计,改为直接传 *events.Bus。
## SDK 补 EventBus() getter
PluginSDK 原先只有 Publish/Subscribe,没有暴露总线对象。而长流程
(升级闭环)需要把总线本身传下去持续发事件 —— 只给 Publish 的话,
每个调用点都要手动转发一遍。
## SSE 通路(守卫判据逼出来的正确修法)
TestEveryPublishedEventHasAnOutwardPath 抓到三个新事件"有发布者但无对外
通路"——症状是「发了、没报错、客户端收不到」。修法不是加白名单豁免,
而是真的接进 SSE:三者都进 sseEventRing(不只是实时推送),
断线重连要能补到**最终结论**(update_result),否则用户重连后只看到
"开始升级"而没有结局。
## 调用点
· pluginmgr(kernel_update):p.sdk.EventBus() → DoUpgradeBus
· webui(/kernel/update):h.sdk.EventBus() → runKernelUpgrade(pkg, bus)
· CLI:bus=nil,退化成纯 printf(CLI 没有总线)
|
2026-10-26 09:23:58 +08:00 |
|
|
|
d17e261b76
|
feat(update): update 注册为内核标准模块(第 0 层)+ 启动失败自动回滚
## 为什么必须是第 0 层(用户口径)
update 肩负「启动失败回滚上一次升级」的职责,该职责必须**早于所有模块**。
若排在后面:升级后新内核在 memory/llm 阶段崩了,而回滚还没跑 ⇒ 机器进
崩溃循环,只能靠 guard 的 failback(受限启动)兜底。failback 是最后手段,
不该承担"换错二进制"这种**已知可回滚**的故障。
⇒ Requires() 为空 ⇒ 依赖图天然把它放 Layers[0]。
## 两段式升级确认(rollback.go)
升级成功 ──→ 写 pending 标记("这个版本尚未被确认能启动")
↓
重启 → 内核启动 → update 模块(第 0 层)读标记
↓
┌────────────┴────────────┐
标记还在 标记已不在
(上次升级没能活到清除) (上次升级已确认成功)
↓ ↓
回滚到备份版本 正常继续 + 清标记
清除时机:模块跑完自检后立刻清。只依赖"进程活着且执行到第 0 层",
不依赖其他模块 ⇒ 不会误判。
**为什么不用 TTL / 重启次数**:那是 guard 已有的 failback,但判据是
"崩了几次",不是"这次升级该不该撤销"。用重启次数会在"新版本偶发崩溃"时
误回滚 —— 而那种情况该修的是崩溃,不是回退版本。
## 分层(与仓库其他模块同一惯例)
internal/update/ 业务实现(三端共用:CLI/WebAPI/agent 工具)
internal/kernelmod/update/ 适配层(读 ks → 调业务包 → 写回 ks)
参照 supervisor 模块:实现在 internal/supervisor,模块里只做装配。
分开的理由:internal/update 被三端调用,它们都没有 KernelState。
## 标记写入时机(一个真实的顺序坑)
MarkPendingUpgrade 在**替换二进制之前**调用。反过来(替换后才写)会在
"替换成功但没来得及写标记就崩了"时丢标记 ⇒ 下次启动不知道该回滚。
且写标记失败 ⇒ **放弃本次升级**(未替换任何文件):没有标记就没有回滚
能力,而不带回滚的升级等于赌运气。
## 回滚失败必须响亮
备份不存在时返回 error,调用方(模块 Init)必须中止启动 —— 继续装配等于
用一个已知起不来的版本。错误文案带备份路径与标记路径(删标记即可放弃回滚),
人能照着修。
## 判据
internal/update/rollback_test.go 10 条,2 条经变异验证:
· TestRollback_RestoresBackup —— 二进制+depgraph 同批回滚、标记清除、
坏版本留档 homed-broken-*、版本轨迹记录
变异:回滚后不清标记 ⇒ 红
· TestRollback_MissingBackupIsLoud —— 备份缺失时报错且可执行
变异:静默返回 nil ⇒ 红
· TestPendingMarker_AbsentIsNotError —— 无标记返回 (nil,nil) 而非 error
(否则每次启动都走"回滚失败"分支 ⇒ 内核永远起不来)
· TestPendingMarker_CorruptIsActionable —— 标记损坏时提示可删
· TestStateSummary_* —— 回滚信息进 healthcheck;nil 安全
## 顺带修正的既有判据
· runtime_dep_test.go:L0 设施集合从 2 个变 3 个(新增 kernel.version),
改为断言**集合**而非顺序(注册表本就是乱序的)
· facilities.go:新增 FacVersion,并登记为 internalOnlyFacilities
(它是启动时序事实,不是插件契约 —— facilities_sync_test 的守卫正确拦住了
"把内部标记泄漏成对外契约")
三个 DoUpgrade 调用点补 dataDir 参数(标记落点必须与内核 --data 一致)。
|
2026-10-26 02:08:29 +08:00 |
|