fix(mcp): catch RuntimeError when cancelling tasks on a closed event loop - #60032
fix(mcp): catch RuntimeError when cancelling tasks on a closed event loop#60032B416-JAFLY wants to merge 1 commit into
Conversation
…loop When the MCP event loop is closed during Hermes shutdown, the finally blocks in _wait_for_lifecycle_event and _wait_for_reconnect_or_shutdown call t.cancel() which schedules a callback via call_soon(). If the loop has already been closed by _stop_mcp_loop(), this raises: RuntimeError: Event loop is closed The exception propagates up through the parked coroutine and surfaces as an 'Exception ignored in' warning during Python GC at exit. While the program shuts down correctly, the traceback is noisy and confusing. Catch RuntimeError around t.cancel() since there is nothing to clean up when the loop is already closed.
There was a problem hiding this comment.
Pull request overview
This PR reduces shutdown noise from the MCP background loop by preventing RuntimeError: Event loop is closed from surfacing during cleanup when cancelling pending asyncio tasks, particularly in the “parked” reconnect-wait paths.
Changes:
- Wrap pending-task
t.cancel()calls in_wait_for_lifecycle_event()and_wait_for_reconnect_or_shutdown()withtry/except RuntimeErrorto avoid shutdown-time tracebacks. - Add inline comments clarifying the “loop already closed” shutdown race.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| try: | ||
| t.cancel() | ||
| except RuntimeError: | ||
| pass # Event loop already closed — nothing to clean up |
| try: | ||
| t.cancel() | ||
| except RuntimeError: | ||
| pass # Event loop already closed — nothing to clean up |
|
撤回,等彻底解决后再提 |
|
Thanks for isolating the cancellation site. Current main still has the unguarded cancellation in the two targeted waiters, so the report is grounded. Problems
Suggested changes
Automated hermes-sweeper review. |
|
Hi! The triage bot flagged that our PRs overlap — this is the closed-loop cancellation family (#60032, #60104, #60380, #80955). My PR #80955 is a superset of this one: it covers your two call sites ( Proposal: consolidate into #80955 so there's a single fix for the whole bug class. Would you be open to closing this PR in favor of it (or pointing me at anything in your change I should absorb)? |
Problem
When Hermes exits and an MCP server is in the parked state (initial connection failed), the
_wait_for_reconnect_or_shutdownand_wait_for_lifecycle_eventmethods' finally blocks callt.cancel()on pending asyncio tasks. A race inshutdown_mcp_servers()can cause the event loop to be closed by_stop_mcp_loop()before the finally block finishes executing:shutdown_mcp_servers()schedules_shutdown()on the MCP loop, with a 15-second timeout_shutdown(), all servers are shut down in parallel viaasyncio.gatherfuture.result(timeout=15)times out_stop_mcp_loop()closes the event loop while the server's finally block is mid-executiont.cancel()→call_soon()→_check_closed()raisesRuntimeError: Event loop is closedThis surfaces as an
Exception ignored inwarning during Python GC at exit:The program shuts down correctly; the traceback is just noisy.
Fix
Wrap
t.cancel()intry/except RuntimeErrorin both_wait_for_lifecycle_eventand_wait_for_reconnect_or_shutdown. When the loop is already closed there is nothing to clean up, so silently passing is correct. The subsequentawait tis already guarded byexcept (CancelledError, Exception)which catches RuntimeError as well.Testing
Verified on macOS 15.7 with Python 3.11.15 — the
RuntimeErrortraceback no longer appears on exit.