Files
HomeAgent/internal/plugin/evtring_test.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

161 lines
4.0 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.

//go:build linux || darwin
package plugin
import (
"testing"
"time"
"gitcode.com/JianFeeeee/HomeAgent/internal/events"
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin/proc"
pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
)
// 事件环基础测试Host 创建事件环 → EventRing 写入 → 消费者读到。
func TestEventRing_BasicWriteAndConsume(t *testing.T) {
host, err := proc.NewHost()
if err != nil {
t.Fatalf("NewHost: %v", err)
}
defer host.Close()
bus := events.NewBus()
er := NewEventRing(host.EvtRing(), int(host.Evtfd().Fd()), bus)
// 消费者:从事件环段读取事件
received := make(chan *pubsdk.Event, 10)
consumer := proc.NewEvtConsumer(
host.EvtData(),
host.EvtfdReadFile(),
0, // typeMask = 0接收全部事件
func(evt *pubsdk.Event) error {
received <- evt
return nil
},
)
go consumer.Run()
defer consumer.Stop()
// 订阅 agent_output 事件
unsub := er.Subscribe(pubsdk.EventAgentOutput)
defer unsub()
// 发布事件
bus.Publish(&events.Event{
Type: events.EventAgentOutput,
Payload: map[string]interface{}{"text": "hello"},
})
// 等待消费者读到
select {
case evt := <-received:
if evt.Type != pubsdk.EventType(events.EventAgentOutput) {
t.Errorf("事件类型 = %v期望 %v", evt.Type, events.EventAgentOutput)
}
case <-time.After(2 * time.Second):
t.Error("消费者在 2s 内未收到事件")
}
}
// 事件环溢出测试:写入超过 cap 时消费者仍能读到最新事件。
func TestEventRing_OverflowStillDelivers(t *testing.T) {
host, err := proc.NewHost()
if err != nil {
t.Fatalf("NewHost: %v", err)
}
defer host.Close()
bus := events.NewBus()
er := NewEventRing(host.EvtRing(), int(host.Evtfd().Fd()), bus)
// 不启动消费者,直接写入超过 cap 的事件(需先订阅,否则 Bus 不会触发事件环写入)
unsub := er.Subscribe(pubsdk.EventSystem)
defer unsub()
for i := uint32(0); i < 8192+100; i++ {
bus.Publish(&events.Event{
Type: events.EventSystem,
Payload: map[string]interface{}{"seq": i},
})
}
// 启动消费者,应能读到最新事件
received := make(chan *pubsdk.Event, 10)
consumer := proc.NewEvtConsumer(
host.EvtData(),
host.EvtfdReadFile(),
0,
func(evt *pubsdk.Event) error {
received <- evt
return nil
},
)
go consumer.Run()
defer consumer.Stop()
select {
case evt := <-received:
if evt == nil {
t.Error("收到 nil 事件")
}
case <-time.After(2 * time.Second):
t.Error("溢出后消费者在 2s 内未收到事件")
}
}
// typeMask 过滤测试:订阅者只收到匹配类型的事件。
func TestEventRing_TypeMaskFiltering(t *testing.T) {
host, err := proc.NewHost()
if err != nil {
t.Fatalf("NewHost: %v", err)
}
defer host.Close()
bus := events.NewBus()
er := NewEventRing(host.EvtRing(), int(host.Evtfd().Fd()), bus)
received := make(chan *pubsdk.Event, 10)
// typeMask 只订阅 tool_callbit 3 = 8
consumer := proc.NewEvtConsumer(
host.EvtData(),
host.EvtfdReadFile(),
1<<3, // tool_call
func(evt *pubsdk.Event) error {
received <- evt
return nil
},
)
go consumer.Run()
defer consumer.Stop()
unsub := er.Subscribe(pubsdk.EventToolCall)
defer unsub()
// 发一个 tool_call 和一个 agent_output
bus.Publish(&events.Event{
Type: events.EventToolCall,
Payload: map[string]interface{}{"tool": "test"},
})
bus.Publish(&events.Event{
Type: events.EventAgentOutput,
Payload: map[string]interface{}{"text": "should be filtered"},
})
// 只应收到 tool_call
select {
case evt := <-received:
if evt.Type != pubsdk.EventType(events.EventToolCall) {
t.Errorf("收到错误类型 %v期望 tool_call", evt.Type)
}
case <-time.After(2 * time.Second):
t.Error("消费者在 2s 内未收到 tool_call 事件")
}
// agent_output 不应到达
select {
case evt := <-received:
t.Errorf("不应收到 agent_output实际收到 %v", evt)
case <-time.After(200 * time.Millisecond):
// 正确agent_output 被过滤
}
}