Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Async retry needs to capture OSError exception in retry #3450

Open
rickyzhang82 opened this issue Dec 5, 2024 · 1 comment
Open

Async retry needs to capture OSError exception in retry #3450

rickyzhang82 opened this issue Dec 5, 2024 · 1 comment

Comments

@rickyzhang82
Copy link

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

@dadwin
Copy link

dadwin commented Dec 23, 2024

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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants