mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-05 16:57:32 +00:00
根因(前一条 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 线合入。
85 lines
6.0 KiB
Diff
85 lines
6.0 KiB
Diff
--- 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 下构建的客户端/工具 ──
|
||
#
|