redis-py: New `asyncio.exceptions.CancelledError` from 4.5.2
Version: 4.5.2 Platform: Python 3.11.2 on Debian unstable Description:
Starting with 4.5.2, I’m seeing a new exception when attempting to write to Redis using the asyncio layer. The problem seems to be related to FastAPI: if I create a FastAPI app with any Starlette custom middlware, and then try to set a key in Redis using a separate connection pool created with redis.asyncio.from_url, the set request fails immediately with an uncaught asyncio.exceptions.CancelledError and the following backtrace:
.tox/py/lib/python3.11/site-packages/gafaelfawr/storage/base.py:140: in store
await self._redis.set(key, encrypted_data, ex=lifetime)
.tox/py/lib/python3.11/site-packages/redis/asyncio/client.py:509: in execute_command
conn = self.connection or await pool.get_connection(command_name, **options)
.tox/py/lib/python3.11/site-packages/redis/asyncio/connection.py:1408: in get_connection
if await connection.can_read_destructive():
.tox/py/lib/python3.11/site-packages/redis/asyncio/connection.py:817: in can_read_destructive
return await self._parser.can_read_destructive()
.tox/py/lib/python3.11/site-packages/redis/asyncio/connection.py:250: in can_read_destructive
return await self._stream.read(1)
/usr/lib/python3.11/asyncio/streams.py:689: in read
await self._wait_for_data('read')
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <StreamReader transport=<_SelectorSocketTransport fd=15 read=polling write=<idle, bufsize=0>>>
func_name = 'read'
async def _wait_for_data(self, func_name):
"""Wait until feed_data() or feed_eof() is called.
If stream was paused, automatically resume it.
"""
# StreamReader uses a future to link the protocol feed_data() method
# to a read coroutine. Running two read coroutines at the same time
# would have an unexpected behaviour. It would not possible to know
# which coroutine would get the next data.
if self._waiter is not None:
raise RuntimeError(
f'{func_name}() called while another coroutine is '
f'already waiting for incoming data')
assert not self._eof, '_wait_for_data after EOF'
# Waiting for data while paused will make deadlock, so prevent it.
# This is essential for readexactly(n) for case when n > self._limit.
if self._paused:
self._paused = False
self._transport.resume_reading()
self._waiter = self._loop.create_future()
try:
> await self._waiter
E asyncio.exceptions.CancelledError
/usr/lib/python3.11/asyncio/streams.py:522: CancelledError
GitHub Actions failure log: https://github.com/lsst-sqre/gafaelfawr/actions/runs/4473916874/jobs/7861846517
Reverting to redis 4.5.1 makes the problem disappear again, so it appears to be triggered by some change in 4.5.2.
Here’s the shortest test case that I’ve come up with. It depends on FastAPI and Starlette but none of my code base. My guess is that something is happening during addition of the middleware that’s breaking something about the asyncio work inside redis, but I’m not sure what that could be. Oddly, adding one of the standard Starlette middleware classes does not trigger this problem, only a custom middleware class derived from BaseHTTPMiddleware.
from collections.abc import Callable, Awaitable
import redis.asyncio
from fastapi import FastAPI, APIRouter, Request, Response
from httpx import AsyncClient
from starlette.middleware.base import BaseHTTPMiddleware
class DummyMiddleware(BaseHTTPMiddleware):
async def dispatch(
self,
request: Request,
call_next: Callable[[Request], Awaitable[Response]],
) -> Response:
return await call_next(request)
@pytest.mark.asyncio
async def test_redis() -> None:
app = FastAPI()
app.add_middleware(DummyMiddleware)
@app.get("/")
async def index() -> dict[str, str]:
return {}
async with AsyncClient(app=app, base_url="https://example.com/") as client:
await client.get("/")
pool = redis.asyncio.from_url("redis://localhost:6379/0")
await pool.set("key", "value")
(If I had to guess, it would be the switch to asyncio.timeout, but I can’t find evidence to support that. I know asyncio.timeout does raise CancelledError and then converts it to TimeoutError, but I couldn’t find any place where that machinery shouldn’t work correctly. This error happens immediately, so it doesn’t seem to be a timeout and therefore I may be barking up the wrong tree.)
About this issue
- Original URL
- State: closed
- Created a year ago
- Reactions: 13
- Comments: 27 (12 by maintainers)
Commits related to this issue
- Avoid redis 4.5.2 for now See https://github.com/redis/redis-py/issues/2633. — committed to lsst-sqre/gafaelfawr by rra a year ago
- fix: do not use asyncio's timeout lib before 3.11.2 There's an issue in asyncio's timeout lib before 3.11.3 that causes async calls to raise `CancelledError`. This is a cpython issue that was fixed ... — committed to bellini666/redis-py by bellini666 a year ago
- fix: do not use asyncio's timeout lib before 3.11.2 (#2659) There's an issue in asyncio's timeout lib before 3.11.3 that causes async calls to raise `CancelledError`. This is a cpython issue that... — committed to redis/redis-py by bellini666 a year ago
- chore(deps): bump redis from 4.5.3 to 4.5.4 Redis 4.5.4 has a workaround for redis/redis-py#2633, which was causing cache misses to raise an uncaught `CancelledError` in the weather backend. — committed to mozilla-services/merino-py by linabutler a year ago
- chore(deps): bump redis from 4.5.3 to 4.5.4 (#258) Redis 4.5.4 has a workaround for redis/redis-py#2633, which was causing cache misses to raise an uncaught `CancelledError` in the weather backend. — committed to mozilla-services/merino-py by linabutler a year ago
- Bump redis to >=4.5.5 because of https://github.com/redis/redis-py/issues/2633 — committed to workfloworchestrator/orchestrator-core by Mark90 a year ago
- Bump redis to >=4.5.5 because of https://github.com/redis/redis-py/issues/2633 — committed to workfloworchestrator/orchestrator-core by Mark90 a year ago
- Bump redis to >=4.5.5 because of https://github.com/redis/redis-py/issues/2633 (#286) — committed to workfloworchestrator/orchestrator-core by Mark90 a year ago
- Merge master to 5.0 (#2827) * fix: do not use asyncio's timeout lib before 3.11.2 (#2659) There's an issue in asyncio's timeout lib before 3.11.3 that causes async calls to raise `CancelledError`... — committed to redis/redis-py by dvora-h a year ago
- Merge 5.0 to master (#2849) * Reorganizing the parsers code, and add support for RESP3 (#2574) * Reorganizing the parsers code * fix build package * fix imports * fix flake8 * add resp... — committed to redis/redis-py by dvora-h a year ago
To everyone here, I think I found the culprit.
After testing checking that the issue from my reproduction example did not happen with the
async_timeoutlib, I went looking at the difference between it and the cpython’s one. I didn’t have to go too far, when looking at the history from the timeouts module I found this commit: https://github.com/python/cpython/commit/04adf2df395ded81922c71360a5d66b597471e49I applied that patch locally and the issue from my reproduction example was gone.
So, this is actually a cpython issue and not a redis issue, and specifically for python
3.11because for 3.10 and earlier theasync_timeoutlib is still being used.What
rediscan do here is to change all imports:to
Because the change was cherry-picked to the 3.11 branch and will be available on the next point release (which is
3.11.3, the current one is3.11.2).I actually already opened a PR for that: https://github.com/redis/redis-py/pull/2659
edit: the cpython’s PR: https://github.com/python/cpython/pull/102815
@sileht I have created a minimal reproduction example using
httpxandredis@4.5.3Running it will result in:
Testing with
4.5.2also gives the same error, but installing4.5.1makes the code work without any errorsIn
4.5.1the output is the expected:Don’t know if
httpxis doing something that it shouldn’t, but it is strange that it affected redis later without any obvious reasonThanks for the help it worked upgrading to 3.11.4
I’m facing the exactly same issue on version 4.5.5 using python 3.11.2
The issue started after anyio version was bumped from 3.6.2 to 3.7.0. The suggested solution of updating python version to 3.11.3 did the trick for me.
@curtiscook Yes, this issue fixed in in 4.5.4 (#2659)
@bellini666 nice finding!
Looks like version 4.5.3 release didn’t resolve the issue.
Since redis uses a connection pool by default, this implies to me that there are two bugs:
asyncio.exceptions.CancelledErrorclosing the redis connection (existing)FWIW: I’m experiencing the same issue. I’ve noticed that the connection does work, but seems to break if you await a function that’s not Redis. We were able to reproduce this on the command line w/o starlette (even though we are using FastAPI) given
This function will work fine:
This will break:
Might be related that bar uses HTTPX