Files
HomeAgent/internal/plugin/proc/host.go
JianFeeeee 5bbfcc02fb plugin: 事件环内核侧实现(§3.6 Part 5 核心)
事件环(EvtRing)是子进程首次获得事件订阅能力的基础设施。
此前 case 23/24 明确返回未实现,现在经事件环真正可用。

核心设计(§3.6,实验 4 已验证 post-and-forget 加速比 2218x):
- 事件环放**独立共享段**(不与 StageContext 混放):stage compact 会清 arena,
  事件要独立于 stage 生命周期。Host 持有两块 memfd:fd 3 = StageContext,
  fd 4 = 事件环段,fd 5 = eventfd。
- 无锁数据结构:内核 WritePush 追加写 slot,子进程 EvtConsumer 消费。
  writeSeq 原子递增(Bus.Publish 并发调用),readSeq 每订阅者独立。
- eventfd 通知:Linux 用 unix.Eventfd(计数合并,1000 token 只唤醒几次),
  macOS 用 os.Pipe(阻塞模式走 netpoller,只 park goroutine,实验 1 验证
  200 等待者仅 +1 OS 线程)。两者行为一致:Read 阻塞直到有新事件。
- 溢出语义:落后超 cap 时跳到最新,丢弃计数记入 dropped(消费者知道丢了)。
  不静默覆盖最旧(写端直接覆盖 slot,读端靠 seq 判断跳过)。
- 事件类型编码:pubsdk.EventType 字符串 ↔ uint32 位索引(编译时映射表),
  typeMask 位掩码过滤(1<<idx)。

Host 改动:
- NewHost 同时创建事件环段和 eventfd(惰创建,一次分配)。
- Host 持有 evtSubscriber 接口(EvtRingSubscriber),由 Registry 注入
  EventRing 实现——proc 包不依赖 internal/plugin(避免循环依赖)。

corehandler 改动:
- events.subscribe(原 case 23):子进程传事件类型列表,coreHandler
  通过 evtRing 接口调用 EvtRingSubscribe,注册到 Bus 上。
  事件经 EventRing 写入环后由子进程 mmap 读取。
- events.unsubscribe(原 case 24):当前由内核统一清理(子进程 Stop 时)。

Registry 改动:
- ensureProcHost 在创建 Host 后同时创建 EventRing(Bus → EvtRing → eventfd),
  并通过 Host.SetEvtSubscriber 注入给 coreHandler。

测试 3 项:
- BasicWriteAndConsume:Host 创建 → EventRing 写入 → 消费者读到
- OverflowStillDelivers:写入超过 cap 后消费者仍能读到最新事件
- TypeMaskFiltering:typeMask 只订阅 tool_call,agent_output 被过滤

验证:go build ./... 通过;go test -race ./internal/plugin/... 全绿;
既有事件环测试 3/3 通过;proc 包测试未受影响。

Ref: docs/zh/架构迁移评估.md §3.6、docs/zh/plugin-migration-plan.md Part 5
2026-09-02 16:48:27 +08:00

