From 8ea2551ad5c417895cd9e4817c5f2c5cfc7d79e0 Mon Sep 17 00:00:00 2001 From: Lin Junrong Date: Sat, 1 Aug 2026 00:24:53 +0800 Subject: [PATCH] fix: don't call create_task in ClientSession.__del__ without a loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit __del__ runs during garbage collection, which can happen with no running event loop — a sync context, or interpreter shutdown. asyncio.create_task() raises RuntimeError there, and exceptions raised in __del__ are swallowed and printed rather than propagated, so the session was silently never closed and the connection leaked. It also emits a "coroutine was never awaited" warning. >>> s = ClientSession(); del s # no running loop RuntimeError: no running event loop Now checks for a running loop first and falls back to the ResourceWarning aiohttp itself emits in this exact situation, matching upstream behaviour. When there is a loop, the close task is also retained: the loop only keeps weak references, so a discarded close() can be collected before it finishes. --- .../core/openbb_core/provider/utils/client.py | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/openbb_platform/core/openbb_core/provider/utils/client.py b/openbb_platform/core/openbb_core/provider/utils/client.py index bc874c1c4383..d593da3cf5ff 100644 --- a/openbb_platform/core/openbb_core/provider/utils/client.py +++ b/openbb_platform/core/openbb_core/provider/utils/client.py @@ -38,6 +38,11 @@ def get_user_agent() -> str: return random.choice(user_agent_strings) # nosec # noqa: S311 +# Session-close tasks spawned from __del__. The event loop only keeps weak +# references to tasks, so they are held here until they complete. +_BACKGROUND_TASKS: set[asyncio.Task] = set() + + class ClientResponse(aiohttp.ClientResponse): """Client response class.""" @@ -81,8 +86,29 @@ def __init__(self, *args, **kwargs): # pylint: disable=unused-argument def __del__(self, _warnings: Any = warnings) -> None: """Close the session.""" - if not self.closed: - asyncio.create_task(self.close()) + if self.closed: + return + + # __del__ runs during garbage collection, which may happen with no + # running loop at all (sync context, interpreter shutdown). Calling + # create_task() there raises RuntimeError, and exceptions in __del__ + # are swallowed and printed — so the session would silently never + # close. Fall back to the ResourceWarning aiohttp itself emits. + try: + loop = asyncio.get_running_loop() + except RuntimeError: + _warnings.warn( + f"Unclosed client session {self!r}", + ResourceWarning, + source=self, + ) + return + + # The loop only keeps a weak reference to a task, so a discarded close() + # can be collected before it finishes. Hold it until it completes. + task = loop.create_task(self.close()) + _BACKGROUND_TASKS.add(task) + task.add_done_callback(_BACKGROUND_TASKS.discard) async def get(self, url: str, **kwargs) -> ClientResponse: # type: ignore """Send GET request."""