-
Notifications
You must be signed in to change notification settings - Fork 2.5k
Description
Describe the bug
When running ADK agents in production with google-genai>=1.50.1, I'm seeing recurring asyncio errors about tasks being destroyed while pending during cleanup. The error happens when the genai AsyncClient.aclose() method is called during garbage collection.
This occurs because the Gemini class uses @cached_property for api_client and _live_api_client, but these clients are never explicitly closed in Runner.close(). Starting from genai 1.50.1, custom httpx clients won't be closed automatically, so when the Gemini instance is garbage collected, the aclose() task starts but gets destroyed before completion.
To Reproduce
Steps to reproduce the behavior:
- Install
google-adk==1.18.0andgoogle-genai==1.50.1 - Run the following minimal code:
"""
Minimal reproduction for ADK Issue #3550
Bug: AsyncClient.__del__ creates orphaned asyncio tasks when Runner.close()
is not called before event loop shutdown.
Setup environment variables:
export GOOGLE_GENAI_USE_VERTEXAI=true
export GOOGLE_CLOUD_PROJECT="your-project-id"
export GOOGLE_CLOUD_LOCATION="your-location"
"""
import asyncio
import gc
from google.adk.agents import LlmAgent
from google.adk.models.google_llm import Gemini
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.genai import types
async def test():
agent = LlmAgent(
model=Gemini(model="gemini-2.0-flash-exp"),
name="test"
)
runner = Runner(
app_name="test",
agent=agent,
session_service=InMemorySessionService()
)
session = await runner.session_service.create_session(
app_name="test",
user_id="user1"
)
content = types.Content(
role='user',
parts=[types.Part(text='hello')]
)
async for event in runner.run_async(
user_id="user1",
session_id=session.id,
new_message=content
):
pass
# Bug: NOT calling await runner.close()
del runner
gc.collect()
if __name__ == '__main__':
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.set_debug(True)
loop.run_until_complete(test())
pending = asyncio.all_tasks(loop)
print(f"\nOrphaned tasks: {len(pending)}")
loop.close()- Run the script
- Error stacktrace:
Orphaned tasks: 2
Task was destroyed but it is pending!
source_traceback: Object created at (most recent call last):
Expected behavior
Runner.close() should clean up all resources including LLM client connections, and no asyncio warnings should appear during cleanup.
Screenshots
N/A
Desktop (please complete the following information):
- OS: Linux (production), macOS (local)
- Python version: Python 3.11
- ADK version: 1.18.0
Model Information:
- Are you using LiteLLM: No
- Which model is being used: gemini-2.5-flash
Additional context
The issue is in runners.py:1298-1300 where Runner.close() only cleans up toolsets:
async def close(self):
"""Closes the runner."""
await self._cleanup_toolsets(self._collect_toolset(self.agent))
# Missing: LLM client cleanupRoot cause in google/adk/models/google_llm.py:
- Line 196-208:
api_clientcached property is never closed - Line 240-246:
_live_api_clientcached property is never closed
Related to #1112 and #2804 but specifically affects LLM clients rather than MCP toolsets.
Workaround attempts that didn't work:
- Upgrading genai 1.49.0 → 1.50.1
- Adding shutdown cleanup in FastAPI (only fixes app shutdown, not per-request cleanup)
- Log filtering (masks the symptom)
Proposed fix following the existing _cleanup_toolsets pattern:
- Add
BaseLlm.aclose()method (default no-op for backward compatibility) - Implement
Gemini.aclose()to close cached clients - Add
Runner._collect_llm_models()to gather LLM instances - Add
Runner._cleanup_llm_models()to close them - Update
Runner.close()to cleanup both toolsets and LLM clients