mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-06 01:07:35 +00:00
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:
@ -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 {
|
||||
|
||||
168
internal/kernelboot/runtimedep_test.go
Normal file
168
internal/kernelboot/runtimedep_test.go
Normal 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 没有判别力")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user