From 3a1d459cd4702eda5d4267e03ad0717bd534b0c8 Mon Sep 17 00:00:00 2001 From: Kathy Wu Date: Mon, 16 Feb 2026 18:50:06 -0800 Subject: [PATCH 1/2] fix: Fix pickling lock errors in McpSessionManager When we added the session_lock_map, it resulted in pickling errors during Agent Engine deployment. To fix, we implemented custom getstate and setstate methods to exclude the lock map lock and session lock map from pickling. Closes https://github.com/google/adk-python/issues/4486. Co-authored-by: Kathy Wu PiperOrigin-RevId: 871056554 --- .../adk/tools/mcp_tool/mcp_session_manager.py | 24 ++++++++++++++ .../mcp_tool/test_mcp_session_manager.py | 33 +++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/src/google/adk/tools/mcp_tool/mcp_session_manager.py b/src/google/adk/tools/mcp_tool/mcp_session_manager.py index 0e9b938609..f4339f8678 100644 --- a/src/google/adk/tools/mcp_tool/mcp_session_manager.py +++ b/src/google/adk/tools/mcp_tool/mcp_session_manager.py @@ -500,6 +500,30 @@ async def create_session( ) raise ConnectionError(f'Failed to create MCP session: {e}') from e + def __getstate__(self): + """Custom pickling to exclude non-picklable runtime objects.""" + state = self.__dict__.copy() + # Remove unpicklable entries or those that shouldn't persist across pickle + state['_sessions'] = {} + state['_session_lock_map'] = {} + + # Locks and file-like objects cannot be pickled + state.pop('_lock_map_lock', None) + state.pop('_errlog', None) + + return state + + def __setstate__(self, state): + """Custom unpickling to restore state.""" + self.__dict__.update(state) + # Re-initialize members that were not pickled + self._sessions = {} + self._session_lock_map = {} + self._lock_map_lock = threading.Lock() + # If _errlog was removed during pickling, default to sys.stderr + if not hasattr(self, '_errlog') or self._errlog is None: + self._errlog = sys.stderr + async def close(self): """Closes all sessions and cleans up resources.""" async with self._session_lock: diff --git a/tests/unittests/tools/mcp_tool/test_mcp_session_manager.py b/tests/unittests/tools/mcp_tool/test_mcp_session_manager.py index ba9a7e801c..327df114a8 100644 --- a/tests/unittests/tools/mcp_tool/test_mcp_session_manager.py +++ b/tests/unittests/tools/mcp_tool/test_mcp_session_manager.py @@ -607,6 +607,39 @@ async def test_close_skips_aclose_for_different_loop_sessions(self): exit_stack2.aclose.assert_not_called() assert len(manager._sessions) == 0 + @pytest.mark.asyncio + async def test_pickle_mcp_session_manager(self): + """Verify that MCPSessionManager can be pickled and unpickled.""" + import pickle + + manager = MCPSessionManager(self.mock_stdio_connection_params) + + # Access the lock to ensure it's initialized + lock = manager._session_lock + assert isinstance(lock, asyncio.Lock) + + # Add a mock session to verify it's cleared on pickling + manager._sessions["test"] = (Mock(), Mock(), asyncio.get_running_loop()) + + # Pickle and unpickle + pickled = pickle.dumps(manager) + unpickled = pickle.loads(pickled) + + # Verify basics are restored + assert unpickled._connection_params == manager._connection_params + + # Verify transient/unpicklable members are re-initialized or cleared + assert unpickled._sessions == {} + assert unpickled._session_lock_map == {} + assert isinstance(unpickled._lock_map_lock, type(manager._lock_map_lock)) + assert unpickled._lock_map_lock is not manager._lock_map_lock + assert unpickled._errlog == sys.stderr + + # Verify we can still get a lock in the new instance + new_lock = unpickled._session_lock + assert isinstance(new_lock, asyncio.Lock) + assert new_lock is not lock + @pytest.mark.asyncio async def test_retry_on_errors_decorator(): From 7a6b62da62f56e327a7782b123129247ba1e68f7 Mon Sep 17 00:00:00 2001 From: Sean Zhou Date: Wed, 18 Feb 2026 13:26:32 -0800 Subject: [PATCH 2/2] chore(version): Bump version and update changelog for 1.25.1 --- .github/.release-please-manifest.json | 2 +- CHANGELOG.md | 6 ++++++ src/google/adk/version.py | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/.release-please-manifest.json b/.github/.release-please-manifest.json index d0f8c0ee4d..661ffa458c 100644 --- a/.github/.release-please-manifest.json +++ b/.github/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.24.1" + ".": "1.25.1" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 1efb2b2412..c486780192 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [1.25.1](https://github.com/google/adk-python/compare/v1.25.0...v1.25.1) (2026-02-18) + +### Bug Fixes + +* Fix pickling lock errors in McpSessionManager ([4e2d615](https://github.com/google/adk-python/commit/4e2d6159ae3552954aaae295fef3e09118502898)) + ## [1.25.0](https://github.com/google/adk-python/compare/v1.24.1...v1.25.0) (2026-02-11) ### Features diff --git a/src/google/adk/version.py b/src/google/adk/version.py index 8002d61987..1ce0bf5e6c 100644 --- a/src/google/adk/version.py +++ b/src/google/adk/version.py @@ -13,4 +13,4 @@ # limitations under the License. # version: major.minor.patch -__version__ = "1.25.0" +__version__ = "1.25.1"