Files
HomeAgent/internal/plugin/proc/test/arena_contract_test.go
JianFeeeee b879558947 feat(proc): 共享段 arena 容量可配置(启动期生效,需重启内核)
## 改了什么

- `proc.NewHostWithArenaSize(bytes)`:按给定容量建段;保留 `NewHost()`
  以兼容默认 4MB。
- 容量低于分配器最小尺寸时**返回明确错误**,不静默回落默认值
  (否则"我配了 32MB"会悄悄变成"仍是 4MB")。
- 删除 `arenaPublishSize` 变量:自从容量改为入参后已无人读取,
  留一个"看着能改、改了没效果"的开关比没有更危险。
- `core.plugin.shm_arena_mb`(默认 4,clamp 1..256):播种 + 定义 +
  `Registry.arenaBytesFromConfig()` 读取。越界时记日志说明被 clamp。

## 为什么容量只能在启动时决定

arena 是统一区域里一段连续 mmap,而 `SharedRef` 存**绝对偏移**。
变大需要 remap,而每个 plugin.bin 子进程可能正在读同一页 ——
跨进程 munmap 会让它 SIGSEGV。该路径历史上三次被明确搁置。
⇒ 本改动只决定"这次启动开多大",配置项描述里已写明需重启。

## 判据(`internal/plugin/proc/test/arena_contract_test.go`)

    TestArenaHostHonoursRequestedCapacity
      给定 8MB → 能写入并读回 4MB(证明容量真的生效)
    TestArenaHostRejectsTooSmallCapacity
      64 字节 → 明确报错

变异验证:
    忽略传入容量、总用默认 4MB   → 4MB 写入报「超出 arena 容量 4194228」
    过小容量改为静默回落默认     → 判据红

    go build ./... / go vet ./...                  PASS
    python3 .github/scripts/check-ci-gates.py      PASS
    go test ./... -count=1 -race                   51 包 / 100 包无测试 / 0 FAIL
    gofmt -l                                       干净
2026-11-14 11:32:03 +08:00

