[python,aiohttp] Don't create persistent aiohttp.ClientSession in __init__ (#20292)

aiohttp's `ClientSession` & `TCPConnector` used to obtain an event loop in
__init__ (via `asyncio.get_event_loop`). However, as of https://github.com/aio-libs/aiohttp/pull/8512 both
classes now obtain the running event loop and won't potentially create one. This
makes it impossible to create `ClientSession` and `TCPConnector` objects outside
of coroutines, as `get_running_loop` must be called from a coroutine.

Thus we defer the creation of a `ClientSession` into the actual request and
cache it for later usage. Thereby we pay only a very small price on the first
request, but subsequent requests will not be any more expensive.
This commit is contained in:
Dan Čermák 2024-12-15 10:11:35 +01:00 committed by GitHub
parent d87a70dd93
commit cdfab4eee3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 62 additions and 76 deletions

View File

@ -44,51 +44,31 @@ class RESTClientObject:
def __init__(self, configuration) -> None: def __init__(self, configuration) -> None:
# maxsize is number of requests to host that are allowed in parallel # maxsize is number of requests to host that are allowed in parallel
maxsize = configuration.connection_pool_maxsize self.maxsize = configuration.connection_pool_maxsize
ssl_context = ssl.create_default_context( self.ssl_context = ssl.create_default_context(
cafile=configuration.ssl_ca_cert cafile=configuration.ssl_ca_cert
) )
if configuration.cert_file: if configuration.cert_file:
ssl_context.load_cert_chain( self.ssl_context.load_cert_chain(
configuration.cert_file, keyfile=configuration.key_file configuration.cert_file, keyfile=configuration.key_file
) )
if not configuration.verify_ssl: if not configuration.verify_ssl:
ssl_context.check_hostname = False self.ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE self.ssl_context.verify_mode = ssl.CERT_NONE
connector = aiohttp.TCPConnector(
limit=maxsize,
ssl=ssl_context
)
self.proxy = configuration.proxy self.proxy = configuration.proxy
self.proxy_headers = configuration.proxy_headers self.proxy_headers = configuration.proxy_headers
# https pool manager self.retries = configuration.retries
self.pool_manager = aiohttp.ClientSession(
connector=connector,
trust_env=True
)
retries = configuration.retries self.pool_manager: Optional[aiohttp.ClientSession] = None
self.retry_client: Optional[aiohttp_retry.RetryClient] self.retry_client: Optional[aiohttp_retry.RetryClient] = None
if retries is not None:
self.retry_client = aiohttp_retry.RetryClient(
client_session=self.pool_manager,
retry_options=aiohttp_retry.ExponentialRetry(
attempts=retries,
factor=2.0,
start_timeout=0.1,
max_timeout=120.0
)
)
else:
self.retry_client = None
async def close(self): async def close(self) -> None:
await self.pool_manager.close() if self.pool_manager:
await self.pool_manager.close()
if self.retry_client is not None: if self.retry_client is not None:
await self.retry_client.close() await self.retry_client.close()
@ -195,10 +175,27 @@ class RESTClientObject:
raise ApiException(status=0, reason=msg) raise ApiException(status=0, reason=msg)
pool_manager: Union[aiohttp.ClientSession, aiohttp_retry.RetryClient] pool_manager: Union[aiohttp.ClientSession, aiohttp_retry.RetryClient]
if self.retry_client is not None and method in ALLOW_RETRY_METHODS:
# https pool manager
if self.pool_manager is None:
self.pool_manager = aiohttp.ClientSession(
connector=aiohttp.TCPConnector(limit=self.maxsize, ssl=self.ssl_context),
trust_env=True,
)
pool_manager = self.pool_manager
if self.retries is not None and method in ALLOW_RETRY_METHODS:
if self.retry_client is None:
self.retry_client = aiohttp_retry.RetryClient(
client_session=self.pool_manager,
retry_options=aiohttp_retry.ExponentialRetry(
attempts=self.retries,
factor=2.0,
start_timeout=0.1,
max_timeout=120.0
)
)
pool_manager = self.retry_client pool_manager = self.retry_client
else:
pool_manager = self.pool_manager
r = await pool_manager.request(**args) r = await pool_manager.request(**args)

