Files
HomeAgent/internal/plugin/proc/arena_test.go
JianFeeeee ca584bda3f refactor(shm): 内核独占共享槽池,插件经 RPC 申请/归还(§13.2/§13.3)
推翻前两版跨进程分配器设计,根因是"共享内存里放了只被单一进程
更新的可变游标":

- v1 在 SuperBlock 放 arenaUsed 游标,内核 CAS bump。但插件模板里
  arenaUsed 是**进程本地变量**,两进程各自 bump 必写同一段内存;
  arenaReset 还会重置共享游标覆盖对方数据。
- v2 把位图 CAS 下沉到插件模板,正确但把分配器实现泄漏进插件运行时,
  且插件必须与内核保持位图布局同步。

新设计(用户明确的架构约束):内核全权管理共享内存,插件通过
syscall 风格 RPC 申请/归还,内核返回偏移与大小。分配器只存在于内核
进程内,一把 sync.Mutex 即可。共享内存是内部实现,不对插件开发者
暴露——SDK 公开 API 仍是普通字符串/Map。

主要改动:
- arena.go: 定长槽 + 位图,Alloc/Put/Read/Free/ReclaimOwner,内核独占;
  槽头记录 owner,Free 校验归属;payload 超槽容量退回内联 RPC
- protocol.go: 新增 arena.alloc / arena.free(CapCore)
- capability.go: 登记两个新 method(checkAllMethodsClassified 要求)
- corehandler.go: 实现 arena.alloc/free;Cleaner 改为内核预分配
  请求槽 + 响应槽(插件完全不分配)
- plugin.go: Start 领 ownerID;handleExit 调 ReclaimOwner 回收残留槽
- unified.go: 移除坏的 bump 分配器;arena 基址 8 字节对齐
- 删除 toollane.go: ring 状态机是死代码(从未接线),且把 RPC 已有的
  请求 ID 关联/错误传递/ctx 取消重新实现了一遍。控制面保留 RPC,
  只把 payload 搬进共享槽。
- 测试: TestArena_*(归属/回收/并发唯一/耗尽/超限/非法引用/布局)+
  TestPlugin_ArenaAllocFreeAcrossProcess(真进程申请→写入→随业务
  RPC 回传→归还→内核读回一致且池归零)
2026-09-10 17:56:50 +08:00

