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:
@@ -47,6 +47,8 @@ class McpGateway:
|
||||
self.interpreter = CodeInterpreter() # Docker 隔离沙箱
|
||||
self._nc: nats.NATS | None = None
|
||||
self._tools: dict[str, callable] = {}
|
||||
# 单实例工具并发上限(垂直扩);队列组多副本再叠加水平扩。
|
||||
self._sem = asyncio.Semaphore(int(os.getenv("MCP_TOOL_CONCURRENCY", "16")))
|
||||
|
||||
async def register_tools(self) -> None:
|
||||
"""注册工具名 → 处理协程。"""
|
||||
@@ -77,25 +79,30 @@ class McpGateway:
|
||||
await asyncio.Event().wait()
|
||||
|
||||
async def _on_call(self, msg) -> None:
|
||||
"""解析 ToolCall → 路由到工具 → Respond ToolResult。"""
|
||||
try:
|
||||
req = json.loads(msg.data)
|
||||
except Exception as e: # noqa: BLE001
|
||||
await self._reply(msg, ok=False, error=f"bad tool call: {e}")
|
||||
return
|
||||
tool = req.get("tool", "")
|
||||
task_id = req.get("task_id", "")
|
||||
args = req.get("args") or {}
|
||||
log.info("[mcp_py] tool=%s task=%s args=%s", tool, task_id, args)
|
||||
fn = self._tools.get(tool)
|
||||
if fn is None:
|
||||
await self._reply(msg, ok=False, error=f"unknown tool: {tool}")
|
||||
return
|
||||
try:
|
||||
content = await fn(args)
|
||||
await self._reply(msg, ok=True, content=content)
|
||||
except Exception as e: # noqa: BLE001
|
||||
await self._reply(msg, ok=False, error=str(e))
|
||||
"""立即派发到独立 task 并发处理,不阻塞订阅消息循环(nats-py 单订阅回调本是串行 await)。"""
|
||||
asyncio.create_task(self._handle(msg))
|
||||
|
||||
async def _handle(self, msg) -> None:
|
||||
"""解析 ToolCall → 路由到工具 → Respond ToolResult(限并发)。"""
|
||||
async with self._sem:
|
||||
try:
|
||||
req = json.loads(msg.data)
|
||||
except Exception as e: # noqa: BLE001
|
||||
await self._reply(msg, ok=False, error=f"bad tool call: {e}")
|
||||
return
|
||||
tool = req.get("tool", "")
|
||||
task_id = req.get("task_id", "")
|
||||
args = req.get("args") or {}
|
||||
log.info("[mcp_py] tool=%s task=%s args=%s", tool, task_id, args)
|
||||
fn = self._tools.get(tool)
|
||||
if fn is None:
|
||||
await self._reply(msg, ok=False, error=f"unknown tool: {tool}")
|
||||
return
|
||||
try:
|
||||
content = await fn(args)
|
||||
await self._reply(msg, ok=True, content=content)
|
||||
except Exception as e: # noqa: BLE001
|
||||
await self._reply(msg, ok=False, error=str(e))
|
||||
|
||||
@staticmethod
|
||||
async def _reply(msg, *, ok: bool, content: str = "", error: str = "") -> None:
|
||||
|
||||
Reference in New Issue
Block a user