mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-06 01:07:35 +00:00
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 线合入。
This commit is contained in:
@ -24,7 +24,6 @@ import (
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kernelboot"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/kernelmod"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/memory"
|
||||
"github.com/JianFeeeee/HologramAgent/internal/supervisor"
|
||||
"github.com/JianFeeeee/HologramAgent/pkg/types"
|
||||
|
||||
agentPkg "github.com/JianFeeeee/HologramAgent/internal/agent"
|
||||
@ -246,15 +245,30 @@ func buildAgent(k *kernelboot.KernelState) *agentCore.Agent {
|
||||
return agent
|
||||
}
|
||||
|
||||
// wirePluginSDK 把内核依赖注入插件注册表(原 cmd/homed 的整块平移)。
|
||||
// wirePluginSDK 注入**只有 agent 才知道**的那两项(KERNEL-REGISTRY-SETLOCK 去重后)。
|
||||
//
|
||||
// ## 为什么这里不再注入那 7 项底层依赖(2026-10-24 去重)
|
||||
//
|
||||
// 原先本函数与 pluginhost.injectBaseDeps 各自把 LuaVM/BaseAPIKey/Supervisor/
|
||||
// Tracker/Config/StageHost/Indexer 写一遍,而两个模块同处启动图第 4 层、
|
||||
// **并行执行** ⇒ 同一批字段被两个 goroutine 无锁写(实测 4% 概率报 DATA RACE)。
|
||||
//
|
||||
// 判掉哪一侧:留 pluginhost,砍这边。理由是**必要性不对称**:
|
||||
//
|
||||
// · pluginhost 的注入是**必需**的 —— 它自己紧接着就 LoadSkipping 拉插件,
|
||||
// 而它刻意 `Requires()` 不含 agent(核心收益:插件不必等 agent 那 35 秒)。
|
||||
// 砍掉它 ⇒ 第 4 层加载的插件拿到全 nil 依赖。
|
||||
// · agent 这边注入的这批值是**同一批 ks.* 字段的同一份值**,写第二次
|
||||
// 没有新增信息;砍掉它 ⇒ 语义不变(pluginhost 侧的 nil 守卫更严,
|
||||
// Supervisor 只因 FacSupervisor 已在 Requires 里而恒非 nil)。
|
||||
//
|
||||
// ## 注意:去重**不能**替代加锁
|
||||
//
|
||||
// 去重后本函数仍有 SetStatusProvider / SetDeviceAuthQuery 两项要写,而
|
||||
// pluginhost 同时可能正在加载插件(buildSDK 读 status)⇒ 写读依然并发。
|
||||
// 所以 registry 侧 Set*/读侧的锁是**真正的修复**,去重只是消除了一处
|
||||
// 无意义的重复写。两件事都做,缺一不可。
|
||||
func wirePluginSDK(k *kernelboot.KernelState) {
|
||||
k.PluginRegistry.SetLuaVM(k.LuaVM)
|
||||
k.PluginRegistry.SetBaseAPIKey(k.BaseAPIKey)
|
||||
k.PluginRegistry.SetSupervisor(supervisor.NewSDKAdapter(k.Supervisor))
|
||||
k.PluginRegistry.SetTracker(k.Tracker)
|
||||
k.PluginRegistry.SetConfig(k.Cfg)
|
||||
k.PluginRegistry.SetStageHost(k.StageHost)
|
||||
k.PluginRegistry.SetIndexer(k.Memory.Indexer)
|
||||
k.PluginRegistry.SetStatusProvider(k.Agent)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user