211 lines
6.9 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 test
// internal/plugin/proc 的共享内存 arena —— 面向接口的模块职责判据。
//
// 唯一入口是公开的 proc.NewHost() 与 Host 的导出方法;块布局、块头字段、
// 空闲链结构都不在断言范围内。断言只看:
// 写入的字节能否原样读出、操作成功还是失败、空间能否被再次使用。
import (
"bytes"
"testing"
"github.com/JianFeeeee/HologramAgent/internal/plugin/proc"
)
func newArenaHost(t *testing.T) *proc.Host {
t.Helper()
h, err := proc.NewHost()
if err != nil {
t.Skipf("共享内存不可用:%v", err)
}
t.Cleanup(func() { _ = h.Close() })
return h
}
func putBytes(t *testing.T, h *proc.Host, owner uint32, payload []byte) proc.SharedRef {
t.Helper()
ref, err := h.Arena().Put(owner, payload, h.Generation())
if err != nil {
t.Fatalf("Put(%d bytes): %v", len(payload), err)
}
return ref
}
func assertReadable(t *testing.T, h *proc.Host, ref proc.SharedRef, want []byte) {
t.Helper()
got, err := h.Arena().Read(ref, h.Generation())
if err != nil {
t.Fatalf("Read: %v", err)
}
if !bytes.Equal(got, want) {
t.Fatalf("Read 返回 %d 字节,期望 %d 字节且内容一致", len(got), len(want))
}
}
// TestArenaWriteReadFreeReuse 是 arena 的核心职责闭环:
// 写入 → 读出 → 归还 → 同样大小的写入再次成功。
func TestArenaWriteReadFreeReuse(t *testing.T) {
h := newArenaHost(t)
payload := bytes.Repeat([]byte("x"), 4096)
ref := putBytes(t, h, proc.OwnerHost, payload)
assertReadable(t, h, ref, payload)
if err := h.Arena().Free(proc.OwnerHost, ref); err != nil {
t.Fatalf("Free: %v", err)
}
again := putBytes(t, h, proc.OwnerHost, payload)
assertReadable(t, h, again, payload)
}
// TestArenaRequestBeyondCapacityFailsCleanly:超过容量的请求必须失败,
// 且失败之后分配器仍能正常工作。
func TestArenaRequestBeyondCapacityFailsCleanly(t *testing.T) {
h := newArenaHost(t)
limit := h.Arena().MaxPayload()
if _, err := h.Arena().Alloc(proc.OwnerHost, limit+1, h.Generation()); err == nil {
t.Fatalf("超过上限 %d 字节的请求被接受", limit)
}
payload := []byte("still-works")
ref := putBytes(t, h, proc.OwnerHost, payload)
assertReadable(t, h, ref, payload)
}
// TestArenaCannotBeFreedByAnotherOwner:别的 owner 不能归还本块,
// 且块内容不受影响。
func TestArenaCannotBeFreedByAnotherOwner(t *testing.T) {
h := newArenaHost(t)
payload := []byte("owned-by-host")
ref := putBytes(t, h, proc.OwnerHost, payload)
if err := h.Arena().Free(10086, ref); err == nil {
t.Fatal("非 owner 的归还被接受")
}
assertReadable(t, h, ref, payload)
}
// TestArenaRejectsStaleReference:旧 generation 的引用必须报错,
// 不允许返回可能错位的数据。
func TestArenaRejectsStaleReference(t *testing.T) {
h := newArenaHost(t)
ref := putBytes(t, h, proc.OwnerHost, []byte("data"))
if _, err := h.Arena().Read(ref, h.Generation()+1); err == nil {
t.Fatal("过期 generation 的读取被接受")
}
}
// TestArenaReclaimReleasesPluginSpace:插件退出后的强制回收,
// 归还的空间必须能被后续分配真正用上。
func TestArenaReclaimReleasesPluginSpace(t *testing.T) {
h := newArenaHost(t)
const pluginOwner = uint32(4242)
payload := bytes.Repeat([]byte("y"), 8192)
putBytes(t, h, pluginOwner, payload)
if n := h.Arena().ReclaimOwner(pluginOwner); n != 1 {
t.Fatalf("ReclaimOwner = %d,期望 1", n)
}
again := putBytes(t, h, proc.OwnerHost, payload)
assertReadable(t, h, again, payload)
}
// TestArenaReclaimDoesNotTouchHostBlocks:回收插件块时不得影响内核自己的块。
func TestArenaReclaimDoesNotTouchHostBlocks(t *testing.T) {
h := newArenaHost(t)
hostPayload := []byte("host-owned")
hostRef := putBytes(t, h, proc.OwnerHost, hostPayload)
const pluginOwner = uint32(9999)
putBytes(t, h, pluginOwner, []byte("plugin-owned"))
h.Arena().ReclaimOwner(pluginOwner)
assertReadable(t, h, hostRef, hostPayload)
if err := h.Arena().Free(proc.OwnerHost, hostRef); err != nil {
t.Fatalf("内核块被回收后无法归还:%v", err)
}
}
// TestArenaInvalidReferenceIsRejected:伪造的偏移不能被当成合法块,
// 归还时必须报错而不是破坏分配器。
func TestArenaInvalidReferenceIsRejected(t *testing.T) {
h := newArenaHost(t)
forged := proc.SharedRef{Offset: 1, Length: 8, Generation: uint32(h.Generation())}
if err := h.Arena().Free(proc.OwnerHost, forged); err == nil {
t.Fatal("伪造偏移的归还未被拒绝")
}
payload := []byte("after-forgery")
ref := putBytes(t, h, proc.OwnerHost, payload)
assertReadable(t, h, ref, payload)
}
// TestArenaFreeTwiceIsRejected:重复归还同一个块必须报错。
func TestArenaFreeTwiceIsRejected(t *testing.T) {
h := newArenaHost(t)
ref := putBytes(t, h, proc.OwnerHost, []byte("once"))
if err := h.Arena().Free(proc.OwnerHost, ref); err != nil {
t.Fatalf("首次归还失败:%v", err)
}
if err := h.Arena().Free(proc.OwnerHost, ref); err == nil {
t.Fatal("重复归还被接受")
}
}
// TestArenaFragmentationPreventsLargeRequest 钉住"总量够但无连续块"的真实失败。
//
// 这是现状行为,不是期望行为:1MB 大块与小块交替铺满后释放大块,
// 剩余空间总量超过请求的一半,但被小块隔开 ⇒ 首次适配拿不到。
// 引入整理回收后,本判据应改为「同一请求成功且内容可读」。
func TestArenaFragmentationPreventsLargeRequest(t *testing.T) {
h := newArenaHost(t)
arena := h.Arena()
limit := arena.MaxPayload()
big := make([]byte, limit/4)
sep := []byte("separator")
putBytes(t, h, proc.OwnerHost, sep)
var bigRefs []proc.SharedRef
for i := 0; i < 3; i++ {
bigRefs = append(bigRefs, putBytes(t, h, proc.OwnerHost, big))
putBytes(t, h, proc.OwnerHost, sep)
}
for _, ref := range bigRefs {
if err := arena.Free(proc.OwnerHost, ref); err != nil {
t.Fatalf("Free: %v", err)
}
}
if _, err := arena.Alloc(proc.OwnerHost, int(limit/2-1024), h.Generation()); err == nil {
t.Log("当前实现已能整理碎片;本判据需改为验证整理后的可用性")
}
}
// TestArenaHostHonoursRequestedCapacity:按调用方给定的容量创建共享段,
// 容量必须真的生效(MaxPayload 随容量变化),而不是静默用默认值。
func TestArenaHostHonoursRequestedCapacity(t *testing.T) {
const mb = 8
h, err := proc.NewHostWithArenaSize(mb * 1024 * 1024)
if err != nil {
t.Skipf("共享内存不可用:%v", err)
}
defer func() { _ = h.Close() }()
payload := bytes.Repeat([]byte("z"), mb*1024*1024/2)
ref := putBytes(t, h, proc.OwnerHost, payload)
assertReadable(t, h, ref, payload)
}
// TestArenaHostRejectsTooSmallCapacity:低于分配器最小尺寸时报明确错误,
// 不静默回落到默认容量。
func TestArenaHostRejectsTooSmallCapacity(t *testing.T) {
if _, err := proc.NewHostWithArenaSize(64); err == nil {
t.Fatal("过小的 arena 容量被接受(应报错而非回落默认值)")
}
}