ccxt icon indicating copy to clipboard operation
ccxt copied to clipboard

Unclosed aiohttp sessions after exchange.close() when using maxRetriesOnFailure with concurrent async execution

Open embogomolov opened this issue 1 month ago • 1 comments

Operating System

macOS / Linux (tested on both)

Programming Language

Python

CCXT Version

4.5.22

Description

When using maxRetriesOnFailure (tested on Binance, but likely affects all other exchanges as well) option and running multiple exchange instances concurrently, some aiohttp sessions are not properly closed by exchange.close(), resulting in "Unclosed client session" warnings.

The issue does NOT occur when maxRetriesOnFailure is not set.

Steps to Reproduce

import asyncio
import ccxt.async_support as ccxt

async def create_and_close():
    exchange = ccxt.binance({
        'apiKey': 'some_invalid_key',
        'secret': 'some_invalid_secret',
        'options': {
            'defaultType': 'future',
            'maxRetriesOnFailure': 3,
        },
    })
    try:
        await exchange.load_markets()
    except Exception:
        pass
    await exchange.close()

async def test():
    # Run 4 concurrent exchanges - some will leak sessions
    await asyncio.gather(*[create_and_close() for _ in range(4)])

asyncio.run(test())

Expected Behavior

All aiohttp sessions should be properly closed after calling await exchange.close(). No warnings should appear.

Actual Behavior

2 out of 4 exchanges leave unclosed sessions:

binance requires to release all resources with an explicit call to the .close() coroutine...
Unclosed client session
client_session: <aiohttp.client.ClientSession object at 0x11a0da5d0>
Unclosed connector
connections: ['deque([...])']
connector: <aiohttp.connector.TCPConnector object at 0x11a0dbe00>

Additional Observations

  1. Without maxRetriesOnFailure — no warnings, all sessions closed properly
  2. With maxRetriesOnFailure — warnings appear even though close() is called
  3. The session address in warnings differs from the session that was closed (verified via debug prints)
  4. Problem is likely related to asyncio.gather inside load_markets() / fetch_currencies() combined with retry logic

embogomolov avatar Nov 30 '25 10:11 embogomolov

@embogomolov thanks for reporting it, we will take a look

@ttodua can you please check it?

carlosmiei avatar Nov 30 '25 10:11 carlosmiei

I was able to reproduce this (even without 4 loop, but just one loop does that), will try to push a fix soon

ttodua avatar Dec 11 '25 07:12 ttodua