264 lines
6.7 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 (
"bytes"
"sync"
"testing"
)
// newTestArena 构造一个独立槽池(不经 Host保持单测快速。
func newTestArena(t *testing.T, slots, slotSize uint32) *arenaRegion {
t.Helper()
const base = 64
need := arenaRequiredSize(slots, slotSize)
region := make([]byte, base+need)
a, err := initArena(region, base, need, slots, slotSize)
if err != nil {
t.Fatalf("initArena: %v", err)
}
return a
}
func TestArena_AllocFreeRoundTrip(t *testing.T) {
a := newTestArena(t, 4, 64)
if got, total := a.Stats(); got != 0 || total != 4 {
t.Fatalf("初始 Stats: got (%d,%d), want (0,4)", got, total)
}
ref, err := a.Put(OwnerHost, []byte("hello"), 0)
if err != nil {
t.Fatalf("Put: %v", err)
}
if used, _ := a.Stats(); used != 1 {
t.Fatalf("Put 后 used=%d, want 1", used)
}
got, err := a.Read(ref, 0)
if err != nil {
t.Fatalf("Read: %v", err)
}
if string(got) != "hello" {
t.Fatalf("Read=%q, want hello", got)
}
if err := a.Free(OwnerHost, ref.Flags); err != nil {
t.Fatalf("Free: %v", err)
}
if used, _ := a.Stats(); used != 0 {
t.Fatalf("Free 后 used=%d, want 0", used)
}
// 归还后再读必须失败(槽已回收)
if _, err := a.Read(ref, 0); err == nil {
t.Fatal("已释放槽的引用应读取失败")
}
}
func TestArena_ConcurrentAllocUnique(t *testing.T) {
const slots = 32
a := newTestArena(t, slots, 64)
refs := make(chan SharedRef, slots)
var wg sync.WaitGroup
for i := 0; i < slots; i++ {
wg.Add(1)
go func() {
defer wg.Done()
ref, err := a.Alloc(OwnerHost, 8)
if err != nil {
t.Errorf("Alloc: %v", err)
return
}
refs <- ref
}()
}
wg.Wait()
close(refs)
seen := make(map[uint32]bool, slots)
for ref := range refs {
if seen[ref.Flags] {
t.Fatalf("并发分配拿到重复槽 %d", ref.Flags)
}
seen[ref.Flags] = true
}
if len(seen) != slots {
t.Fatalf("唯一槽数=%d, want %d", len(seen), slots)
}
if used, _ := a.Stats(); used != slots {
t.Fatalf("used=%d, want %d", used, slots)
}
}
func TestArena_ExhaustionReturnsError(t *testing.T) {
a := newTestArena(t, 2, 64)
for i := 0; i < 2; i++ {
if _, err := a.Alloc(OwnerHost, 8); err != nil {
t.Fatalf("第 %d 次 Alloc: %v", i, err)
}
}
if _, err := a.Alloc(OwnerHost, 8); err == nil {
t.Fatal("槽池耗尽时 Alloc 应返回错误")
}
}
func TestArena_RejectsOversizePayload(t *testing.T) {
a := newTestArena(t, 2, 64)
if _, err := a.Alloc(OwnerHost, a.SlotPayloadCap()+1); err == nil {
t.Fatal("超出槽容量的 payload 应被拒绝")
}
}
func TestArena_FreeEnforcesOwnership(t *testing.T) {
a := newTestArena(t, 4, 64)
const ownerA, ownerB = 7, 9
refA, err := a.Alloc(ownerA, 8)
if err != nil {
t.Fatal(err)
}
// 另一个插件不能释放 A 的槽
if err := a.Free(ownerB, refA.Flags); err == nil {
t.Fatal("跨 owner 释放应被拒绝")
}
if used, _ := a.Stats(); used != 1 {
t.Fatalf("拒绝释放后 used=%d, want 1", used)
}
if a.OwnerOf(refA.Flags) != ownerA {
t.Fatalf("槽 %d 的 owner 应为 %d", refA.Flags, ownerA)
}
// 正确的 owner 可以释放
if err := a.Free(ownerA, refA.Flags); err != nil {
t.Fatalf("同 owner 释放: %v", err)
}
}
func TestArena_ReclaimOwnerReleasesOnlyItsSlots(t *testing.T) {
a := newTestArena(t, 8, 64)
const ownerA, ownerB = 7, 9
for i := 0; i < 3; i++ {
if _, err := a.Alloc(ownerA, 8); err != nil {
t.Fatal(err)
}
}
for i := 0; i < 2; i++ {
if _, err := a.Alloc(ownerB, 8); err != nil {
t.Fatal(err)
}
}
if n := a.ReclaimOwner(ownerA); n != 3 {
t.Fatalf("ReclaimOwner(A)=%d, want 3", n)
}
if used, _ := a.Stats(); used != 2 {
t.Fatalf("回收 A 后 used=%d, want 2B 的槽必须保留)", used)
}
// 内核自己的槽不参与回收
if n := a.ReclaimOwner(OwnerHost); n != 0 {
t.Fatalf("ReclaimOwner(host)=%d, want 0", n)
}
}
func TestArena_ReclaimHostIsNoop(t *testing.T) {
a := newTestArena(t, 4, 64)
if _, err := a.Alloc(OwnerHost, 8); err != nil {
t.Fatal(err)
}
if n := a.ReclaimOwner(OwnerHost); n != 0 {
t.Fatalf("内核槽不应被 ReclaimOwner 回收,实际回收 %d", n)
}
if used, _ := a.Stats(); used != 1 {
t.Fatalf("used=%d, want 1", used)
}
}
func TestArena_ReadRejectsInvalidRefs(t *testing.T) {
a := newTestArena(t, 4, 256)
ref, err := a.Put(OwnerHost, []byte("payload"), 0)
if err != nil {
t.Fatal(err)
}
// generation 过期
stale := ref
stale.Generation = 99
if _, err := a.Read(stale, 0); err == nil {
t.Fatal("generation 不匹配应被拒绝")
}
// offset 与槽不自洽
badOffset := ref
badOffset.Offset++
if _, err := a.Read(badOffset, 0); err == nil {
t.Fatal("offset 与槽不自洽应被拒绝")
}
// 槽号越界
badSlot := ref
badSlot.Flags = 999
if _, err := a.Read(badSlot, 0); err == nil {
t.Fatal("槽号越界应被拒绝")
}
// Length 超容量
tooLong := ref
tooLong.Length = uint32(a.SlotPayloadCap() + 1)
if _, err := a.Read(tooLong, 0); err == nil {
t.Fatal("Length 超容量应被拒绝")
}
}
func TestArena_PutReadLargePayload(t *testing.T) {
a := newTestArena(t, 4, 4096)
payload := bytes.Repeat([]byte("abcdefgh"), 400) // 3200 字节
ref, err := a.Put(OwnerHost, payload, 3)
if err != nil {
t.Fatalf("Put: %v", err)
}
got, err := a.Read(ref, 3)
if err != nil {
t.Fatalf("Read: %v", err)
}
if !bytes.Equal(got, payload) {
t.Fatalf("读回数据不一致len(got)=%d len(want)=%d", len(got), len(payload))
}
}
func TestArena_AttachRejectsCorruptLayout(t *testing.T) {
a := newTestArena(t, 4, 256)
// 魔数被改
putU32(a.region[a.base+arOffMagic:], 0xDEADBEEF)
if _, err := attachArena(a.region, a.base, a.cap); err == nil {
t.Fatal("魔数错误应被拒绝")
}
putU32(a.region[a.base+arOffMagic:], arenaMagic)
// 版本被改
putU32(a.region[a.base+arOffVersion:], 99)
if _, err := attachArena(a.region, a.base, a.cap); err == nil {
t.Fatal("版本不匹配应被拒绝")
}
putU32(a.region[a.base+arOffVersion:], arenaVersion)
// 槽数组越界
putU32(a.region[a.base+arOffSlotCount:], 1<<20)
if _, err := attachArena(a.region, a.base, a.cap); err == nil {
t.Fatal("槽数组越界应被拒绝")
}
}
func TestArenaRequiredSizeAlignment(t *testing.T) {
// 位图按 8 字节对齐、槽数组 8 字节对齐,保证 4 字节原子操作不跨页/不越界。
for _, tc := range []struct{ slots, slotSize uint32 }{
{1, 16}, {8, 64}, {32, 16384}, {33, 1024}, {64, 512}, {100, 256},
} {
need := arenaRequiredSize(tc.slots, tc.slotSize)
if need%8 != 0 {
t.Errorf("slots=%d slotSize=%d: arenaRequiredSize=%d 不是 8 的倍数", tc.slots, tc.slotSize, need)
}
region := make([]byte, 64+need)
if _, err := initArena(region, 64, need, tc.slots, tc.slotSize); err != nil {
t.Errorf("slots=%d slotSize=%d: initArena 失败: %v", tc.slots, tc.slotSize, err)
}
}
}