Files
HomeAgent/id10_ci_closeout.patch
JianFeeeee 19c78ca946 fix(plugin): Registry 的 Set* 全部取写锁,读侧改一致性快照(KERNEL-REGISTRY-SETLOCK)
根因(前一条 CI 线实测):internal/plugin/registry.go 的 Set* 长期是单行
无锁直写,而 r.mu 一直存在却从未被这批 setter 使用;同时
kernelmod/agent.wirePluginSDK 与 kernelmod/pluginhost.injectBaseDeps
同处启动图第 4 层、并行执行,各自把同一批字段写一遍 ⇒ 写-写竞争。

## 一、加锁(真正的修复)

· 24 个 Set* 全部改为 r.mu.Lock() / defer Unlock():
  SetIOManager/SetEventBus/SetMemory/SetTextMemory/SetDocStore/SetKnowledge/
  SetProviderManager/SetConfigRegistry/SetPluginDir/SetDataDir/SetLuaVM/
  SetBaseAPIKey/SetKernelControl/SetToolRegistrar/SetStageRegistrar/
  SetAPIRegistrar/SetToolCleaner/SetStatusProvider/SetSupervisor/SetTracker/
  SetConfig/SetStageHost/SetDeviceAuthQuery/SetIndexer
· 读侧保住 RLock,并新增 sdkDeps + snapshotDeps():buildSDK 在**一次**
  RLock 内取齐整组依赖后立刻放锁,随后用快照装配 SDK。
  为什么不在整个 buildSDK 外套 RLock:其闭包(regOutput 等)在插件 Start
  期间才被调用,持锁跨用户代码有死锁风险;改为快照 + 放锁。
  为什么必须一次取齐:逐字段裸读既各自是一处竞争,也会让同一次装配
  看到"lua 已换新、cfg 还是旧"的撕裂组合。
· kernelControlFor / stageRegistrarFor 拆出无锁内核
  (kernelControlForCtl / stageRegistrarForSource),供快照后复用,
  避免持锁再 RLock 的自锁。
· 其余读点集中走访问器:pluginDirOrEmpty / cfgRegistryOrNil /
  toolCleanerOrNil / stageHostOrNil / eventBusOrNil / ioManagerOrNil
  (dynamic_proc.go 的 evBus 两处同步改)。
· 未改任何语义、未重命名导出符号。

## 二、去重复注入(可行,已做)

按"必要性不对称"判定:保留 pluginhost.injectBaseDeps,砍掉
agent.wirePluginSDK 里的 7 项重复写,agent 侧只留 SetStatusProvider。
理由:pluginhost 紧接着就 LoadSkipping 拉插件,且它刻意 Requires() 不含
agent("插件不必等 agent 35 秒"的收益就在这半边)——砍它会让第 4 层加载
的插件拿到全 nil 依赖;而 agent 侧写的是同一批 ks.* 的同一份值,写第二次
无新增信息。

★ 去重**不能**替代加锁:去重后 agent 仍写 SetStatusProvider,pluginhost
同时可能 buildSDK 读 status,写-读竞争仍在(实测可复现 1 条 DATA RACE)。
所以锁是修复本体,去重只是消除无意义的重复写。

## 三、验证(全部 -count=1)

判据(internal/plugin/registry_setlock_test.go):
  · TestRegistrySettersTakeWriteLock —— 源码级:24 个 Set* 函数体必须各含
    r.mu.Lock()(race 是概率性的,"锁被删了"这类回退必须确定性咬住)
  · TestSettersConcurrentWriteRace —— 两个写 goroutine 用不同值并发写同一批
    字段 + 读侧并行
  · TestBuildSDKConcurrentWithSetters —— 走真实读路径 buildSDK/snapshotDeps
    与 setter 并发(补上一条测不到的**读侧**缺口)

变异实证(每条都实跑,均报红):
  M1 删掉 SetLuaVM 的锁        ⇒ 源码判据报红 + -race 报 DATA RACE
  M2 删掉 SetConfig 的锁        ⇒ 源码判据报红 + TestBuildSDKConcurrent 报红
  M3 只删 snapshotDeps 的 RLock ⇒ -race 报红,栈指向读侧 :486/:494/:495
                                  vs 写侧 :274/:367(证明读侧回退也有人管)

race 数字(CI 线整机判据 go test -race ./internal/kernelboot/modules/):
  修复前 25 轮:2 轮报红,累计 14 条 DATA RACE
  修复后 25 轮:0 轮报红,0 条 DATA RACE
范围包(./internal/plugin/... ./internal/kernelmod/...)修复后 -race 全绿。
  go build ./... / go vet ./... 干净。

## 四、CI 侧收口(不在本分支落地)

ci/boot-graph-tests 的 race 排除表里那条
  pkg|internal/kernelboot/modules|…|KERNEL-REGISTRY-SETLOCK
现在可以删了。按任务边界本分支不碰 ci.yml,改由
id10_ci_closeout.patch(对 ci/boot-graph-tests 可干净 git apply)+
ID10_CI_CLOSEOUT.md 交给 CI 线合入。
2026-10-24 12:17:46 +08:00