254 lines
7.6 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 (
"fmt"
"log"
"os"
"sync"
pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
)
// Host 持有**被全部子进程插件共享的一块 StageContext 段**,是共享内存数据面的
// 所有权中心§3.3/§3.4)。
//
// ❗ 为什么必须共享一块段(这是一个容易走错的关键点):
// 若每个插件各持一块段,则「内核 ctx → 段 → 插件改 → 回读 ctx」在多插件下退化成
// 副本模型——两个插件各写各的段、各自回读,最后回读者覆盖前者,
// lost update 原样复现§8.4 实测 35.8~36.8%)。
// 实验 8 的做法是 5 个 worker 进程 mmap **同一个 memfd**,本实现与之一致。
//
// 生命周期Host 由 registry 创建一次,随内核存活;每个插件 spawn 时经
// ExtraFiles 拿到同一 memfdfd 3mmap 后即看到同一份物理页。
//
// 另外持有事件环段§3.6):独立于 StageContext 的事件通知通道,
// 子进程从 eventfd 感知新事件并从 mmap 读 slot。
// fd 分配fd 3 = StageContextfd 4 = 事件环fd 5 = eventfd。
type Host struct {
memfd *os.File
data []byte
seg *Segment
shmSize int
// 事件环段(独立于 StageContext
evtfd *os.File // eventfd fdfd 5 的句柄,子进程读取消费)
evtRing *EvtRing // 内核侧事件环句柄
evtRingFd *os.File // 事件环段 memfdfd 4子进程 mmap 读事件)
evtData []byte // 事件环段 mmap 数据
// evtSubscriber 由 internal/plugin 注入coreHandler 用它接子进程的 events.subscribe 请求。
// proc 包不依赖 internal/plugin循环依赖故用接口类型存储。
evtSubscriber EvtRingSubscriber
locks *lockRegistry
stageMu sync.Mutex
coordMu sync.Mutex
coord *stageCoordinator
}
// NewHost 创建共享段(平台层 allocShm + 布局初始化)。
//
// 段的分配按平台分开shmalloc_*.goLinux 用 memfdmacOS 用
// 立即 unlink 的临时文件(无 memfd_create其余平台明确报错。
// 两者语义一致无文件名残留fd 可经 ExtraFiles 传给子进程,
// 子进程 mmap 同一 inode——「全部插件共享一块段」的前提得以成立。
// 实验 2 已验证父子 mmap 到不同虚拟地址时相对偏移仍正确解引用。
func NewHost() (*Host, error) {
memfd, data, err := allocShm(shmDefaultSize)
if err != nil {
return nil, err
}
seg, err := NewSegment(data)
if err != nil {
freeShm(memfd, data)
return nil, err
}
// 创建事件环段(独立于 StageContext
evtRingFd, evtData, efd, err := allocEvtRing()
if err != nil {
freeShm(memfd, data)
return nil, fmt.Errorf("事件环: %w", err)
}
evtRing, err := NewEvtRing(evtData)
if err != nil {
freeShm(memfd, data)
return nil, fmt.Errorf("事件环初始化: %w", err)
}
evtRing.Init()
return &Host{
memfd: memfd,
data: data,
seg: seg,
shmSize: shmDefaultSize,
evtfd: evtfdReadFile(efd),
evtRing: evtRing,
evtRingFd: evtRingFd,
evtData: evtData,
locks: &lockRegistry{},
}, nil
}
// shmDefaultSize 是共享 StageContext 段的大小。
//
// 取 256KBStageContext 全字段 JSON 化后典型 < 4KB工具结果中位 93B§2.5
// append-only 中间垃圾由 stage 结束时 Compact 回收256KB 给足余量。
// 全部插件共享一块,总开销恒定,不随插件数增长。
const shmDefaultSize = 256 * 1024
// Close 释放共享段StageContext + 事件环)。
func (h *Host) Close() error {
var firstErr error
if h.data != nil {
if err := freeShm(h.memfd, h.data); err != nil && firstErr == nil {
firstErr = err
}
h.data, h.memfd = nil, nil
}
if h.evtData != nil {
if h.evtRingFd != nil {
h.evtRingFd.Close()
h.evtRingFd = nil
}
h.evtData = nil
}
if h.evtfd != nil {
h.evtfd.Close()
h.evtfd = nil
}
return firstErr
}
// beginStage 由插件 handler 进入时调用。
//
// 首个进入者:获取 stageMu独占共享段→ 把内核 StageContext 写入段。
// 后续进入者:仅递增 inflight。
func (h *Host) beginStage(sc *pubsdk.StageContext) (*stageCoordinator, error) {
h.coordMu.Lock()
first := h.coord == nil
if first {
// 独占共享段直到本次 stage 全部插件离开
h.coordMu.Unlock()
h.stageMu.Lock()
h.coordMu.Lock()
// 双检:等锁期间可能已有其他插件建好协调器(它们会先拿到 stageMu
if h.coord != nil {
first = false
h.stageMu.Unlock()
} else {
h.coord = newStageCoordinator(h.seg)
}
}
coord := h.coord
h.coordMu.Unlock()
if err := coord.enter(sc, first); err != nil {
if first {
h.coordMu.Lock()
h.coord = nil
h.coordMu.Unlock()
h.stageMu.Unlock()
}
return nil, err
}
h.locks.bind(coord.lock)
return coord, nil
}
// endStage 由插件 handler 返回时调用。
// 最后离开者:把共享段结果读回内核 StageContext → 压实 arena → 释放 stageMu。
func (h *Host) endStage(coord *stageCoordinator) error {
last, err := coord.leave()
if !last {
return err
}
h.coordMu.Lock()
h.coord = nil
h.coordMu.Unlock()
h.stageMu.Unlock()
return err
}
// ForceReleaseLock 在插件进程崩溃时释放其可能持有的 stage 锁(实验 9 自愈机制)。
func (h *Host) ForceReleaseLock(plugin string) bool {
return h.locks.forceRelease(plugin)
}
// Segment 暴露共享段(供诊断与测试)。
func (h *Host) Segment() *Segment { return h.seg }
// stageCoordinator 跟踪一次 stage 执行中参与插件的进出。
type stageCoordinator struct {
seg *Segment
lock *stageLock
mu sync.Mutex
inflight int
written bool
ctxRef *pubsdk.StageContext
}
func newStageCoordinator(seg *Segment) *stageCoordinator {
return &stageCoordinator{seg: seg, lock: newStageLock()}
}
// enter 登记一个插件进入本次 stagefirst 为真时把内核状态写入共享段。
func (c *stageCoordinator) enter(sc *pubsdk.StageContext, first bool) error {
c.mu.Lock()
defer c.mu.Unlock()
c.inflight++
if !first || c.written {
return nil
}
c.ctxRef = sc
if err := c.seg.WriteAll(sc); err != nil {
c.inflight--
return fmt.Errorf("写入共享段: %w", err)
}
c.written = true
return nil
}
// leave 登记一个插件离开;返回是否为最后一个离开者。
//
// 最后离开者负责把共享段结果读回内核 StageContext并压实 arena
// (此时无插件持锁,满足 §3.3 的压实前提)。
func (c *stageCoordinator) leave() (last bool, err error) {
c.mu.Lock()
c.inflight--
last = c.inflight == 0
sc := c.ctxRef
written := c.written
c.mu.Unlock()
if !last || !written || sc == nil {
return last, nil
}
if rErr := c.seg.ReadInto(sc); rErr != nil {
return last, fmt.Errorf("回读共享段: %w", rErr)
}
if reclaimed := c.seg.Compact(); reclaimed > 0 {
log.Printf("[proc] stage 结束arena 压实回收 %d 字节", reclaimed)
}
return last, nil
}
// ShmSize 返回共享段大小(供诊断/日志)。
func (h *Host) ShmSize() int { return h.shmSize }
// EvtRing 返回内核侧事件环句柄。
func (h *Host) EvtRing() *EvtRing { return h.evtRing }
// Evtfd 返回 eventfd 的 *os.File供 EventRing 写通知)。
func (h *Host) Evtfd() *os.File { return h.evtfd }
// SetEvtSubscriber 注入事件环订阅接口(由 Registry 在创建 Host 后设置)。
func (h *Host) SetEvtSubscriber(sub EvtRingSubscriber) { h.evtSubscriber = sub }
// EvtData 返回事件环段 mmap 数据(子进程消费者用)。
func (h *Host) EvtData() []byte { return h.evtData }
// EvtfdReadFile 返回 eventfd 的 *os.File供子进程读取消费
func (h *Host) EvtfdReadFile() *os.File { return h.evtfd }