perf(dispatcher): 任务并发消费 —— 待审/慢任务不再阻塞其他任务

bus.ConsumeTasks 由单条串行改为限并发分发:每个任务进独立 worker goroutine,
并发上限 = 信号量 + 消费者 MaxAckPending(DISPATCHER_CONCURRENCY,默认 8)。
- 背压:并发满则在 select{sem, ctx.Done} 处等空位;关停时留消息不 ack 待重投。
- 健壮:worker 内 recover panic → Term(避免崩溃循环);ack/nak/span 收口在 worker。
- 并发安全已核:CircuitBreaker 有锁、Orchestrator.turns 有 turnMu、pool RWMutex、
  evaluate 本就异步。

效果:一个 HITL 待审任务(Handle 阻塞至多 5min)或长 LLM 生成不再冻结后续任务。
验证:单测 TestConcurrentConsume(A 阻塞时 B 完成);live 实测 HITL 停 waiting 期间
普通任务 3s 跑完且 HITL 不受影响。全模块 build+vet+test 全绿。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-06-24 15:03:00 +08:00
parent d7c6fab933
commit e3b0a4f81c
2 changed files with 105 additions and 14 deletions
+60
View File
@@ -194,3 +194,63 @@ func TestTokenStreamRoundTrip(t *testing.T) {
t.Fatal("timeout: 未收到流结束信号")
}
}
// TestConcurrentConsume 验证并发消费:一个长时间阻塞的任务(模拟 HITL 待审)
// 不应阻塞后续任务——旧的串行消费下本测试会超时失败。
func TestConcurrentConsume(t *testing.T) {
url := startEmbeddedNATS(t)
gw, err := bus.Connect(url)
if err != nil {
t.Fatalf("gateway connect: %v", err)
}
defer gw.Close()
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
if err := gw.EnsureTaskStream(ctx); err != nil {
t.Fatalf("ensure stream: %v", err)
}
dp, err := bus.Connect(url)
if err != nil {
t.Fatalf("dispatcher connect: %v", err)
}
defer dp.Close()
blockA := make(chan struct{})
doneB := make(chan string, 1)
stop, err := dp.ConsumeTasks(ctx, func(_ context.Context, task *contract.Task) error {
switch task.ID {
case "A":
<-blockA // 模拟审批长阻塞
case "B":
doneB <- task.ID
}
return nil
})
if err != nil {
t.Fatalf("consume: %v", err)
}
defer stop()
defer close(blockA)
// 先发 A 并等它被消费、卡在 handler 里;再发 B。
if _, err := gw.PublishTask(ctx, &contract.Task{ID: "A", Graph: json.RawMessage(`{}`)}); err != nil {
t.Fatalf("publish A: %v", err)
}
time.Sleep(400 * time.Millisecond)
if _, err := gw.PublishTask(ctx, &contract.Task{ID: "B", Graph: json.RawMessage(`{}`)}); err != nil {
t.Fatalf("publish B: %v", err)
}
select {
case id := <-doneB:
if id != "B" {
t.Fatalf("unexpected task done: %q", id)
}
t.Log("✓ 并发消费生效:A 仍阻塞时 B 已完成")
case <-time.After(4 * time.Second):
t.Fatal("B 被阻塞的 A 卡住——并发消费未生效(仍是串行)")
}
}