mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-01 06:47:29 +00:00
## 1. arena 泄漏(线上 arena 被打满的根因)
插件经 arena.alloc 申请的扩容块,块头 owner 记的是**插件的** ownerID,
而内核三处归还全写着 Free(OwnerHost, ref)。归属校验必然失败,错误又被
`_ =` 吞掉 ⇒ 每返回一次大结果就永久泄漏一块。
线上表现(2026-10-25):重任务跑几轮后 4MB arena 满,之后连 64KB 调用帧
都分不出来,所有插件工具一起报「arena: 空间不足(请求 65779 字节)」。
修法:三处改用插件自己的 ownerID,且失败必须留日志(吞掉错误等于把
下一次「空间不足」的原因提前销毁)。
## 2. 取消成为一等状态
旧实现里「调用在进行中」只表达为 pending 表里有这个键:取消时直接
delete,状态的痕迹全部消失,插件随后回的应答只能走「未知 id,丢弃」。
那是副作用的正确,不是规则的正确。
改成每次调用带显式状态机(callstate.go):
pending ──应答──→ done
├──取消/超时──→ canceled ──迟到应答──→ done
└──进程退出──→ failed
迁移表集中在 legalTransition,非法组合一律拒绝(CAS 只校验 from,
不校验 to——实测曾把 done 改回 failed)。
## 3. 取消跨进程送达
内核超时到点后只停止「等」,插件侧的 handler 还在跑,占着连接/子进程/
文件句柄,结果却已被丢弃。现在 CallContext 取消时向插件发 call.cancel
通知(fire-and-forget),插件运行时按 **RPC id** 定位并 cancel 对应调用的 ctx。
不按工具名定位:同一工具可并发多次,按名字会误伤其它实例。
插件作者零改动:handler 签名不变,取消由模板运行时接住。
## 4. 取消超时判定插件不稳定
严格区分两类超时(用户裁定):
业务超时 = 任务跑太久 → 正常,不作为不稳定依据
取消超时 = 喊停之后还不停 → 异常,是插件失去响应的直接证据
取消宽限(cancelGrace=5s)到期仍无应答 ⇒ ① 强制回收该调用名下的扩容块
(插件已静默退出,永远不会归还)② 计数 ③ 连续两次触发既有崩溃重启链。
插件一旦响应取消即清零;重启后也清零(判据是"连续"不是"累计")。
判据:8 条新增单测 + 全部变异验证(不回收/不上报/不清零/取消不跨进程
任一处改坏,对应用例立刻变红)。
Co-Authored-By: Claude <noreply@anthropic.com>
85 lines
2.7 KiB
Go
85 lines
2.7 KiB
Go
package proc
|
||
|
||
// cancelProbePluginSource 是「取消是否真的送达」的观察装置。
|
||
//
|
||
// 设计要点:
|
||
// - probe_slow 用 **ToolHandlerCtx** 注册,观察 ctx 并记录结果;
|
||
// - 观察到取消后把状态写进包级变量(同进程,测试可直接读回);
|
||
// - probe_state 返回该变量,供测试轮询。
|
||
//
|
||
// 之所以能这么写:模板真的会把 call.cancel 变成 handler 的 ctx cancel
|
||
// (见 hmapdev 模板的 trackInflight/cancelInflight)。若那一环没接上,
|
||
// probe_slow 永远观察不到取消,测试自然红。
|
||
const cancelProbePluginSource = `package main
|
||
|
||
import (
|
||
"context"
|
||
"sync"
|
||
"time"
|
||
|
||
sdk "github.com/JianFeeeee/hologramagentsdk/sdk"
|
||
)
|
||
|
||
type probePlugin struct {
|
||
name string
|
||
mu sync.Mutex
|
||
last string
|
||
}
|
||
|
||
func NewPluginFactory(name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||
return &probePlugin{name: name}, nil
|
||
}
|
||
|
||
func (p *probePlugin) Name() string { return p.name }
|
||
|
||
func (p *probePlugin) Stop() error { return nil }
|
||
|
||
func (p *probePlugin) Start(s *sdk.PluginSDK) error {
|
||
s.RegisterToolCtx("probe_slow", sdk.ToolDef{
|
||
Description: "长任务,用于验证取消是否送达",
|
||
Parameters: map[string]interface{}{"type": "object", "properties": map[string]interface{}{}},
|
||
}, func(ctx context.Context, args map[string]interface{}) (interface{}, error) {
|
||
select {
|
||
case <-ctx.Done():
|
||
p.mu.Lock()
|
||
p.last = "canceled"
|
||
p.mu.Unlock()
|
||
return "canceled", ctx.Err()
|
||
case <-time.After(30 * time.Second):
|
||
p.mu.Lock()
|
||
p.last = "timeout"
|
||
p.mu.Unlock()
|
||
return "finished", nil
|
||
}
|
||
})
|
||
|
||
// probe_block:**用旧签名**注册,完全不看 ctx。
|
||
//
|
||
// 这个工具是"取消后不回应答"那条路径的**唯一**观察点:
|
||
// Ctx 版 handler 会因为 ctx 取消而自己提前返回,从而绕过
|
||
// "跑完后检查是否已被取消" 那段代码,测不到它。
|
||
// 旧签名 handler 跑满 300ms 后才返回——此时内核早已取消,
|
||
// 运行时必须靠 inflightCall.canceled 拦住应答。
|
||
s.RegisterTool("probe_block", sdk.ToolDef{
|
||
Description: "旧签名长任务:不看 ctx,用于验证取消后不回应答",
|
||
Parameters: map[string]interface{}{"type": "object", "properties": map[string]interface{}{}},
|
||
}, func(args map[string]interface{}) (interface{}, error) {
|
||
time.Sleep(300 * time.Millisecond)
|
||
return "finished", nil
|
||
})
|
||
|
||
s.RegisterTool("probe_state", sdk.ToolDef{
|
||
Description: "读回上一次长任务的观察结果",
|
||
Parameters: map[string]interface{}{"type": "object", "properties": map[string]interface{}{}},
|
||
}, func(args map[string]interface{}) (interface{}, error) {
|
||
p.mu.Lock()
|
||
defer p.mu.Unlock()
|
||
if p.last == "" {
|
||
return "idle", nil
|
||
}
|
||
return p.last, nil
|
||
})
|
||
return nil
|
||
}
|
||
`
|