85 lines
6.0 KiB
Diff
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -220,13 +220,31 @@
# ║ ① 让 ~4% 的偶发红进主干(长红 ⇒ 所有人学会忽略 ⇒ 整道门失效) ║
# ║ ② 先挡住、同时派单修根因(本表选这条) ║
# ║ ║
- # ║ **整包永久排除不被接受。** 唯一出口是给 plugin.Registry 的 Set* ║
- # ║ 加锁(修复单 KERNEL-REGISTRY-SETLOCK),修好后: ║
- # ║ · 删掉本表里 kernelboot/modules 那一行 ║
- # ║ · **复跑一次** -race 全仓确认整机判据在 -race 下也稳定 ║
- # ║ · 删掉 evidence 步骤 ② 的静态断言 ║
- # ║ 这三步没做完,本线不算收口。 ║
+ # ║ **整包永久排除不被接受。** 本表目前**已经没有** pkg 级条目。 ║
# ╚══════════════════════════════════════════════════════════════════╝
+ #
+ # ── 已收口:KERNEL-REGISTRY-SETLOCK(2026-10-24)──
+ #
+ # 原先这里有一条 pkg 级排除 `internal/kernelboot/modules`:
+ # 该包把真实整机装起来 ⇒ 第 4 层 agent 与 plugin-host 并行 Init 时,
+ # 双方都无锁直写同一 plugin.Registry 的 7 个 Set*。竞争在**内核装配**里,
+ # 不在该包代码里,所以任何装整机的用例都会撞上(实测约 4%)。
+ #
+ # 已修(分支 fix/registry-setlock):plugin.Registry 的 24 个 Set* 全部取写锁,
+ # 读侧(buildSDK)改为"一次 RLock 取整组快照后立刻放锁",读侧仍是 RLock。
+ # 并按"必要性不对称"去掉了 agent.wirePluginSDK 与 pluginhost.injectBaseDeps
+ # 的重复注入(保留 pluginhost 侧 —— 它不依赖 agent,是"插件不等 agent 35 秒"
+ # 的那半边;agent 侧只留 SetStatusProvider)。
+ #
+ # 收口三步**已全部做完**,故本表删掉了那一行:
+ # · 删掉 kernelboot/modules 排除行 ✔(本提交)
+ # · 复跑 -race 确认整机判据在 -race 下稳定 ✔(25/25 无 DATA RACE;
+ # 修前同条件 2/25 报红、累计 14 条)
+ # · 删掉 evidence 步骤 ② 的内核 setter 静态断言 ✔(本提交)
+ #
+ # ★ 注意:**去重不能替代加锁**(已实测)。去重后 agent 侧仍写
+ # SetStatusProvider,而 pluginhost 侧可能同时 buildSDK 读 status ⇒
+ # 写-读竞争仍在(实测报 1 条 DATA RACE)。所以 registry 侧的锁才是修复。
- name: 计算 -race 排除表并打印
id: skip
run: |
@@ -234,7 +252,6 @@
# 列名:粒度|目标|原因|修复单号 (用 | 分隔,原因内可含空格)
read -r -d '' TABLE <<'EOF' || true
test|internal/plugins/webui|TestOpenAIStreamIsActuallyStreaming|gzipResponseWriter 的 mode/wroteHeader/buf 无锁;handler goroutine(发 finish/[DONE])与增量写 goroutine 并发写同一 ResponseWriter(gzip.go:139 读 mode vs gzip.go:206 写 mode)|WEBUI-GZIP-RACE
- pkg|internal/kernelboot/modules|*(整包)|该包把真实整机装起来 ⇒ 第 4 层 agent 与 plugin-host 并行 Init 时,双方都无锁直写同一 plugin.Registry 的 7 个 Set*(SetLuaVM/SetBaseAPIKey/SetSupervisor/SetTracker/SetConfig/SetStageHost/SetIndexer,registry.go:231-284)。竞争在**内核装配**里,不在本包代码里;任何装整机的用例都会撞上|KERNEL-REGISTRY-SETLOCK
EOF
tests=""; pkgs=""; n=0
@@ -359,34 +376,10 @@
-run 'TestOpenAIStreamIsActuallyStreaming' -count=1 -timeout 5m 2>&1 || true)
echo " (参考)本次 DATA RACE 报告数: $(echo "$out" | grep -c 'WARNING: DATA RACE')(偶发,0 不代表已修)"
- echo "=== ② kernel: agent↔plugin-host 无锁直写 plugin.Registry(修复单 KERNEL-REGISTRY-SETLOCK)==="
- #
- # ★ 这里**不跑**偶发竞争来当证据 —— 实测该竞争单步跑 6/6 次撞不到
- # (它要 92 包并发压满才容易命中),拿它当断言必然假绿。
- # 改成**静态断言**:直接查那 7 个 Set* 是否仍是"单行无锁直写"形态。
- # 修好(改成取锁)后形态变化 ⇒ 本条失败 ⇒ 提醒删排除表。因果稳定。
- SETTERS="SetLuaVM SetBaseAPIKey SetSupervisor SetTracker SetConfig SetStageHost SetIndexer"
- REG=internal/plugin/registry.go
- locked=0; total=0
- for fn in $SETTERS; do
- total=$((total + 1))
- if grep -qE "^func \\(r \\*Registry\\) ${fn}\\([^)]*\\)[[:space:]]*\\{[^}]*\\}[[:space:]]*$" "$REG"; then
- : # 仍是单行直写(未加锁)
- else
- echo " 已不再是无锁单行: $fn"
- locked=$((locked + 1))
- fi
- done
- echo " 仍为无锁直写的 Set*: $((total - locked))/$total"
- if [ "$locked" -gt 0 ]; then
- echo "::error title=内核 setter 已加锁::KERNEL-REGISTRY-SETLOCK 可能已被修复($locked/$total 个 Set* 形态已变)—— 请从 ci.yml 排除表删掉 kernelboot/modules 那一行"
- fail=1
- fi
-
if [ "$fail" -ne 0 ]; then
exit 1
fi
- echo " 证据核对完成:两个排除项对应的缺陷均仍在(排除表有效)"
+ echo " 证据核对完成:webui 排除项对应的缺陷仍在(排除表有效)"
# ── 交叉编译:可在无 cgo 下构建的客户端/工具 ──
#