Files
HomeAgent/internal/plugin/proc/lock_test.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

185 lines
4.3 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 (
"strings"
"sync"
"testing"
"time"
)
// 锁仲裁回归内核§3.7 已裁定)的行为验证,含实验 9 的崩溃自愈机制。
func TestStageLock_MutualExclusion(t *testing.T) {
l := newStageLock()
if err := l.Acquire("A"); err != nil {
t.Fatalf("A 应能获得锁: %v", err)
}
if l.Owner() != "A" {
t.Errorf("Owner 应为 A实际 %q", l.Owner())
}
// B 在 A 持锁期间不得进入
entered := make(chan struct{})
go func() {
_ = l.Acquire("B")
close(entered)
}()
select {
case <-entered:
t.Fatal("A 持锁期间 B 不应获得锁")
case <-time.After(50 * time.Millisecond):
}
if err := l.Release("A"); err != nil {
t.Fatalf("A 释放失败: %v", err)
}
select {
case <-entered:
case <-time.After(2 * time.Second):
t.Fatal("A 释放后 B 应获得锁")
}
if l.Owner() != "B" {
t.Errorf("Owner 应为 B实际 %q", l.Owner())
}
_ = l.Release("B")
}
// 非持锁者不得释放他人的锁(防止串扰导致并发正确性被破坏)。
func TestStageLock_ReleaseByNonOwnerRejected(t *testing.T) {
l := newStageLock()
if err := l.Acquire("A"); err != nil {
t.Fatalf("Acquire: %v", err)
}
defer l.Release("A")
err := l.Release("B")
if err == nil {
t.Fatal("非持锁者释放应被拒绝")
}
if !strings.Contains(err.Error(), "试图释放") {
t.Errorf("错误信息应说明串扰,实际: %v", err)
}
if l.Owner() != "A" {
t.Errorf("A 应仍持锁,实际 owner=%q", l.Owner())
}
}
func TestStageLock_ReleaseWithoutHoldRejected(t *testing.T) {
l := newStageLock()
if err := l.Release("A"); err == nil {
t.Fatal("未持锁时释放应报错")
}
}
// handler 内嵌套加锁会死锁,应显式拒绝而不是让插件挂死到超时。
func TestStageLock_ReentrantAcquireRejected(t *testing.T) {
l := newStageLock()
if err := l.Acquire("A"); err != nil {
t.Fatalf("Acquire: %v", err)
}
defer l.Release("A")
err := l.Acquire("A")
if err == nil {
t.Fatal("同一插件重复加锁应被拒绝(否则死锁 30s")
}
if !strings.Contains(err.Error(), "重复申请") {
t.Errorf("错误信息应说明重复加锁,实际: %v", err)
}
}
// 实验 9 的核心:持锁进程崩溃后内核代为释放,后续插件不死锁。
// 这条彻底排除了 robust pthread_mutex 的必要性 —— 整个架构零 cgo。
func TestStageLock_ForceReleaseOnPluginCrash(t *testing.T) {
l := newStageLock()
// 插件 X 拿锁后"崩溃"(不调用 Release
if err := l.Acquire("X"); err != nil {
t.Fatalf("X Acquire: %v", err)
}
if !l.ForceRelease("X") {
t.Fatal("内核应能强制释放崩溃插件持有的锁")
}
if l.Owner() != "" {
t.Errorf("强制释放后应无持有者,实际 %q", l.Owner())
}
// 插件 Y 随后必须能正常拿到锁(无死锁)
done := make(chan error, 1)
go func() { done <- l.Acquire("Y") }()
select {
case err := <-done:
if err != nil {
t.Fatalf("Y 应能获得锁: %v", err)
}
case <-time.After(2 * time.Second):
t.Fatal("X 崩溃后 Y 无法获得锁 —— 出现死锁")
}
if err := l.Release("Y"); err != nil {
t.Fatalf("Y 释放失败: %v", err)
}
}
// ForceRelease 对非持有者/未持锁应为 no-op不能误放他人的锁。
func TestStageLock_ForceReleaseIsTargeted(t *testing.T) {
l := newStageLock()
if err := l.Acquire("A"); err != nil {
t.Fatalf("Acquire: %v", err)
}
defer l.Release("A")
if l.ForceRelease("B") {
t.Error("强制释放不该动 A 持有的锁")
}
if l.Owner() != "A" {
t.Errorf("A 应仍持锁,实际 %q", l.Owner())
}
}
// 高并发下锁的串行化保证:临界区不重叠。
func TestStageLock_SerializesCriticalSection(t *testing.T) {
l := newStageLock()
var (
mu sync.Mutex
inside int
maxSeen int
)
const workers = 8
const iters = 50
var wg sync.WaitGroup
for i := 0; i < workers; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
name := string(rune('A' + id))
for j := 0; j < iters; j++ {
if err := l.Acquire(name); err != nil {
t.Errorf("Acquire: %v", err)
return
}
mu.Lock()
inside++
if inside > maxSeen {
maxSeen = inside
}
mu.Unlock()
mu.Lock()
inside--
mu.Unlock()
if err := l.Release(name); err != nil {
t.Errorf("Release: %v", err)
return
}
}
}(i)
}
wg.Wait()
if maxSeen > 1 {
t.Fatalf("临界区出现并发:同时 %d 个持有者", maxSeen)
}
}