feat(scheduler)!: 中断/排队两类别模型 + 插件声明 L1-L3、L4 内核独占

用户澄清推翻了早期设计的三处前提,本提交按新模型重做调度核心(行为有意变化):

1) 类别由注入 API 决定,与通道名无关
   - InjectInterrupt*                      -> TaskInterrupt(带级别,可被严格更高级中断打断)
   - InjectText*/InjectInputSync*/内核自循环 -> TaskQueued(无级别,可被任何中断打断)
   - 删除按通道名推断的 taskLevel():qq 走 InjectInterruptTextOpts,本就是中断

2) 级别只属于中断
   - 插件在 InjectOptions.Priority 声明 L1-L3(空/非法降级 L1,声明 L4 夹到 L3)
   - L4 内核独占:新增 raiseKernelInterrupt(panic / selfip);requestKernelPreempt 不夹取
   - panic 现在产生一条带 kernel 标记的 L4 中断;L4 自身 panic 不再产生新 L4(防自我放大)

3) 选择结构:四容器固定次序,删除统一比较器
   - immediate(抢占者立即运行)-> 中断队列 L4..L1 -> 栈顶(与队头比级别) -> 排队 FIFO
   - 删除 pickTaskIndex/taskBefore 与“同级 pending 优先”补丁(根因是抢占者进了队列)
   - 中断栈上界改为结构推论 = 4(= 中断级数);删除“超限转 pendingInterrupts”降级

公开 SDK(feature 分支有意新增,纯追加):InjectOptions.Priority + PriorityL1/2/3;
内核 io / proc 桥 / 插件模板同步透传。

设计稿 §2/§3/§4.1/§6.3/§9/§11/§12/§13/§15 按新模型重写。

验收:go build/vet 干净;go test ./... 37 包 ok 0 FAIL;-race 全绿;
e2e(抢占-挂起-恢复)+ 压力(200 排队 + 50 中断,L1/L2/L3 轮转)通过。
This commit is contained in:
JianFeeeee
2026-09-13 06:57:23 +08:00
parent 86a702b3b0
commit cb32032f76
16 changed files with 1021 additions and 443 deletions

View File

@ -74,8 +74,12 @@ func TestScheduler_StressMixedLoad(t *testing.T) {
for i := 0; i < nInputs; i++ {
a.io.InjectInput("cli", "text", map[string]interface{}{"content": fmt.Sprintf("msg-%d", i)})
}
// 中断按 L1/L2/L3 轮转:把“四条中断队列按级别高→低扫描”真正压上,
// 而不只是排空一条队列。
levels := []string{"L1", "L2", "L3"}
for i := 0; i < nInterrupts; i++ {
a.io.InjectInterruptText("qq", "cli", fmt.Sprintf("intr-%d", i))
a.io.InjectInterruptTextOpts("qq", "cli", fmt.Sprintf("intr-%d", i),
agentIO.InjectOptions{Priority: levels[i%len(levels)]})
}
snap := waitQuiescent(t, a, nInputs+nInterrupts, 30*time.Second)
@ -129,7 +133,7 @@ func TestObservability_SchedulerEventsAndStatus(t *testing.T) {
intrEvt, _ := textEvent("cli", "紧急")
intrEvt.Payload["interrupt"] = true
a.sched.requestPreempt(intrEvt, LevelCritical)
a.sched.requestKernelPreempt(intrEvt)
a.cancelCurrentLLM()
<-done
@ -185,16 +189,18 @@ func TestE2E_RealLoopPreemption(t *testing.T) {
a.Start()
defer a.Stop()
// L1:qq 入站消息 → 阻塞在第一次 LLM 调用
a.io.InjectInput("qq", "text", map[string]interface{}{"content": "低优先级长任务"})
// 排队输入:qq 入站消息 → 阻塞在第一次 LLM 调用(排队任务无级别)
a.io.InjectInput("qq", "text", map[string]interface{}{"content": "长任务"})
select {
case <-sp.entered:
case <-time.After(5 * time.Second):
t.Fatal("低优先级任务未进入 LLM")
}
// L4:cli 紧急打断 → interceptLoop 应取消 LLM、登记抢占
a.io.InjectInterruptText("cli", "cli", "紧急打断")
// 插件声明的 L3 中断:interceptLoop 应取消 LLM 并登记抢占。
// 注意它能打断**排队任务**不是因为级别高,而是因为排队任务无级别——
// 任何中断都大于它。
a.io.InjectInterruptTextOpts("cli", "cli", "紧急打断", agentIO.InjectOptions{Priority: "L3"})
a.io.InjectInput("cli", "text", map[string]interface{}{"content": "后续常规输入"})
// 排空:中断任务 + 被恢复的原任务 + 后续常规输入