mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-05 16:57:32 +00:00
fix(proc): arena 扩容块泄漏 + 取消成为一等状态
## 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>
This commit is contained in:
@ -224,12 +224,24 @@ func TestProcess_KillHungPlugin(t *testing.T) {
|
||||
t.Errorf("应为 DeadlineExceeded,实际 %v", err)
|
||||
}
|
||||
|
||||
// pending 条目必须已清理(不泄漏)
|
||||
// 在途条目必须被推进到**可解释的状态**,且最终会被回收(不泄漏)。
|
||||
//
|
||||
// 判据从"表必须立刻清空"改成了下面两条,因为语义改了:
|
||||
// 取消后条目**刻意保留**一会(cancelRetainFor),以便迟到的应答能被
|
||||
// 认出归属,而不是被当成"未知 id"丢弃。真正要防的泄漏是
|
||||
// "条目永远留着、状态说不清",而不是"取消后短暂保留"。
|
||||
p.mu.Lock()
|
||||
pendingCount := len(p.pending)
|
||||
residual := make([]*call, 0, len(p.inflight))
|
||||
for _, c := range p.inflight {
|
||||
residual = append(residual, c)
|
||||
}
|
||||
p.mu.Unlock()
|
||||
if pendingCount != 0 {
|
||||
t.Errorf("超时后 pending 表应清空,实际残留 %d 条", pendingCount)
|
||||
for _, c := range residual {
|
||||
// 允许 pending(应答还在路上)或 canceled(已放弃、等迟到应答);
|
||||
// done/failed 留在表里才是真泄漏(它们终态且不会再有后续)。
|
||||
if st := c.State(); st == callDone || st == callFailed {
|
||||
t.Errorf("终态调用 id=%d 仍留在 inflight 表里(泄漏)", c.id)
|
||||
}
|
||||
}
|
||||
|
||||
// Kill 真正回收资源
|
||||
@ -264,12 +276,12 @@ func TestProcess_NotifyDoesNotWait(t *testing.T) {
|
||||
t.Errorf("100 条通知耗时 %v,疑似在等应答(应 post-and-forget)", elapsed)
|
||||
}
|
||||
|
||||
// 通知不占 pending 表
|
||||
// 通知不占在途表
|
||||
p.mu.Lock()
|
||||
pendingCount := len(p.pending)
|
||||
inflightCount := len(p.inflight)
|
||||
p.mu.Unlock()
|
||||
if pendingCount != 0 {
|
||||
t.Errorf("通知不应占用 pending 表,实际 %d 条", pendingCount)
|
||||
if inflightCount != 0 {
|
||||
t.Errorf("通知不应占用 inflight 表,实际 %d 条", inflightCount)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user