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:
@@ -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 卡住——并发消费未生效(仍是串行)")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user