Skip to content

Commit 837ef2d

Browse files
committed
fix: address Copilot AI review - timeouts and CancelledError handling
Changes based on Copilot AI review (3 issues): 1. Increased thread join timeout (Issue #1): - Changed from 2 to 5 seconds - Made proportional to cleanup timeout - Defined as class constant _THREAD_JOIN_TIMEOUT 2. Handle asyncio.CancelledError explicitly (Issue #2): - Added separate except clause for CancelledError - Logs specific warning for cancelled cleanup - Re-raises CancelledError as per asyncio best practices - Added for both session and stdio_context cleanup 3. Increased cleanup timeout to match connection timeout (Issue #3): - Changed from 5 to 10 seconds (matches _connect_stdio timeout) - Defined as class constant _CLEANUP_TIMEOUT - Prevents incomplete cleanup with slow MCP servers
1 parent a39afbe commit 837ef2d

File tree

1 file changed

+17
-5
lines changed

1 file changed

+17
-5
lines changed

core/framework/runner/mcp_client.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,10 @@ def _call_tool_http(self, tool_name: str, arguments: dict[str, Any]) -> Any:
389389
except Exception as e:
390390
raise RuntimeError(f"Failed to call tool via HTTP: {e}")
391391

392+
# Cleanup timeout should match or exceed connection timeout (10 seconds in _connect_stdio)
393+
_CLEANUP_TIMEOUT = 10 # seconds
394+
_THREAD_JOIN_TIMEOUT = 5 # seconds - should be proportional to cleanup timeout
395+
392396
async def _cleanup_stdio_async(self) -> None:
393397
"""Async cleanup for STDIO session and context managers.
394398
@@ -404,6 +408,10 @@ async def _cleanup_stdio_async(self) -> None:
404408
try:
405409
if self._session:
406410
await self._session.__aexit__(None, None, None)
411+
except asyncio.CancelledError:
412+
# Coroutine was cancelled (e.g., during event loop teardown)
413+
logger.warning("MCP session cleanup was cancelled")
414+
raise # Re-raise CancelledError as per asyncio best practices
407415
except Exception as e:
408416
logger.warning(f"Error closing MCP session: {e}")
409417
finally:
@@ -413,6 +421,10 @@ async def _cleanup_stdio_async(self) -> None:
413421
try:
414422
if self._stdio_context:
415423
await self._stdio_context.__aexit__(None, None, None)
424+
except asyncio.CancelledError:
425+
# Coroutine was cancelled (e.g., during event loop teardown)
426+
logger.warning("STDIO context cleanup was cancelled")
427+
raise # Re-raise CancelledError as per asyncio best practices
416428
except Exception as e:
417429
logger.warning(f"Error closing STDIO context: {e}")
418430
finally:
@@ -434,11 +446,11 @@ def disconnect(self) -> None:
434446
self._cleanup_stdio_async(),
435447
self._loop
436448
)
437-
cleanup_future.result(timeout=5) # Wait for cleanup with timeout
449+
cleanup_future.result(timeout=self._CLEANUP_TIMEOUT)
438450
cleanup_attempted = True
439451
except TimeoutError:
440-
# Cleanup took too long - may indicate stuck resources
441-
logger.warning("Async cleanup timed out after 5 seconds")
452+
# Cleanup took too long - may indicate stuck resources or slow MCP server
453+
logger.warning(f"Async cleanup timed out after {self._CLEANUP_TIMEOUT} seconds")
442454
except Exception as e:
443455
# This can happen if loop stopped between is_running() check and
444456
# run_coroutine_threadsafe(), or if cleanup itself failed
@@ -462,9 +474,9 @@ def disconnect(self) -> None:
462474
"skipping async cleanup. Resources may not be fully released."
463475
)
464476

465-
# Wait for thread to finish
477+
# Wait for thread to finish (timeout proportional to cleanup timeout)
466478
if self._loop_thread and self._loop_thread.is_alive():
467-
self._loop_thread.join(timeout=2)
479+
self._loop_thread.join(timeout=self._THREAD_JOIN_TIMEOUT)
468480

469481
# Clear remaining references
470482
# Note: _session and _stdio_context are cleared in _cleanup_stdio_async()

0 commit comments

Comments
 (0)