perf(tools): 工具服务单实例并发(ServeTool 协程化)+ mcp-py 同改
垂直扩:单个 mcp 实例内每个工具调用分发到独立 goroutine/task 并发处理, 不再受 NATS 单订阅回调串行所限;配合队列组多副本即「单实例并发 × 副本数」横向扩。 - bus.ServeTool:回调立即起 goroutine 并返回(不阻塞 NATS 投递),信号量限并发 (MCP_TOOL_CONCURRENCY,默认 16),handler panic → 回错误结果避免调用方干等超时。 - mcp-py mcp_gateway:_on_call 改 asyncio.create_task 派发 + Semaphore 限并发(同默认 16)。 - 测试 TestConcurrentToolServe:slow 工具阻塞时 fast 工具仍返回(旧串行下会超时)。 验证:单测通过;mcp-go/mcp-py live 重启工具就绪,工具往返正常(memory/history/kb_search 均正常响应,无 panic)。全模块 build+vet+test 全绿。 注:下游共享后端(Milvus/Neo4j/PG/嵌入端点)是横向扩的最终上限,届时扩这些而非 mcp 实例。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -254,3 +254,55 @@ func TestConcurrentConsume(t *testing.T) {
|
||||
t.Fatal("B 被阻塞的 A 卡住——并发消费未生效(仍是串行)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestConcurrentToolServe 验证单实例工具服务并发:一个慢工具调用不阻塞另一个快调用。
|
||||
// 旧的串行 ServeTool 下本测试会超时失败。
|
||||
func TestConcurrentToolServe(t *testing.T) {
|
||||
url := startEmbeddedNATS(t)
|
||||
|
||||
srv, err := bus.Connect(url)
|
||||
if err != nil {
|
||||
t.Fatalf("server connect: %v", err)
|
||||
}
|
||||
defer srv.Close()
|
||||
|
||||
blockSlow := make(chan struct{})
|
||||
unsub, err := srv.ServeTool(contract.SubjectToolsGoAll, contract.QueueToolsGo,
|
||||
func(_ context.Context, call *contract.ToolCall) *contract.ToolResult {
|
||||
if call.Tool == "slow" {
|
||||
<-blockSlow // 模拟慢工具(如 RAG 嵌入)
|
||||
}
|
||||
return &contract.ToolResult{OK: true, Content: call.Tool}
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("serve tool: %v", err)
|
||||
}
|
||||
defer unsub()
|
||||
defer close(blockSlow)
|
||||
|
||||
cli, err := bus.Connect(url)
|
||||
if err != nil {
|
||||
t.Fatalf("client connect: %v", err)
|
||||
}
|
||||
defer cli.Close()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 先发 slow(会阻塞在 handler 里),再发 fast——fast 应在 slow 仍阻塞时返回。
|
||||
go func() {
|
||||
_, _ = cli.CallTool(ctx, contract.ToolSubjectGo("slow"), &contract.ToolCall{Tool: "slow"})
|
||||
}()
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
|
||||
fctx, fcancel := context.WithTimeout(ctx, 3*time.Second)
|
||||
defer fcancel()
|
||||
res, err := cli.CallTool(fctx, contract.ToolSubjectGo("fast"), &contract.ToolCall{Tool: "fast"})
|
||||
if err != nil {
|
||||
t.Fatalf("fast 工具被慢工具阻塞——单实例并发未生效: %v", err)
|
||||
}
|
||||
if res.Content != "fast" {
|
||||
t.Fatalf("unexpected result: %q", res.Content)
|
||||
}
|
||||
t.Log("✓ 单实例工具并发生效:slow 仍阻塞时 fast 已返回")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user