Files
HomeAgent/internal/plugin/proc/lock.go
dev 610e9d0bbb feat(plugin): entry 双通道分派 + 共享内存 stage 数据面(Part 1 + Part 4 核心)
Part 1 加载分派骨架(迁移可逐插件推进、随时回退的前提):
- dynamic.go: 新增 binEntry/skillEntry 常量 + entryKind 枚举 + classifyEntry/detectEntryKind
  manifest entry 优先级最高(改回 plugin.so 即回退 cabi);无 manifest 时按目录探测,.bin 优先
- registry.go: tryDynamic 按 entry 分派 proc/cabi 双通道;
  entry 声明 .bin 但二进制缺失时报明确错误,不静默回退(否则'已迁移插件跑回旧通道'极难排查)
- registry.go: pluginEntryHash 候选顺序与 detectEntryKind 对齐(.bin 优先),
  否则增量重载会用错文件算 hash
- dynamic_proc_{unix,windows}.go: tryLoadProc 桩位(权限/类型校验已实现,进程管理属 Part 2)

Part 4 共享内存数据面(迁移评估 §3.3/§3.4/§3.7,最关键一环):
- proc/shm.go: 段布局(Header + ShmStageCtx 描述符数组 + append-only arena)
  相对偏移设计——各进程 mmap 到不同虚拟地址仍能正确解引用
  arena 用尽显式报错而非静默截断(§4.4 风险登记);Compact() 回收 append-only 垃圾
- proc/shmcodec.go: StageContext 16 字段跨进程编解码
  字段级描述符消除 lost update:只改 FinalText 的插件不触碰 ToolResults 描述符
  WriteDirty 只写脏字段——只读插件零写入,不可能覆盖他人改写
  Snapshot 存序列化字符串(切片共享底层数组的坑,C ABI 侧修 11.3 时已踩过)
  Extra 4 键提升为具名字段;Response 用标志位表达 nil vs 空串
- proc/lock.go: 锁仲裁回归内核(§3.7 已裁定,零 cgo)
  ForceRelease 实现实验 9 的崩溃自愈——排除 robust pthread_mutex 必要性
  重复加锁显式拒绝(否则死锁 30s);等待超时有补偿 goroutine 防锁泄漏

验证:
- proc 包 16 项测试全绿(含 -race):全字段往返/只读零写回/原地改切片识别/
  现网 sanitizer+weather 场景/5插件×40轮并发零丢失/arena 耗尽报错/压实不破坏字段/
  锁互斥·串扰拒绝·崩溃自愈·临界区串行化
- entry 分派 9 项测试全绿;go build ./... exit 0;接口冻结 git diff sdk/ 为空
2026-09-02 10:41:18 +08:00

132 lines
4.1 KiB
Go
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.

package proc
import (
"fmt"
"sync"
"time"
)
// 跨进程锁锁仲裁回归内核§3.7 已裁定,实验 3 + 实验 9 支撑)。
//
// 为什么不用 robust pthread_mutex
// - PTHREAD_PROCESS_SHARED + ROBUST 属性 Go 标准库无等价物,引入它意味着
// **为了一把锁保留 cgo**——与"C 整体退场"的目标冲突。
// - 锁仲裁回内核后,持锁进程崩溃由 cmd.Wait()/stdio EOF 检测,内核代为释放;
// 实验 9 实测无死锁、**无需 EOWNERDEAD 处理**。
// - 成本:一次 RPC 往返 19.4 µs实验 320000 次测得。stage handler 的加锁
// 频率很低(每次 stage 一两次,不是每字段一次),微秒级往返可忽略。
//
// 于是整个新架构可做到**完全无 cgo**。
// lockWaitTimeout 是插件申请 stage 锁的最长等待时间。
//
// 取 30sstage handler 自身受 60s 工具超时约束toolcall.go锁等待
// 必须显著短于它,否则超时错误会指向错误的原因。超时返回错误而非
// 静默继续——**持锁失败下改写共享段会破坏并发正确性**。
const lockWaitTimeout = 30 * time.Second
// stageLock 是内核侧为单个 stage 执行持有的互斥体。
//
// 一次 RunStage 对应一个 stageLock 实例:同阶段并发扇出的所有插件
// (含跨进程的)在此排队,语义等价于今日内置插件共享
// *StageContext 的 sync.RWMutex——这正是"保留并发扇出原始设计"
// §0.2 第 1 条:并发扇出是原始设计,不是缺陷)。
type stageLock struct {
mu sync.Mutex
// ownerMu 保护 owner/held使 ForceRelease 能安全介入
ownerMu sync.Mutex
owner string // 当前持锁的插件名,空表示未持有
held bool
}
func newStageLock() *stageLock { return &stageLock{} }
// Acquire 为 plugin 申请写锁,带超时。
//
// 同一插件重复 Acquire 会死锁stage handler 不应嵌套加锁),
// 故显式拒绝并返回错误——比让插件挂死 30s 更容易排查。
func (l *stageLock) Acquire(plugin string) error {
l.ownerMu.Lock()
if l.held && l.owner == plugin {
l.ownerMu.Unlock()
return fmt.Errorf("proc: 插件 %s 重复申请 stage 锁handler 内不应嵌套加锁)", plugin)
}
l.ownerMu.Unlock()
acquired := make(chan struct{})
go func() {
l.mu.Lock()
close(acquired)
}()
select {
case <-acquired:
l.ownerMu.Lock()
l.owner = plugin
l.held = true
l.ownerMu.Unlock()
return nil
case <-time.After(lockWaitTimeout):
// 等待超时:上面的 goroutine 可能随后拿到锁,必须让它能释放,
// 否则锁永久泄漏。用一个补偿 goroutine 等它拿到后立刻放掉。
go func() {
<-acquired
l.ownerMu.Lock()
stillFree := !l.held
l.ownerMu.Unlock()
if stillFree {
l.mu.Unlock()
}
}()
return fmt.Errorf("proc: 插件 %s 申请 stage 锁超时(%s", plugin, lockWaitTimeout)
}
}
// Release 释放写锁。非持锁者调用返回错误(防止串扰)。
func (l *stageLock) Release(plugin string) error {
l.ownerMu.Lock()
if !l.held {
l.ownerMu.Unlock()
return fmt.Errorf("proc: 插件 %s 释放未持有的 stage 锁", plugin)
}
if l.owner != plugin {
owner := l.owner
l.ownerMu.Unlock()
return fmt.Errorf("proc: 插件 %s 试图释放 %s 持有的 stage 锁", plugin, owner)
}
l.owner = ""
l.held = false
l.ownerMu.Unlock()
l.mu.Unlock()
return nil
}
// ForceRelease 在插件进程崩溃/退出时由内核代为释放其持有的锁(实验 9 的自愈机制)。
//
// 返回是否实际释放了锁。**这是"无需 robust mutex"的核心**
// 锁的所有权在内核进程,插件死亡由 cmd.Wait()/stdio EOF 检测到,
// 内核直接解锁,不存在"持锁者死亡导致全局死锁"。
func (l *stageLock) ForceRelease(plugin string) bool {
l.ownerMu.Lock()
if !l.held || l.owner != plugin {
l.ownerMu.Unlock()
return false
}
l.owner = ""
l.held = false
l.ownerMu.Unlock()
l.mu.Unlock()
return true
}
// Owner 返回当前持锁插件名(诊断用)。
func (l *stageLock) Owner() string {
l.ownerMu.Lock()
defer l.ownerMu.Unlock()
if !l.held {
return ""
}
return l.owner
}