mirror of
https://github.com/encounter/adk-python.git
synced 2026-07-09 18:19:28 -07:00
feat: Add Graceful Plugin Shutdown to Runner
This change introduces a shutdown lifecycle hook for plugins. The `PluginManager` now has an `async def shutdown()` method that will call `await plugin.shutdown()` on any registered plugins that implement the method. This is called from `Runner.close()`, allowing plugins to perform cleanup tasks like flushing logs or closing connections when the runner instance is being closed. This improves the reliability of plugins that perform background operations. PiperOrigin-RevId: 831037737
This commit is contained in:
committed by
Copybara-Service
parent
01bac62f0c
commit
249216e890
@@ -513,10 +513,8 @@ class TestBigQueryAgentAnalyticsPlugin:
|
||||
mock_write_client.append_rows.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_shutdown(
|
||||
self, bq_plugin_inst, mock_bq_client, mock_write_client
|
||||
):
|
||||
await bq_plugin_inst.shutdown()
|
||||
async def test_close(self, bq_plugin_inst, mock_bq_client, mock_write_client):
|
||||
await bq_plugin_inst.close()
|
||||
mock_write_client.transport.close.assert_called_once()
|
||||
mock_bq_client.close.assert_called_once()
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from unittest.mock import AsyncMock
|
||||
from unittest.mock import Mock
|
||||
|
||||
from google.adk.models.llm_response import LlmResponse
|
||||
@@ -267,3 +269,51 @@ async def test_all_callbacks_are_supported(
|
||||
"on_model_error_callback",
|
||||
]
|
||||
assert set(plugin1.call_log) == set(expected_callbacks)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_close_calls_plugin_close(
|
||||
service: PluginManager, plugin1: TestPlugin
|
||||
):
|
||||
"""Tests that close calls the close method on registered plugins."""
|
||||
plugin1.close = AsyncMock()
|
||||
service.register_plugin(plugin1)
|
||||
|
||||
await service.close()
|
||||
|
||||
plugin1.close.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_close_raises_runtime_error_on_plugin_exception(
|
||||
service: PluginManager, plugin1: TestPlugin
|
||||
):
|
||||
"""Tests that close raises a RuntimeError if a plugin's close fails."""
|
||||
plugin1.close = AsyncMock(side_effect=ValueError("Shutdown error"))
|
||||
service.register_plugin(plugin1)
|
||||
|
||||
with pytest.raises(
|
||||
RuntimeError, match="Failed to close plugins: 'plugin1': ValueError"
|
||||
):
|
||||
await service.close()
|
||||
|
||||
plugin1.close.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_close_with_timeout(plugin1: TestPlugin):
|
||||
"""Tests that close respects the timeout and raises on failure."""
|
||||
service = PluginManager(close_timeout=0.1)
|
||||
|
||||
async def slow_close():
|
||||
await asyncio.sleep(0.2)
|
||||
|
||||
plugin1.close = slow_close
|
||||
service.register_plugin(plugin1)
|
||||
|
||||
with pytest.raises(RuntimeError) as excinfo:
|
||||
await service.close()
|
||||
|
||||
assert "Failed to close plugins: 'plugin1': TimeoutError" in str(
|
||||
excinfo.value
|
||||
)
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
from pathlib import Path
|
||||
import textwrap
|
||||
from typing import Optional
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from google.adk.agents.base_agent import BaseAgent
|
||||
from google.adk.agents.context_cache_config import ContextCacheConfig
|
||||
@@ -562,6 +563,29 @@ class TestRunnerWithPlugins:
|
||||
|
||||
assert modified_event_message == MockPlugin.ON_EVENT_CALLBACK_MSG
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_runner_close_calls_plugin_close(self):
|
||||
"""Test that runner.close() calls plugin manager close."""
|
||||
# Mock the plugin manager's close method
|
||||
self.runner.plugin_manager.close = AsyncMock()
|
||||
|
||||
await self.runner.close()
|
||||
|
||||
self.runner.plugin_manager.close.assert_awaited_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_runner_passes_plugin_close_timeout(self):
|
||||
"""Test that runner passes plugin_close_timeout to PluginManager."""
|
||||
runner = Runner(
|
||||
app_name="test_app",
|
||||
agent=MockLlmAgent("test_agent"),
|
||||
session_service=self.session_service,
|
||||
artifact_service=self.artifact_service,
|
||||
plugins=[self.plugin],
|
||||
plugin_close_timeout=10.0,
|
||||
)
|
||||
assert runner.plugin_manager._close_timeout == 10.0
|
||||
|
||||
def test_runner_init_raises_error_with_app_and_app_name_and_agent(self):
|
||||
"""Test that ValueError is raised when app, app_name and agent are provided."""
|
||||
with pytest.raises(
|
||||
|
||||
Reference in New Issue
Block a user