fix(mcp): serialize keepalive with active RPCs - #62811
Conversation
|
Thanks for the focused fix. Current main still sends keepalive No blocking problems identified in the reviewed diff. This is an automated hermes-sweeper review. |
|
Operational note: while this PR is pending, I applied a per-server workaround in the affected deployment: mcp_servers:
fable-advisor:
keepalive_interval: 86400and reloaded the MCP servers. This only makes scheduled probes rare for that server. It is not a correctness fix: a probe can still coincide with a long call, and it weakens idle liveness detection. The PR remains the durable fix because it enforces the no-overlap invariant while preserving idle keepalives for all MCP servers. |
|
Checking in after ~12 days. Still mergeable against today's main (d9165d7), and the gap is still live after the recent recycle/backoff work: |
What does this PR do?
Prevents MCP keepalive probes from overlapping an active RPC on the same
ClientSession/JSON-RPC stream.When
_rpc_lockis already held, the keepalive cycle now returns immediately because active RPC traffic already proves liveness. Otherwise, the probe acquires the same per-server lock used bycall_tool()and the other client-initiated RPCs before sendingpingor itslist_toolsfallback. This closes the check/acquire race without queueing the common keepalive path behind a long-running tool call.The bounded trade-off is the opposite ordering: if keepalive acquires the lock first against a dead server, a newly arriving user RPC waits behind the probe's existing 30-second timeout before its own timeout starts. The regression suite covers that serialization order explicitly. When a user RPC owns the lock and is itself hung, its own timeout remains the failure detector while the redundant periodic probe stays silent.
The bug was reproduced with a long-running stdio MCP call: a concurrent ping completed while the original
tools/callremained wedged until the outer timeout. A fresh-process live canary after the fix completed the real call in 10.546 seconds, skipped keepalive during the active call, and completed a post-call keepalive.Relationship to #48069
#48069 identified the same in-flight keepalive race and proposed a broader fix that also tracks and cancels orphaned calls during reconnect. Credit to @arminanton for documenting that failure mode first.
That branch is currently 2,923 commits behind
mainand has a content conflict intools/mcp_tool.py. This PR is a narrow current-mainsalvage of only the serialization invariant, with a direct regression in the existing capability-gating suite. It intentionally does not duplicate the broader reconnect/orphan-cancellation behavior from #48069.Related Issue
Related to #48069 and #30268.
Type of Change
Changes Made
tools/mcp_tool.py_rpc_lockalready indicates active traffic;pingand thelist_toolsfallback with_rpc_lock;tests/tools/test_mcp_capability_gating.pypingnorlist_toolswhile another RPC owns the lock;How to Test
Focused regression:
Targeted MCP suites:
Static checks:
Observed locally on macOS 26.5.2 / Apple Silicon:
py_compile: passed;status=ok, 10.546 s, keepalive skipped during the call, post-call keepalive completed.I also attempted the repository-wide
scripts/run_tests.shin the managed local environment. It did not produce a clean baseline: 39 failures were reported across 17 unrelated test files, 10 files did not run, andtests/tools/test_browser_homebrew_paths.pyhit the wrapper's 300-second per-file timeout. None of the failures were in the three targeted MCP suites above, so this PR does not claim a clean local full-suite run; fork CI is the authoritative full check.Checklist
Code
pytest tests/ -qsuite locally (repository-wide wrapper was attempted but had unrelated local failures/timeouts; targeted 265-test MCP suite passed; full CI is expected to run on the PR)Documentation & Housekeeping
cli-config.yaml.example: N/ACONTRIBUTING.md/AGENTS.md: N/Aasyncio.Locksemantics and passes the Windows-footgun scanner