View File

@ -54,51 +54,31 @@ class RESTClientObject:
def __init__(self, configuration) -> None: def __init__(self, configuration) -> None:
# maxsize is number of requests to host that are allowed in parallel # maxsize is number of requests to host that are allowed in parallel
maxsize = configuration.connection_pool_maxsize self.maxsize = configuration.connection_pool_maxsize
ssl_context = ssl.create_default_context( self.ssl_context = ssl.create_default_context(
cafile=configuration.ssl_ca_cert cafile=configuration.ssl_ca_cert
) )
if configuration.cert_file: if configuration.cert_file:
ssl_context.load_cert_chain( self.ssl_context.load_cert_chain(
configuration.cert_file, keyfile=configuration.key_file configuration.cert_file, keyfile=configuration.key_file
) )
if not configuration.verify_ssl: if not configuration.verify_ssl:
ssl_context.check_hostname = False self.ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE self.ssl_context.verify_mode = ssl.CERT_NONE
connector = aiohttp.TCPConnector(
limit=maxsize,
ssl=ssl_context
)
self.proxy = configuration.proxy self.proxy = configuration.proxy
self.proxy_headers = configuration.proxy_headers self.proxy_headers = configuration.proxy_headers
# https pool manager self.retries = configuration.retries
self.pool_manager = aiohttp.ClientSession(
connector=connector,
trust_env=True
)
retries = configuration.retries self.pool_manager: Optional[aiohttp.ClientSession] = None
self.retry_client: Optional[aiohttp_retry.RetryClient] self.retry_client: Optional[aiohttp_retry.RetryClient] = None
if retries is not None:
self.retry_client = aiohttp_retry.RetryClient(
client_session=self.pool_manager,
retry_options=aiohttp_retry.ExponentialRetry(
attempts=retries,
factor=2.0,
start_timeout=0.1,
max_timeout=120.0
)
)
else:
self.retry_client = None
async def close(self): async def close(self) -> None:
await self.pool_manager.close() if self.pool_manager:
await self.pool_manager.close()
if self.retry_client is not None: if self.retry_client is not None:
await self.retry_client.close() await self.retry_client.close()
@ -205,10 +185,27 @@ class RESTClientObject:
raise ApiException(status=0, reason=msg) raise ApiException(status=0, reason=msg)
pool_manager: Union[aiohttp.ClientSession, aiohttp_retry.RetryClient] pool_manager: Union[aiohttp.ClientSession, aiohttp_retry.RetryClient]
if self.retry_client is not None and method in ALLOW_RETRY_METHODS:
# https pool manager
if self.pool_manager is None:
self.pool_manager = aiohttp.ClientSession(
connector=aiohttp.TCPConnector(limit=self.maxsize, ssl=self.ssl_context),
trust_env=True,
)
pool_manager = self.pool_manager
if self.retries is not None and method in ALLOW_RETRY_METHODS:
if self.retry_client is None:
self.retry_client = aiohttp_retry.RetryClient(
client_session=self.pool_manager,
retry_options=aiohttp_retry.ExponentialRetry(
attempts=self.retries,
factor=2.0,
start_timeout=0.1,
max_timeout=120.0
)
)
pool_manager = self.retry_client pool_manager = self.retry_client
else:
pool_manager = self.pool_manager
r = await pool_manager.request(**args) r = await pool_manager.request(**args)

View File

@ -10,14 +10,6 @@ import petstore_api
HOST = 'http://localhost/v2' HOST = 'http://localhost/v2'
class TestApiClient(unittest.IsolatedAsyncioTestCase): class TestApiClient(unittest.IsolatedAsyncioTestCase):
async def test_context_manager_closes_client(self):
async with petstore_api.ApiClient() as client:
# pool_manager
self.assertFalse(client.rest_client.pool_manager.closed)
rest_pool_ref = client.rest_client.pool_manager
self.assertTrue(rest_pool_ref.closed)
async def test_ignore_operation_servers(self): async def test_ignore_operation_servers(self):
config = petstore_api.Configuration(host=HOST) config = petstore_api.Configuration(host=HOST)
async with petstore_api.ApiClient(config) as client: async with petstore_api.ApiClient(config) as client: