mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-15 08:51:49 +08:00
## 改了什么
- `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 干净
211 lines
6.9 KiB
Go
211 lines
6.9 KiB
Go
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 容量被接受(应报错而非回落默认值)")
|
||
}
|
||
}
|