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 {

View File

@ -0,0 +1,168 @@
package kernelboot
// 执行期依赖校验(verifyLayerDependencies)的纯逻辑判据(2026-10-24,ID:14)。
//
// ## 为什么与 Build 的判据分开
//
// Build 的判据(TestBuildRejectsMissingDependency / …Cycle …)验的是**声明**:
// 一个模块 Requires 了没人 Provides 的设施,或成环 ⇒ Build 就得拒绝。
//
// 本文件的判据假设**声明完全合法**(Build 能算出图),只把算好的 Plan 的
// 层顺序改坏 —— 这才是 Build 绿而 Run 崩的那类故障(实测 SIGSEGV 见
// internal/kernelmod/all/runtime_dep_test.go)。
//
// ## 覆盖的四类形状
//
// ① 正序(正确开销) ⇒ 不误报
// ② 提供者排到消费者之后 ⇒ 指名模块名 + 缺的设施
// ③ 提供者整层缺失 ⇒ 第 0 层就拒绝,一个 Init 都不跑
// ④ 层内多模块各缺不同依赖 ⇒ 逐个模块报(不是只报第一个)
//
// 另有一条"M2 型变异"的自检:如果 produced 从 plan 预填(拿声明当事实),
// ② ③ 会变成**永不触发**的假绿 —— 判据必须能把它抓出来。
import (
"errors"
"strings"
"testing"
)
// chainPlan 造一个三层的合法计划:c → b → a(c 无依赖,a 最上层)。
func chainPlan(t *testing.T) *Plan {
t.Helper()
mods := []Module{
&fakeModule{name: "a", provides: []string{"fa"}, requires: []string{"fb"}},
&fakeModule{name: "b", provides: []string{"fb"}, requires: []string{"fc"}},
&fakeModule{name: "c", provides: []string{"fc"}},
}
plan, err := Build(mods)
if err != nil {
t.Fatalf("前置:声明必须合法(本文件只测顺序被改坏): %v", err)
}
return plan
}
// ★★★ ① 正序不误报。
func TestRunGuardAcceptsCorrectOrder(t *testing.T) {
if _, err := Run(chainPlan(t), RunOptions{}); err != nil {
t.Fatalf("★ 合法顺序被判成缺依赖(误报): %v", err)
}
}
// ★★★ ② 提供者排到消费者之后 ⇒ 指名道姓报错,且 Init 一个都没跑。
func TestRunGuardRejectsSwappedLayers(t *testing.T) {
plan := chainPlan(t)
// 把 b(提供 fb)与其消费者 a 交换:a 先跑,它要的 fb 还没产出。
swapped := &Plan{Layers: [][]Module{
plan.Layers[0], // c
plan.Layers[2], // a ← 原本在最后
plan.Layers[1], // b
}}
var inited []string
for _, l := range swapped.Layers {
for _, m := range l {
mm := m.(*fakeModule)
inner := mm.onInit
mm.onInit = func() {
inited = append(inited, mm.name)
if inner != nil {
inner()
}
}
}
}
_, err := Run(swapped, RunOptions{})
if err == nil {
t.Fatal("★ 顺序被改坏却装配成功 —— 校验没生效(模块会拿到 nil 依赖)")
}
var dnr *DependencyNotReadyError
if !errors.As(err, &dnr) {
t.Fatalf("★ 期望 *DependencyNotReadyError,实际 %T: %v", err, err)
}
if got, want := strings.Join(dnr.Missing["a"], ","), "fb"; got != want {
t.Errorf("★ 缺依赖报错不准:期望 a 缺 fb,实际 %v", dnr.Missing)
}
if len(inited) != 1 || inited[0] != "c" {
t.Errorf("★ 违规层不该有任何模块被 Init(只应跑过 c 层),实际跑了 %v", inited)
}
for _, sub := range []string{"第 1 层", "模块 a", "fb"} {
if !strings.Contains(err.Error(), sub) {
t.Errorf("★ 报错文本缺少 %q(原文: %s)", sub, err.Error())
}
}
}
// ★★★ ③ 提供者整层缺失 ⇒ 第 0 层就拒绝,produced 为空。
func TestRunGuardRejectsMissingProviderLayer(t *testing.T) {
plan := chainPlan(t)
cut := &Plan{Layers: [][]Module{plan.Layers[1], plan.Layers[2]}}
_, err := Run(cut, RunOptions{})
if err == nil {
t.Fatal("★ 提供者整层缺失却装配成功")
}
var dnr *DependencyNotReadyError
if !errors.As(err, &dnr) {
t.Fatalf("★ 期望 *DependencyNotReadyError,实际 %T: %v", err, err)
}
if dnr.Layer != 0 {
t.Errorf("★ 应在第 0 层拒绝,实际第 %d 层", dnr.Layer)
}
if len(dnr.Produced) != 0 {
t.Errorf("★ 第 0 层之前不该有产出,实际 %v", dnr.Produced)
}
}
// ★★★ ④ 同层多模块各缺不同依赖 ⇒ 逐个报(不是只报第一个,否则要修 N 轮)。
func TestRunGuardReportsEveryMissingModuleInLayer(t *testing.T) {
// 把 a 与 b 放进同一层,且都不提供任何东西 ⇒ 两者都缺依赖。
merged := &Plan{Layers: [][]Module{
{&fakeModule{name: "a", requires: []string{"fb"}},
&fakeModule{name: "b", requires: []string{"fc"}}},
}}
_, err := Run(merged, RunOptions{})
if err == nil {
t.Fatal("★ 缺依赖却装配成功")
}
var dnr *DependencyNotReadyError
if !errors.As(err, &dnr) {
t.Fatalf("★ 期望 *DependencyNotReadyError,实际 %T: %v", err, err)
}
if len(dnr.Missing) != 2 {
t.Fatalf("★ 只报了 %d 个模块缺依赖(应 2 个,否则要修 N 轮才看得全): %v",
len(dnr.Missing), dnr.Missing)
}
if strings.Join(dnr.Missing["a"], ",") != "fb" || strings.Join(dnr.Missing["b"], ",") != "fc" {
t.Errorf("★ 报错内容不准: %v", dnr.Missing)
}
}
// ★★★ 自检(防 M2 型变异):把 produced 预填成"全部声明" ⇒ 上面 ②③ 必须红。
//
// 这条不是测被测代码,而是测**判据具不具备判别力**:
// 若有人把执行期校验改成"从 plan 预填 produced"(等于相信声明),
// 它就退化成 Build 的重复实现,抓不到顺序被改坏 —— 那时本文件的 ②③
// 会变绿(假绿)。所以这里直接把这种实现形态跑一遍,要求它**确实**放行违规,
// 从而证明 ②③ 的红不是"任何实现都会红"。
func TestGuardPrefilledFromPlanIsDetectablyWeaker(t *testing.T) {
plan := chainPlan(t)
prefilled := map[string]bool{}
for _, l := range plan.Layers {
for _, m := range l {
for _, p := range m.Provides() {
prefilled[p] = true
}
}
}
// 违规层:a 在第 0 层,但它要的 fb 由第 2 层的 b 提供。
layer := []Module{plan.Layers[2][0]}
if err := verifyLayerDependencies(0, layer, prefilled); err != nil {
t.Fatalf("★ 预填 produced(拿声明当事实)竟报错 —— 说明本判据的选择无法区分两种实现,需重设: %v", err)
}
// 同一层、同一顺序,用**空** produced(真事实)必须报错 ⇒ 判据有判别力。
if err := verifyLayerDependencies(0, layer, map[string]bool{}); err == nil {
t.Fatal("★ 空 produced 也不报错 —— verifyLayerDependencies 没有判别力")
}
}