mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-01 14:57:38 +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>
193 lines
6.1 KiB
Go
193 lines
6.1 KiB
Go
package proc
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
|
||
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
|
||
)
|
||
|
||
// handlePluginMgr 处理插件管理:reloadOne / listLoaded / isDisabled。
|
||
//
|
||
// 本函数体是 corehandler.go 里 Handle 那一个大 switch 的**整块平移**:
|
||
// case 标签与 case 体逐字保留,只换了宿主函数(§3.2 的平移原则)。
|
||
func (h *coreHandler) handlePluginMgr(method string, params json.RawMessage) (interface{}, error) {
|
||
switch method {
|
||
// ---- 插件管理(原 case 48/49/50)----
|
||
case MethodPluginReloadOne:
|
||
pm := h.sdk.PluginMgr()
|
||
if pm == nil {
|
||
return nil, errUnavailable("plugin manager")
|
||
}
|
||
var p struct {
|
||
Name string `json:"name"`
|
||
}
|
||
if err := unmarshal(params, &p); err != nil {
|
||
return nil, err
|
||
}
|
||
return nil, pm.ReloadOne(p.Name)
|
||
case MethodPluginListLoaded:
|
||
pm := h.sdk.PluginMgr()
|
||
if pm == nil {
|
||
return nil, errUnavailable("plugin manager")
|
||
}
|
||
list := pm.ListLoadedPlugins()
|
||
if list == nil {
|
||
list = []string{}
|
||
}
|
||
return map[string]interface{}{"plugins": list}, nil
|
||
case MethodPluginIsDisabled:
|
||
pm := h.sdk.PluginMgr()
|
||
if pm == nil {
|
||
return nil, errUnavailable("plugin manager")
|
||
}
|
||
var p struct {
|
||
Name string `json:"name"`
|
||
}
|
||
if err := unmarshal(params, &p); err != nil {
|
||
return nil, err
|
||
}
|
||
return map[string]interface{}{"disabled": pm.IsPluginDisabled(p.Name)}, nil
|
||
case MethodPluginMgrStop:
|
||
pm := h.sdk.PluginMgr()
|
||
if pm == nil {
|
||
return nil, errUnavailable("plugin manager")
|
||
}
|
||
var p struct {
|
||
Name string `json:"name"`
|
||
}
|
||
if err := unmarshal(params, &p); err != nil {
|
||
return nil, err
|
||
}
|
||
return nil, pm.StopPlugin(p.Name)
|
||
case MethodPluginMgrStart:
|
||
pm := h.sdk.PluginMgr()
|
||
if pm == nil {
|
||
return nil, errUnavailable("plugin manager")
|
||
}
|
||
var p struct {
|
||
Name string `json:"name"`
|
||
}
|
||
if err := unmarshal(params, &p); err != nil {
|
||
return nil, err
|
||
}
|
||
return nil, pm.StartPlugin(p.Name)
|
||
case MethodPluginMgrIsRuntimeStopped:
|
||
pm := h.sdk.PluginMgr()
|
||
if pm == nil {
|
||
return nil, errUnavailable("plugin manager")
|
||
}
|
||
var p struct {
|
||
Name string `json:"name"`
|
||
}
|
||
if err := unmarshal(params, &p); err != nil {
|
||
return nil, err
|
||
}
|
||
return map[string]interface{}{"runtime_stopped": pm.IsPluginRuntimeStopped(p.Name)}, nil
|
||
|
||
}
|
||
|
||
// 组内不应到达:Handle 的分派表与本函数的 case 标签同源,
|
||
// 出现即说明两处不同步。
|
||
return nil, fmt.Errorf("未知 method: %s", method)
|
||
}
|
||
|
||
// handleStageLocks 处理共享段锁仲裁(§3.7)。
|
||
//
|
||
// 本函数体是 corehandler.go 里 Handle 那一个大 switch 的**整块平移**:
|
||
// case 标签与 case 体逐字保留,只换了宿主函数(§3.2 的平移原则)。
|
||
func (h *coreHandler) handleStageLocks(method string, params json.RawMessage) (interface{}, error) {
|
||
switch method {
|
||
// ---- 共享段锁仲裁(新增,§3.7)----
|
||
case MethodStageLock:
|
||
if h.locks == nil {
|
||
return nil, fmt.Errorf("stage.lock: 锁仲裁未就绪")
|
||
}
|
||
return nil, h.locks.acquire(h.name)
|
||
case MethodStageUnlock:
|
||
if h.locks == nil {
|
||
return nil, fmt.Errorf("stage.unlock: 锁仲裁未就绪")
|
||
}
|
||
return nil, h.locks.release(h.name)
|
||
|
||
}
|
||
|
||
// 组内不应到达:Handle 的分派表与本函数的 case 标签同源,
|
||
// 出现即说明两处不同步。
|
||
return nil, fmt.Errorf("未知 method: %s", method)
|
||
}
|
||
|
||
// handleEvents 处理事件订阅(§3.6,子进程下首次真正可用)。
|
||
//
|
||
// 本函数体是 corehandler.go 里 Handle 那一个大 switch 的**整块平移**:
|
||
// case 标签与 case 体逐字保留,只换了宿主函数(§3.2 的平移原则)。
|
||
func (h *coreHandler) handleEvents(method string, params json.RawMessage) (interface{}, error) {
|
||
switch method {
|
||
// ---- 事件订阅(原 case 23/24,子进程下首次真正可用,§3.6)----
|
||
case MethodEventsSubscribe:
|
||
var p struct {
|
||
Types []pubsdk.EventType `json:"types"`
|
||
}
|
||
if err := unmarshal(params, &p); err != nil {
|
||
return nil, err
|
||
}
|
||
if h.evtRing == nil {
|
||
return nil, fmt.Errorf("%s: 事件环未就绪", method)
|
||
}
|
||
// 订阅请求来自子进程——handler 直接注册到 Bus,
|
||
// 事件经 EventRing 写入环后由子进程消费。
|
||
//
|
||
// ★ 用 tracked 版本:订阅会被登记,Host.Close 在内核关停时统一退订。
|
||
// 必须如此——这些 handler 写共享内存,而 Host.Close 会 munmap 整块区域;
|
||
// 未退订的 handler 在关停后会写已解除映射的内存 ⇒ SIGSEGV。
|
||
h.evtRing.EvtRingSubscribeTracked(p.Types)
|
||
return nil, nil
|
||
|
||
case MethodEventsUnsubscribe:
|
||
// 事件环的订阅没有**按插件**持久化句柄(取消函数由 Subscribe 返回,
|
||
// 但子进程不保存,故无法精确撤销单个插件的订阅)。
|
||
// 当前设计:子进程 Stop 时由内核统一清理——具体落点是
|
||
// Host.Close → evtCloser.Close 退订全部 tracked 订阅。
|
||
// 因此这里仍是 no-op;但「统一清理」现在是真的有实现,
|
||
// 不再是只写在注释里的承诺。
|
||
return nil, nil
|
||
|
||
}
|
||
|
||
// 组内不应到达:Handle 的分派表与本函数的 case 标签同源,
|
||
// 出现即说明两处不同步。
|
||
return nil, fmt.Errorf("未知 method: %s", method)
|
||
}
|
||
|
||
// handleArena 处理共享槽池分配/释放(内部传输层,见 protocol.go)。
|
||
//
|
||
// 本函数体是 corehandler.go 里 Handle 那一个大 switch 的**整块平移**:
|
||
// case 标签与 case 体逐字保留,只换了宿主函数(§3.2 的平移原则)。
|
||
func (h *coreHandler) handleArena(method string, params json.RawMessage) (interface{}, error) {
|
||
switch method {
|
||
// ---- 共享槽池(内部传输层,见 protocol.go 注释)----
|
||
case MethodArenaAlloc:
|
||
var p ArenaAllocParams
|
||
if err := unmarshal(params, &p); err != nil {
|
||
return nil, err
|
||
}
|
||
ref, err := h.arenaAlloc(p.Size, p.CallID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return ArenaAllocResult{Ref: ref}, nil
|
||
|
||
case MethodArenaFree:
|
||
var p ArenaFreeParams
|
||
if err := unmarshal(params, &p); err != nil {
|
||
return nil, err
|
||
}
|
||
return nil, h.arenaFree(p.Ref)
|
||
|
||
}
|
||
|
||
// 组内不应到达:Handle 的分派表与本函数的 case 标签同源,
|
||
// 出现即说明两处不同步。
|
||
return nil, fmt.Errorf("未知 method: %s", method)
|
||
}
|