feat(kernelboot): ID:14 Run 增加执行期依赖校验,顺序被改坏时报可读错而非 SIGSEGV

Build() 里已有构建期校验 diagnoseUnresolvable(查声明:缺依赖/环/重名),
本单**不重复**它。缺的是执行期那一层:声明全对、图也自洽时,plan 的层顺序
仍可能被改坏(手改 Layers / 图与代码漂移 / 有人把某层提前),此时 Build 绿,
模块 Init 拿到 nil 依赖,崩在业务代码里。

实测复现(本单判据的复现形状):把 kernelmod 真实登记表算出的第 1 层
(lua/memory/onnx-parser/…)与第 2 层(llm/text-memory/knowledge-store)交换,
llm.buildProviderManager 收到 0x0 的 LuaVM:
  panic: runtime error: invalid memory address or nil pointer dereference
  [signal SIGSEGV ... addr=0x10]
  internal/lua.(*VM).ConfigureConcurrency(0x0, ...)  vm.go:213
  internal/kernelmod/llm.buildProviderManager(..., 0x0, ...)  mod.go:70

改动:Run 逐层维护 produced(**只有真跑完的层**才贡献设施,刻意不从 plan 预填),
每层 Init **之前**核对本层各模块 Requires ⊆ produced;缺则返回
*DependencyNotReadyError 并**本层一个 Init 都不调**(宁可什么都不装,也不装一半再崩)。
错误文本指名「第 N 层 / 模块 X 缺设施 [...]」并列已实际产出的设施。

判据:
  internal/kernelmod/all/runtime_dep_test.go  —— 用真实登记表算 plan 后做变异
    A 正序前缀不误报
    B 交换两层 ⇒ 判据红(指名 llm 缺 lua / text-memory 缺 memory,onnx /
      knowledge-store 缺 knowledge-migrated),且 ks.LuaVM 仍为 nil(没跑到 llm)
    C 删掉提供者整层 ⇒ 第 0 层就拒绝,IO/EventBus 仍 nil(一个模块都没跑)
  internal/kernelboot/runtimedep_test.go      —— 纯逻辑四形状 + 防 M2 自检

变异证明(4 个,逐个人工注入后跑判据,全部咬住):
  M0 撤掉守卫                → 复现原 SIGSEGV(ConfigureConcurrency(0x0))
  M1 守卫恒放行               → 5 条判据红
  M2 produced 从 plan 预填    → 退化成 Build 重复实现,顺序类判据红
  M3 报错丢模块名             → 指名类判据红

修前红/修后绿:
  修前(M0):panic SIGSEGV @ lua.(*VM).ConfigureConcurrency(0x0)
  修后:DependencyNotReadyError,文本含「第 1 层」「llm」「lua」「缺设施」

go build ./... = 0 error,go vet ./... = 0 error
go test ./internal/kernelboot/... ./internal/kernelmod/... = ok(含 -count=1)
This commit is contained in:
JianFeeeee
2026-10-24 12:50:57 +08:00
parent 653c87f5f2
commit e70c2b55b4
3 changed files with 515 additions and 0 deletions

View File

