redis-py icon indicating copy to clipboard operation
redis-py copied to clipboard

Async retry needs to capture OSError exception in retry

Open rickyzhang82 opened this issue 1 year ago • 5 comments

Version: What redis-py and what redis version is the issue happening on?

redis-py 5.20

Platform: What platform / version? (For example Python 3.5.1 on Windows 7 / Ubuntu 15.10 / Azure)

Fedora 40, Python 3.12

Description: Description of your issue, stack traces from errors and code that reproduces the issue

I tried the program for asyncio version from Redis doc by taking down Redis server.

The first exception was raised is built-in OSError rather than redis.exceptions.ConnectionError. The exception regarding to the lost connection from Redis sever behave differently between the asnycio version and the normal version.

import asyncio
import redis.asyncio as redis
from redis.asyncio.retry import Retry
from redis.backoff import ExponentialBackoff
from redis.exceptions import BusyLoadingError, ConnectionError, TimeoutError

import logging

# Configure the logging module
logging.basicConfig(
    format='%(asctime)s - %(levelname)s - %(message)s',
    level=logging.INFO
)


async def main():
    logging.info(f"Creating async Redis client...")
    r = await redis.from_url("redis://127.0.0.1",
                             retry=Retry(ExponentialBackoff(8, 1), 25),
                             retry_on_error=[BusyLoadingError, ConnectionError, TimeoutError, ConnectionResetError, ])
    logging.info(f"Created async Redis client...")
    logging.info(f"Redis client pinging...")
    await r.ping()
    logging.info(f"Redis client pinged!")
    logging.info(f"Closing async Redis client...")
    await r.aclose()
    logging.info(f"Closed async Redis client...")


# start the asyncio program
asyncio.run(main())
/home/Ricky/.virtualenv/pytool/bin/python /home/Ricky/private/repo/pytool/asyncio/demo_asyncio_redis_client_retry.py 
2024-12-05 14:18:09,802 - INFO - Creating async Redis client...
2024-12-05 14:18:09,803 - INFO - Created async Redis client...
2024-12-05 14:18:09,803 - INFO - Redis client pinging...
Traceback (most recent call last):
  File "/home/Ricky/.virtualenv/pytool/lib/python3.12/site-packages/redis/asyncio/connection.py", line 275, in connect
    await self.retry.call_with_retry(
  File "/home/Ricky/.virtualenv/pytool/lib/python3.12/site-packages/redis/asyncio/retry.py", line 59, in call_with_retry
    return await do()
           ^^^^^^^^^^
  File "/home/Ricky/.virtualenv/pytool/lib/python3.12/site-packages/redis/asyncio/connection.py", line 691, in _connect
    reader, writer = await asyncio.open_connection(
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib64/python3.12/asyncio/streams.py", line 48, in open_connection
    transport, _ = await loop.create_connection(
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib64/python3.12/asyncio/base_events.py", line 1121, in create_connection
    raise exceptions[0]
  File "/usr/lib64/python3.12/asyncio/base_events.py", line 1103, in create_connection
    sock = await self._connect_sock(
           ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib64/python3.12/asyncio/base_events.py", line 1006, in _connect_sock
    await self.sock_connect(sock, address)
  File "/usr/lib64/python3.12/asyncio/selector_events.py", line 651, in sock_connect
    return await fut
           ^^^^^^^^^
  File "/usr/lib64/python3.12/asyncio/selector_events.py", line 691, in _sock_connect_cb
    raise OSError(err, f'Connect call failed {address}')
ConnectionRefusedError: [Errno 111] Connect call failed ('127.0.0.1', 6379)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/Ricky/private/repo/pytool/asyncio/demo_asyncio_redis_client_retry.py", line 31, in <module>
    asyncio.run(main())
  File "/usr/lib64/python3.12/asyncio/runners.py", line 194, in run
    return runner.run(main)
           ^^^^^^^^^^^^^^^^
  File "/usr/lib64/python3.12/asyncio/runners.py", line 118, in run
    return self._loop.run_until_complete(task)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib64/python3.12/asyncio/base_events.py", line 687, in run_until_complete
    return future.result()
           ^^^^^^^^^^^^^^^
  File "/home/Ricky/private/repo/pytool/asyncio/demo_asyncio_redis_client_retry.py", line 23, in main
    await r.ping()
  File "/home/Ricky/.virtualenv/pytool/lib/python3.12/site-packages/redis/asyncio/client.py", line 611, in execute_command
    conn = self.connection or await pool.get_connection(command_name, **options)
                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/Ricky/.virtualenv/pytool/lib/python3.12/site-packages/redis/asyncio/connection.py", line 1058, in get_connection
    await self.ensure_connection(connection)
  File "/home/Ricky/.virtualenv/pytool/lib/python3.12/site-packages/redis/asyncio/connection.py", line 1091, in ensure_connection
    await connection.connect()
  File "/home/Ricky/.virtualenv/pytool/lib/python3.12/site-packages/redis/asyncio/connection.py", line 283, in connect
    raise ConnectionError(self._error_message(e))
redis.exceptions.ConnectionError: Error 111 connecting to 127.0.0.1:6379. Connect call failed ('127.0.0.1', 6379).

Process finished with exit code 1

rickyzhang82 avatar Dec 05 '24 19:12 rickyzhang82

you can create a custom Retry class with adjusted call_with_retry() method and pass it down to pool/client:

class NeatRetry(Retry):
    def __init__(self, backoff, retries: int, supported_errors):
        if OSError not in supported_errors:
            supported_errors += (OSError,)
        super().__init__(backoff, retries, supported_errors)

    async def call_with_retry(self, do: Callable[[], Awaitable[T]], fail: Callable[[RedisError], Any]) -> T:
        """
        Execute an operation that might fail and returns its result, or
        raise the exception that was thrown depending on the `Backoff` object.
        `do`: the operation to call. Expects no argument.
        `fail`: the failure handler, expects the last error that was thrown
        """
        self._backoff.reset()
        failures = 0
        while True:
            try:
                return await do()
            except self._supported_errors as error:
                if isinstance(error, OSError) and error.args and len(error.args) > 0:
                    if error.args[0] not in (61, 104, 111):
                        # retrying ENODATA, ECONNRESET, ECONNREFUSED
                        raise error

                failures += 1
                await fail(error)
                if self._retries >= 0 and failures > self._retries:
                    raise error
                backoff = self._backoff.compute(failures)
                if backoff > 0:
                    await asyncio.sleep(backoff)

dadwin avatar Dec 23 '24 17:12 dadwin

Hi! Client instance accept a list of exceptions you want to Retry on, you can specify OSError there

retry_on_error: Optional[list] = None,

Closing issue for now. Let me know if it doesn't resolved

vladvildanov avatar Feb 11 '25 11:02 vladvildanov

Why was this even closed @vladvildanov ? The provided response does not address the issue at hand. This behaviour needs to be documented somewhere.

1henrypage avatar Mar 25 '25 18:03 1henrypage

I second your point, @1henrypage. It is a hidden bomb in the absence of proper documentation. The Asyncio version redis-py is not a drop-in replacement for the regular version.

rickyzhang82 avatar Mar 26 '25 01:03 rickyzhang82

@1henrypage You're right, was a bit confused with first response, looked like an issue with retries. Reopening issue

vladvildanov avatar Mar 26 '25 07:03 vladvildanov