|
41 | 41 | from .oxy.base_flow import BaseFlow |
42 | 42 | from .oxy.base_tool import BaseTool |
43 | 43 | from .oxy.llms.base_llm import BaseLLM |
44 | | -from .oxy.mcp_tools.base_mcp_client import BaseMCPClient |
45 | 44 | from .routes import router |
46 | 45 | from .schemas import OxyRequest, OxyResponse, SSEMessage, WebResponse |
47 | 46 | from .utils.common_utils import ( |
@@ -213,17 +212,41 @@ async def __aenter__(self) -> "MAS": |
213 | 212 | return self |
214 | 213 |
|
215 | 214 | async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: |
| 215 | + async def _safe(coro, label: str) -> None: |
| 216 | + try: |
| 217 | + await coro |
| 218 | + logger.info(f"✓ {label} shutdown completed.") |
| 219 | + except Exception as e: |
| 220 | + logger.warning(f"Error during shutdown ({label}): {e}", exc_info=True) |
| 221 | + |
| 222 | + logger.info("=" * 64) |
| 223 | + logger.info("🔒 OxyGent MAS Application Shutdown Initiated") |
| 224 | + logger.info("=" * 64) |
| 225 | + |
| 226 | + # 1. Wait for background tasks |
216 | 227 | all_tasks = [t for tasks in self.background_tasks.values() for t in tasks] |
217 | 228 | if all_tasks: |
218 | | - await asyncio.gather(*all_tasks) |
| 229 | + await _safe( |
| 230 | + asyncio.gather(*all_tasks, return_exceptions=True), |
| 231 | + "background tasks", |
| 232 | + ) |
| 233 | + |
| 234 | + # 2. Tear down resources — each step runs regardless of prior failures |
| 235 | + from .live_prompt import close_prompt_manager |
| 236 | + |
| 237 | + await _safe(close_prompt_manager(), "prompt manager") |
| 238 | + |
| 239 | + if self.es_client: |
| 240 | + await _safe(self.es_client.close(), "ES client") |
| 241 | + |
| 242 | + if self.redis_client: |
| 243 | + await _safe(self.redis_client.close(), "Redis client") |
| 244 | + |
| 245 | + await _safe(self.cleanup_all(), "oxy cleanup") |
| 246 | + |
219 | 247 | logger.info("=" * 64) |
220 | 248 | logger.info("🪂 OxyGent MAS Application Exit") |
221 | 249 | logger.info("=" * 64) |
222 | | - if self.es_client: |
223 | | - await self.es_client.close() |
224 | | - if self.redis_client: |
225 | | - await self.redis_client.close() |
226 | | - await self.cleanup_servers() |
227 | 250 |
|
228 | 251 | def add_background_task(self, trace_id: str, task: asyncio.Task) -> None: |
229 | 252 | """Register a background task under the given trace_id.""" |
@@ -341,24 +364,21 @@ async def init(self) -> None: |
341 | 364 | logger.warning(f"Failed to setup dynamic agents: {e}", exc_info=True) |
342 | 365 | logger.info("=" * 64) |
343 | 366 |
|
344 | | - async def cleanup_servers(self) -> None: |
345 | | - """Gracefully shut down remote servers/clients. |
| 367 | + async def cleanup_all(self) -> None: |
| 368 | + """Gracefully release resources held by all registered Oxy components. |
346 | 369 |
|
347 | | - The method concurrently calls ``cleanup()`` on every |
348 | | - :class:`BaseMCPClient` that has been registered. It is automatically |
349 | | - invoked by :func:`__aexit__`. |
| 370 | + Calls ``cleanup()`` on every registered :class:`Oxy` instance. |
| 371 | + Each cleanup is invoked individually so that a failure in one |
| 372 | + component does not prevent others from releasing their resources. |
350 | 373 | """ |
351 | | - cleanup_tasks = [] |
352 | | - for oxy in self.oxy_name_to_oxy.values(): |
353 | | - if not isinstance(oxy, BaseMCPClient): |
354 | | - continue |
355 | | - cleanup_tasks.append(asyncio.create_task(oxy.cleanup())) |
356 | | - |
357 | | - if cleanup_tasks: |
| 374 | + for oxy_name, oxy in self.oxy_name_to_oxy.items(): |
358 | 375 | try: |
359 | | - await asyncio.gather(*cleanup_tasks, return_exceptions=False) |
| 376 | + await oxy.cleanup() |
360 | 377 | except Exception as e: |
361 | | - logger.warning(f"Warning during final cleanup: {e}", exc_info=True) |
| 378 | + logger.warning( |
| 379 | + f"Error during cleanup of oxy '{oxy_name}': {e}", |
| 380 | + exc_info=True, |
| 381 | + ) |
362 | 382 |
|
363 | 383 | async def init_db(self) -> None: |
364 | 384 | """Es --- (table_name: key) |
@@ -635,21 +655,59 @@ async def batch_init_oxy(self, *class_type: type) -> None: |
635 | 655 | class_types: List of class types to initialize (e.g., BaseLLM, BaseTool, BaseAgent). |
636 | 656 |
|
637 | 657 | NOTE: |
638 | | - Initialize all oxy objects of the specified class types, |
| 658 | + Initialize all oxy objects of the specified class types. |
| 659 | + If any init fails, already-initialized oxy objects of the same |
| 660 | + batch are cleaned up before the exception propagates. |
639 | 661 | """ |
640 | | - tasks = [] |
641 | | - for oxy_name in list(self.oxy_name_to_oxy.keys()): |
642 | | - oxy = self.oxy_name_to_oxy[oxy_name] |
643 | | - if not isinstance(oxy, class_type): |
644 | | - continue |
| 662 | + targets = [ |
| 663 | + (name, oxy) |
| 664 | + for name, oxy in self.oxy_name_to_oxy.items() |
| 665 | + if isinstance(oxy, class_type) |
| 666 | + ] |
| 667 | + for _name, oxy in targets: |
645 | 668 | oxy.set_mas(self) |
646 | | - task = oxy.init() |
647 | | - if Config.get_tool_is_concurrent_init(): |
648 | | - tasks.append(task) |
649 | | - else: |
650 | | - await task |
651 | | - if tasks: |
652 | | - await asyncio.gather(*tasks) |
| 669 | + |
| 670 | + initialized_names: list[str] = [] |
| 671 | + if Config.get_tool_is_concurrent_init(): |
| 672 | + results = await asyncio.gather( |
| 673 | + *(oxy.init() for _name, oxy in targets), |
| 674 | + return_exceptions=True, |
| 675 | + ) |
| 676 | + first_error = None |
| 677 | + for (name, _oxy), result in zip(targets, results): |
| 678 | + if isinstance(result, Exception): |
| 679 | + logger.error( |
| 680 | + f"Failed to initialize oxy '{name}': {result}", exc_info=result |
| 681 | + ) |
| 682 | + if first_error is None: |
| 683 | + first_error = result |
| 684 | + else: |
| 685 | + initialized_names.append(name) |
| 686 | + if first_error is not None: |
| 687 | + for name in initialized_names: |
| 688 | + try: |
| 689 | + await self.oxy_name_to_oxy[name].cleanup() |
| 690 | + except Exception as ce: |
| 691 | + logger.warning( |
| 692 | + f"Error during rollback cleanup of oxy '{name}': {ce}", |
| 693 | + exc_info=True, |
| 694 | + ) |
| 695 | + raise first_error |
| 696 | + else: |
| 697 | + try: |
| 698 | + for name, oxy in targets: |
| 699 | + await oxy.init() |
| 700 | + initialized_names.append(name) |
| 701 | + except Exception: |
| 702 | + for name in initialized_names: |
| 703 | + try: |
| 704 | + await self.oxy_name_to_oxy[name].cleanup() |
| 705 | + except Exception as ce: |
| 706 | + logger.warning( |
| 707 | + f"Error during rollback cleanup of oxy '{name}': {ce}", |
| 708 | + exc_info=True, |
| 709 | + ) |
| 710 | + raise |
653 | 711 |
|
654 | 712 | async def init_all_oxy(self) -> None: |
655 | 713 | """Initializing all tools and agents assign values of agent.tools to each |
|
0 commit comments