mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-22 18:08:04 +00:00
fix(offload): 内核说明不能被再转投(自我循环)+ offload_owned 未接线
★ 线上实测两个缺陷: 1. **自我循环**:转投会在队列留一条 [系统] 说明(source=kernel), 而转投条件把这条说明也算进「积压够了」⇒ 每次转投都产生下一轮要转投的东西。 实测 5 秒内连续触发两次,分诊助手不断收到「N 条积压已转投」这类噪音。 修法:takeQueuedInputs 排除 isKernelNotice(source=kernel)。 2. **offload_owned 永远为 false**:我加了 ResidentInfo 字段、加了状态面映射, 却漏了在 rc.info() 里赋值 ⇒ 线上转投子明明存在,读出来是 null。 这类「加了字段但没接线」不会报错,只会让父的判断悄悄失效 (父据此决定回收策略,读到 false 就会把临时助手当成正式子)。 测试 +3:说明不转投 / 循环必须终止 / offload_owned 会被上报。 前两条已实测「禁用守卫会失败、恢复后通过」,是真回归测试。
This commit is contained in:
@ -580,3 +580,119 @@ func TestResidualEmptyIsReported(t *testing.T) {
|
||||
t.Errorf("应报告无残余任务,实际 n=%d msg=%q", n, msg)
|
||||
}
|
||||
}
|
||||
|
||||
// ★ 回归:offload_owned 必须真的**被填上**。
|
||||
//
|
||||
// 我第一版加了字段、加了状态面映射,却漏了在 rc.info() 里赋值 ⇒ 父读到的
|
||||
// 永远是 false(实测:线上转投子明明存在,offload_owned 却是 null)。
|
||||
// 这类"加了字段但没接线"的缺陷不会报错,只会让上层判断悄悄失效。
|
||||
func TestOffloadOwnedIsReported(t *testing.T) {
|
||||
root, main := newRootWithoutSchedulerLoop(t)
|
||||
defer main.Close()
|
||||
|
||||
// 人工建的子(offload_owned 应为 false)
|
||||
manual, err := root.SpawnResident(ResidentOptions{
|
||||
ID: "manual-child", TempPath: root.residentTempPath("manual-child"),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("创建驻留子失败: %v", err)
|
||||
}
|
||||
if manual.OffloadOwned {
|
||||
t.Error("人工创建的子不应被标记为 offload_owned")
|
||||
}
|
||||
|
||||
// 内核为转投拉起的子(应为 true)
|
||||
opts := DefaultOffloadOptions()
|
||||
opts.Enabled = true
|
||||
opts.BusyAfter = time.Nanosecond
|
||||
opts.MinPending = 1
|
||||
opts.MaxResidents = 1
|
||||
root.sched.enqueue(makeQueuedInput(1))
|
||||
root.sched.nextRef()
|
||||
root.sched.enqueue(makeQueuedInput(2))
|
||||
if n := root.offloadPendingTasks(opts); n != 1 {
|
||||
t.Fatalf("应转投 1 条,实际 %d", n)
|
||||
}
|
||||
|
||||
list := root.Residents()
|
||||
var foundOffload *ResidentInfo
|
||||
for i := range list {
|
||||
if strings.HasPrefix(list[i].ID, "offload-") {
|
||||
foundOffload = &list[i]
|
||||
}
|
||||
}
|
||||
if foundOffload == nil {
|
||||
t.Fatal("未找到转投子")
|
||||
}
|
||||
if !foundOffload.OffloadOwned {
|
||||
t.Error("★ 转投子必须被标记 offload_owned=true(父据此决定回收策略)")
|
||||
}
|
||||
}
|
||||
|
||||
// ★★ 回归:内核自己留的说明**不能再被转投**,否则自我循环。
|
||||
//
|
||||
// 我第一版漏了这一步:转投会在队列里留一条 [系统] 说明(source=kernel),
|
||||
// 而转投条件("排队输入够了")又会把这条说明算进去 ⇒ 每次转投都产生下一轮
|
||||
// 要转投的东西。实测:5 秒内连续触发两次,分诊助手不断收到这类噪音。
|
||||
func TestKernelNoticeIsNeverOffloaded(t *testing.T) {
|
||||
root, main := newRootWithoutSchedulerLoop(t)
|
||||
defer main.Close()
|
||||
|
||||
opts := DefaultOffloadOptions()
|
||||
opts.Enabled = true
|
||||
opts.BusyAfter = time.Nanosecond
|
||||
opts.MinPending = 1
|
||||
opts.MaxResidents = 1
|
||||
|
||||
root.sched.enqueue(makeQueuedInput(1))
|
||||
root.sched.nextRef()
|
||||
|
||||
// 队列里放一条"内核说明"+ 一条真实积压
|
||||
root.sched.enqueue(newInputTask(root.syntheticEvent("2 条积压已交由临时助手分诊")))
|
||||
root.sched.enqueue(makeQueuedInput(2))
|
||||
|
||||
moved := root.offloadPendingTasks(opts)
|
||||
if moved != 1 {
|
||||
t.Fatalf("★ 只应转投真实积压 1 条(说明不可转投),实际 %d", moved)
|
||||
}
|
||||
// 说明必须还在队列里(留给主 agent 看),不能被搬走
|
||||
var noticeLeft bool
|
||||
for _, task := range root.sched.queue {
|
||||
if task.Event != nil && isKernelNotice(task.Event) {
|
||||
noticeLeft = true
|
||||
}
|
||||
}
|
||||
if !noticeLeft {
|
||||
t.Error("内核说明应留在队列里给主 agent 看,不应被转投走")
|
||||
}
|
||||
}
|
||||
|
||||
// 转投自身产生的说明也不能构成下一轮的积压(循环必须终止)。
|
||||
func TestOffloadDoesNotLoopOnOwnNotice(t *testing.T) {
|
||||
root, main := newRootWithoutSchedulerLoop(t)
|
||||
defer main.Close()
|
||||
|
||||
opts := DefaultOffloadOptions()
|
||||
opts.Enabled = true
|
||||
opts.BusyAfter = time.Nanosecond
|
||||
opts.MinPending = 2
|
||||
opts.MaxResidents = 1
|
||||
|
||||
root.sched.enqueue(makeQueuedInput(1))
|
||||
root.sched.nextRef()
|
||||
root.sched.enqueue(makeQueuedInput(2))
|
||||
root.sched.enqueue(makeQueuedInput(3))
|
||||
|
||||
if n := root.offloadPendingTasks(opts); n != 2 {
|
||||
t.Fatalf("第一轮应转 2 条,实际 %d", n)
|
||||
}
|
||||
// 再调若干次:队列里只剩一条说明,不够 MinPending ⇒ 不该再转
|
||||
for i := 0; i < 5; i++ {
|
||||
if n := root.offloadPendingTasks(opts); n != 0 {
|
||||
t.Fatalf("第 %d 次仍在转投(自我循环):转了 %d 条", i+1, n)
|
||||
}
|
||||
}
|
||||
if got := len(root.Residents()); got != 1 {
|
||||
t.Errorf("不应反复拉起新子,实际 %d 个", got)
|
||||
}
|
||||
}
|
||||
|
||||
@ -662,6 +662,9 @@ func (rc *residentChild) info() ResidentInfo {
|
||||
// 注意它**不等于** len(table):处理表记的是"当前上下文窗口内"的轮次,
|
||||
// 压缩会清空(§8.3),所以窗口内的条数会被重置,而轮次总数不会。
|
||||
Rounds: rc.agent.roundsExecuted(),
|
||||
// OffloadOwned:让父分清"我建的子"与"内核临时拉的分诊助手"。
|
||||
// 父据此决定回收策略(临时的可以空闲时回收,正式的按需保留)。
|
||||
OffloadOwned: rc.offloadOwned,
|
||||
}
|
||||
if len(table) > 0 {
|
||||
info.Table = table
|
||||
|
||||
@ -454,6 +454,14 @@ func (s *scheduler) takeQueuedInputs(min int) []offloadCandidate {
|
||||
if t == nil || t.Class != TaskQueued || t.Kind != TaskKindInput || t.Event == nil {
|
||||
continue
|
||||
}
|
||||
// ❗内核自己造的说明(source=kernel)**不能再被转投**。
|
||||
//
|
||||
// 它本来就是"告知这条信息不用再处理"的通知,而转投又会在队列里留下
|
||||
// 一条新的同款通知 ⇒ 自我循环:每次转投都产生下一轮要转投的东西。
|
||||
// 实测(2026-09-19):5 秒内连续触发两次,子侧不断收到这类噪音。
|
||||
if isKernelNotice(t.Event) {
|
||||
continue
|
||||
}
|
||||
out = append(out, offloadCandidate{Event: t.Event})
|
||||
if len(out) >= min {
|
||||
break
|
||||
@ -1377,3 +1385,14 @@ func (a *Agent) resumeTask(t *Task, f *TaskFrame) {
|
||||
a.finishInputTask(f, out)
|
||||
a.sched.done(t)
|
||||
}
|
||||
|
||||
// kernelNoticeSource 是内核自己造的输入事件的来源名(见 offload.syntheticEvent)。
|
||||
//
|
||||
// 它存在的主要理由是**可被识别**:转投等机制必须能排除"自己造的说明",
|
||||
// 否则会在队列里自我循环(说明 → 被转投 → 产生新说明 → …)。
|
||||
const kernelNoticeSource = "kernel"
|
||||
|
||||
// isKernelNotice 判断一条输入是不是内核自己造的通知。
|
||||
func isKernelNotice(evt *agentIO.InputEvent) bool {
|
||||
return evt != nil && evt.Source == kernelNoticeSource
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user