mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-15 08:51:49 +08:00
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 干净
This commit is contained in:
@ -837,6 +837,7 @@ func (r *ConfigRegistry) seedDBValues(dataDir string) bool {
|
||||
}
|
||||
|
||||
set("core.plugin.dir", filepath.Join(dataDir, "plugins"))
|
||||
set("core.plugin.shm_arena_mb", "4")
|
||||
set("core.memory.graph", filepath.Join(dataDir, "memory", "graph.db"))
|
||||
set("core.memory.text", filepath.Join(dataDir, "memory", "text"))
|
||||
set("core.memory.documents", filepath.Join(dataDir, "memory", "documents"))
|
||||
@ -974,6 +975,11 @@ func (r *ConfigRegistry) seedCoreDefs(dataDir string, fresh bool) {
|
||||
|
||||
plgDir := filepath.Join(dataDir, "plugins")
|
||||
reg(ConfigDef{Key: "core.plugin.dir", Default: plgDir, Type: "string", DisplayName: "插件目录", Description: "外部插件安装目录", Category: "paths"})
|
||||
reg(ConfigDef{Key: "core.plugin.shm_arena_mb", Default: "4", Type: "int", DisplayName: "插件共享内存(MB)",
|
||||
Description: "子进程插件共用的共享段 arena 容量(1–256MB)。" +
|
||||
"段大小在启动时一次确定,**修改后必须重启内核才生效**;" +
|
||||
"改大不会立刻占物理内存(memfd 惰性分配),但会扩大虚拟地址空间占用。",
|
||||
Category: "plugin"})
|
||||
reg(ConfigDef{Key: "core.memory.graph", Default: filepath.Join(dataDir, "memory", "graph.db"), Type: "string", DisplayName: "图数据库路径", Description: "长期记忆(图数据库)存储路径", Category: "paths"})
|
||||
reg(ConfigDef{Key: "core.memory.text", Default: filepath.Join(dataDir, "memory", "text"), Type: "string", DisplayName: "文本记忆路径", Description: "短期文本记忆存储目录", Category: "paths"})
|
||||
reg(ConfigDef{Key: "core.memory.documents", Default: filepath.Join(dataDir, "memory", "documents"), Type: "string", DisplayName: "文档记忆路径", Description: "文档记忆存储目录", Category: "paths"})
|
||||
|
||||
@ -118,7 +118,7 @@ func (r *Registry) ensureProcHost() (*proc.Host, error) {
|
||||
if r.procHost != nil {
|
||||
return r.procHost, nil
|
||||
}
|
||||
host, err := proc.NewHost()
|
||||
host, err := proc.NewHostWithArenaSize(r.arenaBytesFromConfig())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -278,3 +278,38 @@ func (r *Registry) ResetProcCrashCount(name string) {
|
||||
defer r.crashMu.Unlock()
|
||||
delete(r.procCrashes, name)
|
||||
}
|
||||
|
||||
// arenaBytesFromConfig 读 arena 容量配置(core.plugin.shm_arena_mb)。
|
||||
//
|
||||
// ## 为什么容量只能在启动时决定
|
||||
//
|
||||
// arena 是统一区域里一段连续 mmap,而 SharedRef 存**绝对偏移**。
|
||||
// 变大需要 remap,而每个 plugin.bin 子进程可能正在读同一页 ——
|
||||
// 跨进程 munmap 会让它 SIGSEGV。该路径历史上三次被明确搁置。
|
||||
// ⇒ 这里只决定"这次启动开多大",改配置必须重启内核才生效。
|
||||
//
|
||||
// ## 越界时记日志
|
||||
//
|
||||
// clamp 到 1..256 MB。超上限时**说出来**(否则"我配了 512"会静默变成
|
||||
// "仍是 256",与配置系统其它项的行为不一致)。
|
||||
func (r *Registry) arenaBytesFromConfig() uint32 {
|
||||
const defMB = 4
|
||||
const minMB = 1
|
||||
const maxMB = 256
|
||||
|
||||
mb := defMB
|
||||
if r.cfgReg != nil {
|
||||
if v := r.cfgReg.GetInt("core.plugin.shm_arena_mb", defMB); v != 0 {
|
||||
mb = v
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case mb < minMB:
|
||||
log.Printf("[plugin] core.plugin.shm_arena_mb=%d 过小,按 %dMB 使用", mb, minMB)
|
||||
mb = minMB
|
||||
case mb > maxMB:
|
||||
log.Printf("[plugin] core.plugin.shm_arena_mb=%d 超过上限,按 %dMB 使用", mb, maxMB)
|
||||
mb = maxMB
|
||||
}
|
||||
return uint32(mb) * 1024 * 1024
|
||||
}
|
||||
|
||||
@ -98,8 +98,9 @@ const (
|
||||
// 但它让单个工具调用可以承载 MB 级 payload,不必再退回内联 RPC。
|
||||
const arenaDefaultCapacity = 4 * 1024 * 1024
|
||||
|
||||
// arenaPublishSize 是统一区域里预留给 arena 的字节数。
|
||||
var arenaPublishSize = uint32(arenaDefaultCapacity)
|
||||
// 容量由 NewHostWithArenaSize 决定(见 host.go),这里不再维护第二份。
|
||||
// 之前有个 arenaPublishSize 变量,但自 NewHost 改为接受参数后已无人读取 ——
|
||||
// 留着一个"看着能改、改了没效果"的开关比没有更危险。
|
||||
|
||||
// arenaRegion 是块分配器视图。
|
||||
//
|
||||
|
||||
@ -69,11 +69,33 @@ type Host struct {
|
||||
// 三者共同点:全部插件看到同一份物理页,段内一律用相对偏移而非指针
|
||||
// (实验 2 已验证各进程 mmap 到不同虚拟地址时偏移解引用仍正确)。
|
||||
func NewHost() (*Host, error) {
|
||||
return NewHostWithArenaSize(arenaDefaultCapacity)
|
||||
}
|
||||
|
||||
// NewHostWithArenaSize 与 NewHost 相同,但按调用方给定的 arena 容量创建段。
|
||||
//
|
||||
// ## 为什么容量只能在**构造时**决定
|
||||
//
|
||||
// arena 是统一区域里的一段连续 mmap,而 SharedRef 存的是**绝对偏移**。
|
||||
// 要变大就得 remap,而对端进程(每个 plugin.bin)可能正在读同一页 ——
|
||||
// munmap + 重新映射会让它 SIGSEGV。跨进程 remap 因此被明确否决
|
||||
// (历史上三次实现计划均搁置)。⇒ 容量只影响"启动时开多大",
|
||||
// 改了配置必须重启内核。
|
||||
//
|
||||
// ## 越界如何处理
|
||||
//
|
||||
// 太小(低于 arenaMinSize)无法建立分配器;这里**返回明确错误**而不是
|
||||
// 静默回落到默认值 —— 配置写错要能看见,否则"我明明配了 32MB"会变成
|
||||
// "悄悄仍是 4MB"。调用方(Registry)负责读配置 + clamp。
|
||||
func NewHostWithArenaSize(arenaBytes uint32) (*Host, error) {
|
||||
if arenaBytes < arenaMinSize() {
|
||||
return nil, fmt.Errorf("共享段 arena 容量过小:%d 字节(至少 %d)", arenaBytes, arenaMinSize())
|
||||
}
|
||||
// 统一区域大小:SuperBlock + StageContext + EvtRing + Exchange Arena
|
||||
//
|
||||
// +8 是 arena 基址 8 字节对齐的 padding 余量:arenaOff 向上取整可能
|
||||
// 吃掉最多 4 字节,预留 8 字节保证 arenaCap 不会小于 arenaPublishSize。
|
||||
unifiedSize := superBlockSize + shmDefaultSize + evtTotalSize + int(arenaPublishSize) + 8
|
||||
// 吃掉最多 4 字节,预留 8 字节保证 arenaCap 不会小于请求容量。
|
||||
unifiedSize := superBlockSize + shmDefaultSize + evtTotalSize + int(arenaBytes) + 8
|
||||
memfd, data, err := allocShm(unifiedSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@ -185,3 +185,26 @@ func TestArenaFragmentationPreventsLargeRequest(t *testing.T) {
|
||||
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 容量被接受(应报错而非回落默认值)")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user