alisaifee / limits

Rate limiting using various strategies and storage backends such as redis & memcached
https://limits.readthedocs.org
MIT License
410 stars 57 forks source link

Bug: async RedisStorage does not accept `async+unix` URIs #189

Closed drygdryg closed 9 months ago

drygdryg commented 9 months ago

Async RedisStorage does not accept URIs of the form async+unix as mentioned in this docstring, like async+unix:///path/to/socket?db=0.

Program to reproduce

import asyncio

from limits.aio.storage import RedisStorage
from limits.aio.strategies import FixedWindowRateLimiter
from limits import parse

async def main():
    storage = RedisStorage('async+unix:///run/redis/redis.sock?db=3')
    limiter = FixedWindowRateLimiter(storage)
    await limiter.hit(parse('1/minute'), limiter)

asyncio.run(main())

Expected Behaviour

The program should connect to Redis at async+unix:///run/redis/redis.sock?db=3 and run without errors.

Current Behaviour

The program is trying to connect to Redis at 127.0.0.1:6379, and a connection error occurs:

Traceback (most recent call last):
  File "/home/user/limits_bug/venv/lib/python3.11/site-packages/coredis/connection.py", line 305, in connect
    await self._connect()
  File "/home/user/limits_bug/venv/lib/python3.11/site-packages/coredis/connection.py", line 792, in _connect
    transport, _ = await connection
                   ^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/asyncio/base_events.py", line 1093, in create_connection
    raise OSError('Multiple exceptions: {}'.format(
OSError: Multiple exceptions: [Errno 111] Connect call failed ('127.0.0.1', 6379), [Errno 101] Network is unreachable

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/user/limits_bug/reproduce.py", line 14, in <module>
    asyncio.run(main())
  File "/usr/lib/python3.11/asyncio/runners.py", line 190, in run
    return runner.run(main)
           ^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/asyncio/runners.py", line 118, in run
    return self._loop.run_until_complete(task)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/asyncio/base_events.py", line 653, in run_until_complete
    return future.result()
           ^^^^^^^^^^^^^^^
  File "/home/user/limits_bug/reproduce.py", line 11, in main
    await limiter.hit(parse('1/minute'), limiter)
  File "/home/user/limits_bug/venv/lib/python3.11/site-packages/limits/aio/strategies.py", line 141, in hit
    await self.storage.incr(
  File "/home/user/limits_bug/venv/lib/python3.11/site-packages/limits/aio/storage/redis.py", line 226, in incr
    int, await self.lua_incr_expire.execute([key], [expiry, amount])
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/user/limits_bug/venv/lib/python3.11/site-packages/coredis/commands/script.py", line 137, in execute
    return await self(keys, args, client, readonly)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/user/limits_bug/venv/lib/python3.11/site-packages/coredis/commands/script.py", line 117, in __call__
    return cast(ResponseType, await method(self.sha, keys=keys, args=args))
                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/user/limits_bug/venv/lib/python3.11/site-packages/coredis/commands/_wrappers.py", line 212, in wrapped
    async with command_cache(callable, *args, **kwargs) as response:
  File "/usr/lib/python3.11/contextlib.py", line 204, in __aenter__
    return await anext(self.gen)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/home/user/limits_bug/venv/lib/python3.11/site-packages/coredis/commands/_wrappers.py", line 106, in __call__
    yield await func(*args, **kwargs)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/user/limits_bug/venv/lib/python3.11/site-packages/coredis/commands/core.py", line 6117, in evalsha
    return await self._evalsha(CommandName.EVALSHA, sha1, keys, args)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/user/limits_bug/venv/lib/python3.11/site-packages/coredis/commands/core.py", line 6098, in _evalsha
    return await self.execute_command(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/user/limits_bug/venv/lib/python3.11/site-packages/coredis/client/basic.py", line 938, in execute_command
    return await self.retry_policy.call_with_retries(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/user/limits_bug/venv/lib/python3.11/site-packages/coredis/retry.py", line 76, in call_with_retries
    raise last_error
  File "/home/user/limits_bug/venv/lib/python3.11/site-packages/coredis/retry.py", line 60, in call_with_retries
    return await func()
           ^^^^^^^^^^^^
  File "/home/user/limits_bug/venv/lib/python3.11/site-packages/coredis/client/basic.py", line 969, in _execute_command
    request = await connection.create_request(
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/user/limits_bug/venv/lib/python3.11/site-packages/coredis/connection.py", line 620, in create_request
    await self.connect()
  File "/home/user/limits_bug/venv/lib/python3.11/site-packages/coredis/connection.py", line 311, in connect
    raise ConnectionError(str(err)) from err
coredis.exceptions.ConnectionError: Multiple exceptions: [Errno 111] Connect call failed ('127.0.0.1', 6379), [Errno 101] Network is unreachable

My Environment

Ways to solve this issue

If you specify async+redis+unix instead of async+unix in the URI scheme, then everything works correctly. We need to fix the docstring or RedisStorage logic.