fix(contrib): pool and idle-evict MCP connections in google_adk_agents#1664
Open
wankhede04 wants to merge 2 commits into
Open
fix(contrib): pool and idle-evict MCP connections in google_adk_agents#1664wankhede04 wants to merge 2 commits into
wankhede04 wants to merge 2 commits into
Conversation
TemporalMcpToolSetProvider's list-tools and call-tool activities called self._toolset_factory(...) on every invocation, constructing a brand-new McpToolset (and therefore a new MCPSessionManager/subprocess for stdio servers) each time with no cleanup. This leaked a spawned process per activity execution under sustained load. Pool one McpToolset per activity name, reused across calls and refcounted so idle eviction (default 5 minutes, overridable via the new mcp_connection_idle_timeout constructor parameter) only fires once no calls are in flight. A failed get_tools()/run_async() call evicts the connection so the next call reconnects instead of reusing a dead session. This brings google_adk_agents to parity with the pooling already shipped in the strands and google_genai contribs. Closes temporalio#1663
…viction in google_adk_agents Regression tests against a fake McpToolset asserting: N sequential call_tool executions against the same name reuse one toolset instead of creating N; list-tools and call-tool share a connection; idle connections close after the configured timeout but not while a call is in flight; and a failed call evicts the broken connection so the next call reconnects.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What was changed
TemporalMcpToolSetProvider's{name}-list-toolsand{name}-call-toolactivities intemporalio/contrib/google_adk_agents/_mcp.pynow pool and reuse a singleMcpToolsetconnection per activity name, instead of constructing a brand-new one on every single invocation._ConnectionRecord+ a module-level connection pool keyed by activity name.get_connection()reuses an existing live connection (refcounted) or lazily opens a new one via the existingtoolset_factory.mcp_connection_idle_timeoutparameter onTemporalMcpToolSetProvider.get_tools()/run_async()raises, the connection is evicted immediately so the next call gets a fresh one. A "no matching tool" business-logic error does not evict a healthy connection.tests/contrib/google_adk_agents/test_mcp_pool.pycovering connection reuse, list-tools/call-tool sharing a connection, idle eviction, in-flight calls blocking eviction, and error eviction.Why?
Every activity invocation called
self._toolset_factory(args.factory_argument), constructing a new ADKMcpToolset(and therefore a newMCPSessionManager) with no.close()/cleanup ever called on it. For stdio-transport MCP servers this spawns a new child process on every single activity execution and never cleans it up — a genuine subprocess leak under sustained load, not just a missed optimization.This brings
google_adk_agentsto parity with the pooling already implemented in thestrands(_temporal_mcp_client.py) andgoogle_genai(_mcp.py) contribs, both of which pool and idle-evict connections the same way.How tested
tests/contrib/google_adk_agents/test_mcp_pool.pyexercising the pool directly against a fakeMcpToolset(no real MCP server needed): connection reuse across N calls, list-tools/call-tool sharing one connection, idle eviction after timeout, in-flight calls blocking premature eviction, and eviction-then-reconnect on bothget_tools()andrun_async()failures.ruff check/ruff format --checkandmypy --namespace-packages --check-untyped-defsagainst the changed file — clean.tests/contrib/google_adk_agents/test_google_adk_agents.pyto confirm no regressions.Risks
factory_argumentis only consulted the first time a connection opens for a given activity name; a warm connection is reused regardless of later calls'factory_argumentvalues. This matches howstrands/google_genaipool a single connection per name (they have no per-callfactory_argumentconcept at all), but is a behavior change if any caller relied onfactory_argumentselecting a different backend on every call for the same provider name.strands/google_genaiimplementations directly, neither actually wires one in either (despite that being suggested in the tracking issue); idle + error eviction is the actual existing pattern, which this PR now matches.Closes #1663