This commit is contained in:
Timothy Jaeryang Baek
2026-04-24 17:04:47 +09:00
parent 752238247c
commit 9771898c58
3 changed files with 27 additions and 13 deletions
+18 -5
View File
@@ -1923,8 +1923,6 @@ async def chat_completion(
# uses anyio task groups whose cancel scopes enforce # uses anyio task groups whose cancel scopes enforce
# same-task exit. Do NOT wrap in asyncio.shield() or # same-task exit. Do NOT wrap in asyncio.shield() or
# asyncio.wait_for() — both create a new task. # asyncio.wait_for() — both create a new task.
# MCPClient.disconnect() self-shields via
# anyio.CancelScope(shield=True).
try: try:
if mcp_clients := metadata.get('mcp_clients'): if mcp_clients := metadata.get('mcp_clients'):
for client in reversed(list(mcp_clients.values())): for client in reversed(list(mcp_clients.values())):
@@ -1932,14 +1930,29 @@ async def chat_completion(
await client.disconnect() await client.disconnect()
except Exception as e: except Exception as e:
log.debug(f'Error disconnecting MCP client: {e}') log.debug(f'Error disconnecting MCP client: {e}')
except asyncio.CancelledError:
# Let the client close asynchronously by GC
pass
except Exception as e: except Exception as e:
log.debug(f'Error cleaning up MCP clients: {e}') log.debug(f'Error cleaning up MCP clients: {e}')
except asyncio.CancelledError:
pass
try: try:
if metadata.get('chat_id'): if metadata.get('chat_id'):
event_emitter = await get_event_emitter(metadata, update_db=False) async def emit_inactive_event():
if event_emitter: try:
await event_emitter({'type': 'chat:active', 'data': {'active': False}}) event_emitter = await get_event_emitter(metadata, update_db=False)
if event_emitter:
await event_emitter({'type': 'chat:active', 'data': {'active': False}})
except Exception:
pass
try:
# Shield the event emission so it finishes even if the main task is cancelled
await asyncio.shield(emit_inactive_event())
except asyncio.CancelledError:
pass
except Exception: except Exception:
pass pass
+8 -8
View File
@@ -156,14 +156,14 @@ class MCPClient:
try: try:
# IMPORTANT: Do NOT use asyncio.shield() or asyncio.wait_for() # IMPORTANT: Do NOT use asyncio.shield() or asyncio.wait_for()
# here — both create a new asyncio task. The MCP SDK's # because they create a new asyncio task, which violates the MCP SDK's
# streamablehttp_client uses anyio task groups / cancel scopes # requirement that its TaskGroup be exited in the exact same task.
# that MUST be exited in the same task they were entered in. # ALSO do NOT use anyio.CancelScope(shield=True) or anyio.fail_after(),
# Using anyio.CancelScope(shield=True) protects from # because they push a new cancel scope onto the task, violating LIFO
# CancelledError while staying in the current task. # order when aclose() attempts to exit the inner TaskGroup.
with anyio.CancelScope(shield=True): # We simply call aclose() directly. If the task is cancelled, the
with anyio.fail_after(5.0): # sockets will eventually be cleaned up by garbage collection.
await exit_stack.aclose() await exit_stack.aclose()
except TimeoutError: except TimeoutError:
log.warning('MCPClient.disconnect() timed out after 5 s') log.warning('MCPClient.disconnect() timed out after 5 s')
except RuntimeError as exc: except RuntimeError as exc:
+1
View File
@@ -22,6 +22,7 @@ aiocache
aiofiles aiofiles
starlette-compress==1.7.0 starlette-compress==1.7.0
Brotli==1.2.0 Brotli==1.2.0
brotlicffi==1.2.0.1
httpx[socks,http2,zstd,cli,brotli]==0.28.1 httpx[socks,http2,zstd,cli,brotli]==0.28.1
starsessions[redis]==2.2.1 starsessions[redis]==2.2.1