@ -230,6 +230,36 @@ func diagnoseUnresolvable(pending []Module, declared map[string]bool, owner map[
return &UnresolvableError{Missing: missing, Cycle: cycle, Declared: declaredList}
}
// verifyLayerDependencies 核对本层各模块的依赖是否**已由更早的层实际产出**。
//
// 返回 nil 表示全部就绪;否则返回 *DependencyNotReadyError(带模块名与缺失设施名)。
// 判据形状与 satisfied 一致(Requires ⊆ produced),但数据源是 produced(事实)
// 而非 Build 里的 ready(声明推演)—— 这正是它能咬住"层顺序被改坏"的原因。
func verifyLayerDependencies(li int, layer []Module, produced map[string]bool) error {
missing := map[string][]string{}
for _, m := range layer {
var lack []string
for _, r := range m.Requires() {
if !produced[r] {
lack = append(lack, r)
}
}
if len(lack) > 0 {
sort.Strings(lack)
missing[m.Name()] = lack
}
}
if len(missing) == 0 {
return nil
}
producedList := make([]string, 0, len(produced))
for f := range produced {
producedList = append(producedList, f)
}
sort.Strings(producedList)
return &DependencyNotReadyError{Layer: li, Missing: missing, Produced: producedList}
}
// RunOptions 控制执行。
type RunOptions struct {
// MaxParallel 是**层内**的并发上限(<=0 表示不限制)。
@ -270,13 +300,74 @@ type RunResult struct {
Order []string
}
// DependencyNotReadyError 是"执行期"依赖校验失败:某模块要开工,但它需要的设施
// **在已经真正跑完的层里**找不到。
//
// ★ 与 Build/diagnoseUnresolvable 的分工(**刻意不重复实现**):
//
// Build(构建期) :看**声明**——Requires 有没有人 Provides、有没有环。
// 本错误(执行期,Run):看**事实**——按这份 plan 真的跑完的层里,产出了什么。
//
// 两者不可互相替代:声明全对、图也自洽,plan 的**层顺序**仍可能被改坏
// (手改 Layers / 旧图/新代码混装 / 有人把某层提前)。那时 Build 不会报错,
// 模块的 Init 拿到 nil 依赖 → 崩在业务代码里。实测形态:把 lua 排到 llm 之后,
// llm.buildProviderManager 收到 0x0 的 LuaVM,ConfigureConcurrency 里 SIGSEGV。
//
// ⇒ 这条判据的判据对象是**实际执行进度**,不是声明,所以它咬得住"顺序被破坏"。
type DependencyNotReadyError struct {
// Layer 是出问题的层号(0 起)。
Layer int
// Missing 是 模块名 → 它缺的设施名(各自升序)。
Missing map[string][]string
// Produced 是此前各层**实际产出**的设施名(升序),供对照"哪些还没跑"。
Produced []string
}
func (e *DependencyNotReadyError) Error() string {
mods := make([]string, 0, len(e.Missing))
for m := range e.Missing {
mods = append(mods, m)
}
sort.Strings(mods)
var b strings.Builder
fmt.Fprintf(&b, "kernelboot: 第 %d 层装配前校验失败 —— 依赖未就绪", e.Layer)
b.WriteString("(启动计划的层顺序与模块声明不一致,或图与代码漂移): ")
for i, m := range mods {
if i > 0 {
b.WriteString(";")
}
fmt.Fprintf(&b, "模块 %s 缺设施 %v", m, e.Missing[m])
}
fmt.Fprintf(&b, "。此前各层已实际产出的设施: %v", e.Produced)
b.WriteString("。\n" +
" 为什么必须在这里拦住:依赖没就绪就调 Init,模块拿到的是 nil," +
"崩在业务代码里(实测 lua 排到 llm 之后 ⇒ llm 拿 0x0 LuaVM,SIGSEGV)。\n" +
" 修复:让提供者在**更早的层**(检查各模块的 Requires/Provides 是否与图一致," +
"并重新生成依赖图)")
return b.String()
}
// Run 按计划执行:逐层 Init,层内有界并行。
//
// ## 执行期依赖校验(每层 Init **之前**)
//
// 逐层维护"这一层之前**真的跑完**的模块产出了哪些设施"(produced),
// 每层开工前先核对本层各模块的 Requires ⊆ produced:
//
// · 缺则**立刻返回** DependencyNotReadyError(指名模块名前、缺哪些设施),
// 本层**一个 Init 都不调** —— 宁可什么都不装,也不要装一半再崩
// · 这一层全部成功后才把本层 Provides 记入 produced(与 syncbus 的层边界同语义)
//
// ★ 为什么不复用 Build 的结论:Build 校验的是**声明**(图能否解开)。
// 声明全对时层顺序仍可能被改坏,那时 Build 绿、Init 崩。见 DependencyNotReadyError。
//
// 失败策略(文档 §2.3.3):**任何 Init 失败都令整体失败**。
// 不静默跳过、不"带着半初始化状态继续"——后者正是病 A 的形态。
//
// 同层内若**多个**模块失败,返回第一个(按名字序,稳定);
// 但**不等同于只跑一个**:同层其余模块仍会被执行完(避免半途中断造成更乱的状态)。
// (依赖未就绪属于"开工前就发现",那时本层还没有任何模块动过,故一个都不跑。)
func Run(plan *Plan, opts RunOptions) (*RunResult, error) {
if plan == nil {
return nil, fmt.Errorf("kernelboot: 计划为 nil")
@ -284,6 +375,10 @@ func Run(plan *Plan, opts RunOptions) (*RunResult, error) {
res := &RunResult{}
var orderMu sync.Mutex
// produced 是**执行期事实**:只有真的跑完的层才贡献设施。
// 它刻意不从 plan 里预填(预填就等于相信声明 ⇒ 变回构建期校验,抓不到顺序被改坏)。
produced := map[string]bool{}
ks := opts.State
if ks == nil {
ks = NewKernelState()
@ -312,6 +407,14 @@ func Run(plan *Plan, opts RunOptions) (*RunResult, error) {
if len(layer) == 0 {
continue
}
// ★ 装配前校验:本层每个模块要的设施,必须在**此前真正跑完**的层里产出过。
// 放在这里(而不是循环中途逐个模块查)⇒ 本层一个 Init 都不调就拒绝,
// 避免"前半层已经装了一半"的脏状态。
if err := verifyLayerDependencies(li, layer, produced); err != nil {
return res, err
}
limit := opts.MaxParallel
if limit <= 0 || limit > len(layer) {
limit = len(layer)
@ -376,6 +479,14 @@ func Run(plan *Plan, opts RunOptions) (*RunResult, error) {
}
res.LayerCount++
// ★ 执行期事实:本层全部成功 ⇒ 它提供的设施才"真的存在"。
for _, m := range layer {
for _, p := range m.Provides() {
if p != "" {
produced[p] = true
}
}
}
// ★ 层边界:把本层贡献的设施标记就绪(设计 §2.6.2)。
// 在层内并发**全部完成之后**统一标记 ⇒ 就绪顺序确定、可复现。
if opts.Bus != nil {