Skip to content

fix: don't call create_task in ClientSession.__del__ without a running loop - #7621

Open
noron12234 wants to merge 1 commit into
OpenBB-finance:developfrom
noron12234:fix/client-session-del
Open

fix: don't call create_task in ClientSession.__del__ without a running loop#7621
noron12234 wants to merge 1 commit into
OpenBB-finance:developfrom
noron12234:fix/client-session-del

Conversation

@noron12234

Copy link
Copy Markdown

The problem

ClientSession.__del__ closes the session by scheduling a task:

def __del__(self, _warnings: Any = warnings) -> None:
    """Close the session."""
    if not self.closed:
        asyncio.create_task(self.close())

__del__ runs during garbage collection, and that can happen with no running event loop — a sync context, or interpreter shutdown. asyncio.create_task() raises RuntimeError there:

>>> s = ClientSession(); del s        # no running loop
RuntimeError: no running event loop
sys:1: RuntimeWarning: coroutine 'ClientSession.close' was never awaited

Exceptions raised inside __del__ are swallowed by Python and printed to stderr rather than propagated, so this does not crash anything — the session simply never closes and the connection leaks, with a stray warning as the only sign.

There is a second, smaller issue on the path that does work: when a loop is running, the task is discarded, and the loop only keeps a weak reference, so the close can be collected before it finishes.

What aiohttp itself does

Worth noting because this class subclasses aiohttp.ClientSession — upstream's __del__ never schedules anything (aiohttp/client.py:427):

def __del__(self, _warnings: Any = warnings) -> None:
    if not self.closed:
        _warnings.warn(f"Unclosed client session {self!r}", ResourceWarning, source=self)
        context = {"client_session": self, "message": "Unclosed client session"}
        ...
        self._loop.call_exception_handler(context)

It warns and hands off to the exception handler, precisely because __del__ cannot rely on a loop being available. The override here departs from that.

The change

Check for a running loop first; fall back to the same ResourceWarning upstream emits. When a loop is available, keep the close task referenced until it completes.

try:
    loop = asyncio.get_running_loop()
except RuntimeError:
    _warnings.warn(f"Unclosed client session {self!r}", ResourceWarning, source=self)
    return

task = loop.create_task(self.close())
_BACKGROUND_TASKS.add(task)
task.add_done_callback(_BACKGROUND_TASKS.discard)

Verification

Both paths, with the patched __del__:

before after
no running loop RuntimeError: no running event loop + coroutine … was never awaited ResourceWarning: Unclosed client session — same as aiohttp
running loop task discarded, collectable mid-close task held until done
$ ruff check openbb_platform/core/openbb_core/provider/utils/client.py    # ruff 0.12.12, pinned in .pre-commit-config.yaml
All checks passed!
$ black --check openbb_platform/core/openbb_core/provider/utils/client.py # black 25.1.0
1 file would be left unchanged.

No test added: the no-loop path is straightforward to test, but the loop path is a garbage-collection race and a test for it would be flaky by construction. Happy to add a test for the RuntimeError case alone if you'd like one.

Found with an AST scan for create_task / ensure_future results discarded as bare expression statements. This was the only hit under openbb_platform/.

__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.
@CLAassistant

CLAassistant commented Jul 31, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants