Repository navigation
Closed
Description
This might be intentional or not, but I couldn't find anything in the docs about it.
Code-Example
import asyncio
import concurrent.futures
import httpx
async def fetch(client):
print("Sending request..")
r = await client.get('https://www.example.com/')
print("Got %d chars" % len(r.content))
async def foo():
loop = asyncio.get_running_loop()
async with httpx.AsyncClient() as client:
with concurrent.futures.ThreadPoolExecutor() as pool:
# first time works fine
await loop.run_in_executor(pool, asyncio.run, fetch(client))
# subsequent calls fail during SSL handshake
await loop.run_in_executor(pool, asyncio.run, fetch(client))
asyncio.run(foo())I am aware that these are different methods of achieving concurrency combined, but there are szenarios where both are required. We tried to re-use the client to have it use the same connection pool, but no luck.
The single threaded version where await loop.run_in_executor(pool, asyncio.run, fetch(client)) is replaced with await fetch(client) works just fine.
Expected output
Sending request..
Got 1256 chars
Sending request..
Got 1256 charsActual output
Sending request..
Got 1256 chars
Sending request..
Traceback (most recent call last):
File "<redacted>/virtualenvs/<project>/lib/python3.10/site-packages/anyio/streams/tls.py", line 108, in _call_sslobject_method
result = func(*args)
File "/usr/lib/python3.10/ssl.py", line 916, in read
v = self._sslobj.read(len)
ssl.SSLWantReadError: The operation did not complete (read) (_ssl.c:2540)
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "<redacted>/virtualenvs/<project>/lib/python3.10/site-packages/httpcore/_async/http11.py", line 81, in handle_async_request
) = await self._receive_response_headers(**kwargs)
File "<redacted>/virtualenvs/<project>/lib/python3.10/site-packages/httpcore/_async/http11.py", line 143, in _receive_response_headers
event = await self._receive_event(timeout=timeout)
File "<redacted>/virtualenvs/<project>/lib/python3.10/site-packages/httpcore/_async/http11.py", line 172, in _receive_event
data = await self._network_stream.read(
File "<redacted>/virtualenvs/<project>/lib/python3.10/site-packages/httpcore/backends/asyncio.py", line 31, in read
return await self._stream.receive(max_bytes=max_bytes)
File "<redacted>/virtualenvs/<project>/lib/python3.10/site-packages/anyio/streams/tls.py", line 171, in receive
data = await self._call_sslobject_method(self._ssl_object.read, max_bytes)
File "<redacted>/virtualenvs/<project>/lib/python3.10/site-packages/anyio/streams/tls.py", line 115, in _call_sslobject_method
data = await self.transport_stream.receive()
File "<redacted>/virtualenvs/<project>/lib/python3.10/site-packages/anyio/_backends/_asyncio.py", line 1104, in receive
self._transport.resume_reading()
File "/usr/lib/python3.10/asyncio/selector_events.py", line 808, in resume_reading
self._add_reader(self._sock_fd, self._read_ready)
File "/usr/lib/python3.10/asyncio/selector_events.py", line 754, in _add_reader
self._loop._add_reader(fd, callback, *args)
File "/usr/lib/python3.10/asyncio/selector_events.py", line 258, in _add_reader
self._check_closed()
File "/usr/lib/python3.10/asyncio/base_events.py", line 510, in _check_closed
raise RuntimeError('Event loop is closed')
RuntimeError: Event loop is closed
During handling of the above exception, another exception occurred:
[...]Suggested workarounds
- Use
threading.localto have thread local client instances (loosing the shared connection pool, complicates connection management to the backend)
Suggested fixes
- If this is not possible to support or wanted behaviour, update the documentation at https://www.python-httpx.org/async/ to state that AsyncClient is not thread safe
- If a connection from the pool can be separated from an event loop this might be supported?
Metadata
Metadata
Assignees
Labels
No labels
Activity
lovelydinosaur commentedon Feb 1, 2022
Um... yes certainly not an expected usage, or something that I'd expect to work.
It's not so much about "thread safety" (which I'd consider to be to do with locking during concurrency), as that it doesn't support a single client instance being used across multiple different event loops.
I guess you might want to take a look at if the behaviour here is any different with:
aiohttpinstead ofhttpx.trioinstead ofhttpx.But generally I wouldn't expect it to work, and I wouldn't expect us to support it.
Perhaps you can explain more why you're bumping into this?
[-]AsyncClient is not thread safe (RuntimeError: Event loop is closed)[/-][+]AsyncClient does not support being used within multiple event loops[/+]bede commentedon May 31, 2022
Like the OP I am receiving
ssl.SSLWantReadError: The operation did not complete (read) (_ssl.c:2548)The following code raises this intermittently, and having read this issue I wondered if there is problem with the pattern I am using.
download_single_async()is basically just awaiting aclient.get()callprovinzkraut commentedon Sep 28, 2022
I'm running into the same issue and I'd argue that it's actually a fairly common usage. Maybe the original example wasn't that great but I think I can illustrate the issue a bit more clearly:
Having two running event loops simultaneously might not be common, but creating and closing event loops most certainly is, and it causes the same issue.
I'd say that this is not thread safe, as you cannot pass an
AsyncClientinstance into another thread and make requests there without either causing aRuntimeErrorthere or in the main thread. Not being able to be used across multiple event loops is what causesAsyncClientto not be thread safe, as that is required when using threads.The only way around this currently is to either manually reset the pool (which is hacky) or to create a new client instance for each request (which is not recommended).
Possible fix
If a connection in the pool "sticks" to an event loop, don't try to reuse that connection if the event loop it's associated with is closed.
Alternatives
If discarding connections with closed associated event loops is out of scope or simply not a desirable feature (I'm not sure what the drawbacks of that would be though), it would be nice to at least have an option to either reset the pool manually, or to opt out of it (ie have a non-pool transport).
Workarounds
Set
keepalive_expiry=0by instantiatingAsyncClientlike this:More hacky workaround that still makes use of connections pools within the same event loop:
The drawback here is that connection pooling won't be used for any request, but you still get the overhead of creating and managing the pool.
Zac-HD commentedon Feb 23, 2024
FYI this is leading Stripe to disable connection pooling, because of user friction where
unittestcreates a separate loop for each test.If creating a separate pool per-event-loop when running on
asynciois feasible, I think that would allow connection pooling to stay default-enabled and lead to an overall better user experience.