Files
HomeAgent/internal/agent/core/scheduler_starvation_test.go
JianFeeeee a971fc8877 feat(scheduler): M5 饥饿防护(抢占计数提升有效级 + 抢占冷却)
设计依据 docs/zh/input-scheduler-design.md §9、§11.5(G1/G2)。

- Task 增加 PreemptCount / LastPreemptAt;effectiveLevel(t) =
  min(L4, Level + min(PreemptCount, 2)):被抢占越多越“值钱”,
  逐步追上抢占它的流,但封顶 L4 因而抢不过真正的紧急输入
- 选择函数 taskBefore 改用有效级;requestPreempt 与 preemptGrantedFor
  同样以有效级比较
- 抢占冷却 preemptCooldown=2s:刚被抢占的任务期内不再被抢占,
  避免同一任务被反复打断到永不完结
- 新增 scheduler_starvation_test.go 4 项:提升与封顶、冷却期内不得再抢占、
  提升后同级不得抢占而更高可、选择函数确实用有效级
- 验收:agent 全量 + -race;全仓 build/vet 通过
2026-09-13 00:38:30 +08:00

119 lines
4.1 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.

package core
// M5 验收测试:饥饿防护(抢占计数提升有效级 + 抢占冷却)。
//
// 设计依据 docs/zh/input-scheduler-design.md §9、§11.5G1/G2
//
// 为什么需要:固定四级 + 「严格大于才抢占」下,一条 L4 流可以反复打断同一个
// L1 任务,使它永不完结。提升被抢占者的**有效**优先级,让它在竞争排队时
// 逐步追上;封顶 L4因此它永远抢不过真正的紧急输入紧急输入本身不被抢占
import (
"testing"
"time"
)
func TestStarvation_EffectiveLevelPromotion(t *testing.T) {
base := &Task{Level: LevelBackground}
if got := effectiveLevel(base); got != LevelBackground {
t.Fatalf("未抢占时有效级=%v期望 L1", got)
}
base.PreemptCount = 1
if got := effectiveLevel(base); got != LevelMessage {
t.Fatalf("被抢占 1 次后有效级=%v期望 L2", got)
}
base.PreemptCount = 2
if got := effectiveLevel(base); got != LevelInteractive {
t.Fatalf("被抢占 2 次后有效级=%v期望 L3", got)
}
base.PreemptCount = 99
if got := effectiveLevel(base); got != LevelInteractive {
t.Fatalf("提升应封顶在 +2 档,实际 %v", got)
}
// 封顶 L4L3 任务被多次抢占也不会超过紧急级。
high := &Task{Level: LevelInteractive, PreemptCount: 99}
if got := effectiveLevel(high); got != LevelCritical {
t.Fatalf("L3 提升后应封顶为 L4实际 %v", got)
}
}
func TestStarvation_CooldownBlocksImmediateRepreempt(t *testing.T) {
a := newPreemptAgent(t, newPreemptProvider())
low := &Task{ID: 1, Level: LevelBackground, EnqueuedAt: time.Now()}
a.sched.enqueue(low)
a.sched.nextRef() // running = low
e1, _ := textEvent("qq", "第一次打断")
if !a.sched.requestPreempt(e1, LevelMessage) {
t.Fatal("L2 应能抢占 L1首次")
}
a.sched.suspend(low, a.newTaskFrame("x", a.stageCtxFromInput("x", "", "")))
if low.PreemptCount != 1 {
t.Fatalf("PreemptCount=%d期望 1", low.PreemptCount)
}
if low.LastPreemptAt.IsZero() {
t.Fatal("挂起必须记录 LastPreemptAt冷却起点")
}
// 冷却期内:即使 L4 也不得再抢占。
a.sched.mu.Lock()
a.sched.running = low
a.sched.mu.Unlock()
e2, _ := textEvent("cli", "冷却期内的紧急打断")
if a.sched.requestPreempt(e2, LevelCritical) {
t.Fatal("抢占冷却期内不得再抢占")
}
if a.sched.preemptGrantedFor() {
t.Fatal("冷却期内不得 arm 让位信号")
}
}
func TestStarvation_PromotionBlocksSameLevelPreempt(t *testing.T) {
a := newPreemptAgent(t, newPreemptProvider())
low := &Task{ID: 1, Level: LevelBackground, EnqueuedAt: time.Now()}
a.sched.enqueue(low)
a.sched.nextRef()
// 模拟「已被抢占过一次」:有效级 = L2。
low.PreemptCount = 1
low.LastPreemptAt = time.Now().Add(-time.Hour) // 冷却已过
e1, _ := textEvent("qq", "同级打断")
if a.sched.requestPreempt(e1, LevelMessage) {
t.Fatal("有效级 L2 时L2 中断不得抢占(严格大于才抢占)")
}
e2, _ := textEvent("cli", "更高级打断")
if !a.sched.requestPreempt(e2, LevelInteractive) {
t.Fatal("L3 应能抢占有效级 L2")
}
if !a.sched.preemptGrantedFor() {
t.Fatal("L3 > 有效 L2应已 arm")
}
}
// 选择函数必须用有效级:被抢占过的任务在排队时应当优先于同级/更低的任务。
func TestStarvation_SelectionUsesEffectiveLevel(t *testing.T) {
base := time.Now()
promoted := &Task{ID: 1, Level: LevelBackground, PreemptCount: 2, EnqueuedAt: base} // 有效 L3
normal := &Task{ID: 2, Level: LevelMessage, EnqueuedAt: base.Add(time.Second)} // L2
if !taskBefore(promoted, normal) {
t.Fatal("被抢占 2 次的 L1有效 L3应先于 L2 执行")
}
if taskBefore(normal, promoted) {
t.Fatal("选择函数不得只看基础级")
}
// 提升不改变调度器自身的排序稳定性:同为有效级时按入队时刻。
a := &Task{ID: 3, Level: LevelBackground, PreemptCount: 1, EnqueuedAt: base.Add(2 * time.Second)} // 有效 L2
b := &Task{ID: 4, Level: LevelMessage, EnqueuedAt: base.Add(time.Second)} // L2更早
if !taskBefore(b, a) {
t.Fatal("同有效级时应先到先服务")
}
}