Skip to main content

Provdes distributed async client rate limiters for using Redis

Project description




distributed async rate limiters for clients


PyPI test status coverage python version

This library implements an async distributed semaphore, as well as the token bucket algorithm. Both implementations are FIFO, and use redis as the backend.

Between them, the two implementations make it possible to regulate traffic with respect to:

  • Concurrency based limits (max 5 active requests at the time, across all servers), or
  • Time based limits (max 5 requests every 10 seconds, across all servers)

The motivation for implementing these was to help with rate-limiting, but the semaphore and token bucket implementations can be used for anything.

Installation

pip install self-limiters

Usage

Both implementations are written as async context managers, and are used the same way.

Semaphore

A semaphore can be initialized and used like this:

from self_limiters import Semaphore


# allow 10 concurrent requests
concurrency_limited_queue = Semaphore(  # <-- init
    name="foo",
    capacity=10,
    max_sleep=15,
    redis_url="redis://localhost:6379"
)

async def main():
    async with concurrency_limited_queue:  # <-- use
        client.get(...)

A MaxSleepExceededError is raised after max_sleep seconds, if a value was specified. By default, the value is zero, which means wait forever.

Token bucket

A token bucket is used like this:

from self_limiters import TokenBucket

# allow 10 requests per minute
time_limited_queue = TokenBucket(  # <-- init
    name="unique-resource-name",
    capacity=10,
    refill_frequency=60,
    refill_amount=10,  #
    max_sleep=600,
    redis_url="redis://localhost:6379"
)

async def main():
    async with time_limited_queue:  # <-- use
        client.get(...)

The token bucket implementation immediately estimates when a new entry to the queue will have its turn. A MaxSleepExceededError is raised if the time estimated exceeds the specified max sleep. If max_sleep is unspecified, we'll let the queue grow forever.

Decorators

The package doesn't ship any decorators, but if you want to apply these to a function, you can roll your own, like this:

from functools import wraps

from self_limiters import Semaphore


def limit(name, capacity):
    # Initialize semaphore or token bucket
    limiter = Semaphore(name=name, capacity=capacity, ...)

    async def middle(f):
        @wraps(f)
        async def inner(*args, **kwargs):
            # Use context manager within the decorator
            async with limiter:
                return f(*args, **kwargs)
        return inner
    return middle


# The decorator will then ensure we sleep until ready
@limit(name="my-user-api", capacity=50)
def retrieve_user(id: UUID) -> User:
    ...

Performance considerations

Some parts of the package logic are implemented using Lua scripts, to run on the redis instance. This makes it possible to do the same work in one request (from the client), that would otherwise need n requests. Lua scripts seems to present the most efficient way to run the calls each implementation requires us to, and client calls to Lua scripts are non-blocking.

For example, the flow of the semaphore implementation is:

Each step is one call by the client, and all of these are non-blocking.

For the second implementation, the token bucket, the breakdown is even simpler.

  • Run the initial Lua script to retrieve a wake-up time
  • Sleep asynchronously until the wake-up time

Both of these are also non-blocking.

In other words, the limiters' impact on the application event-loop should be negligible.

Implementation details

This section assumes you know how a Python (async) context manager works. If this is new to you, take a look here first!

The semaphore implementation

The semaphore implementation is useful when you need to limit a process to n concurrent actions. For example if you have 10 web servers, and you're interacting with an API that will only tolerate 5 concurrent requests before locking you out.

In terms of fairness, the semaphore implementation skews towards FIFO, but is opportunistic. A worker will not be allowed to run until there is capacity assigned to them, specifically; but the order of execution is not guaranteed to be exactly FIFO.

The flow can be broken down as follows:

  • An initial Lua script will call SETNX on the name of the queue plus a postfix (if the name specified in the class instantiation is "my-queue", then the queue name will be __self-limiters:my-queue). If the returned value is 1 it means the queue we will use for our semaphore does not exist yet and needs to be created.

    (It might strike you as weird to maintain a separate string, just to indicate the existence of a list, when we could just check the list itself. It would actually be great if we could use EXISTS on the list directly, but a list is deleted when all elements are popped (i.e., when a semaphore is fully acquired), so I don't see another way of doing this. Contributions are welcome if you do.)

  • If the queue needs to be created we call RPUSH with the number of arguments equal to the capacity value used when initializing the semaphore instance.

  • Once the queue has been created, we call BLPOP to block until it's our turn. BLPOP is FIFO by default. We also make sure to specify the max_sleep based on the initialized semaphore instance setting. If nothing was passed we allow sleeping forever.

  • On __aexit__ we call another script to RPUSH a 1 value back into the queue (i.e., release the semaphore) and set an expiry on the queue and the string value we called SETNX on.

    The expires are a half measure for dealing with dropped capacity. If a node holding the semaphore dies, the capacity might never be returned. If, however, there is no one using the semaphore for the duration of the expiry value, all values will be cleared, and the semaphore will be recreated at full capacity next time it's used. The expiry is 30 seconds at the time of writing, but could be made configurable.

The token bucket implementation

The token bucket implementation is useful when you need to limit a process by time. For example, to 1 request per minute, or 500 every 10 seconds.

The implementation is forward-looking. It works out the time there would have been capacity in the bucket for a given client and returns that time. From there we can asynchronously sleep until it's time to perform our rate limited action.

The flow can be broken down as follows:

  • A Lua script GETs the state of the bucket.

    The bucket has state for the last slot scheduled and the number of tokens left for that slot. With a capacity of 1, having a tokens_left_for_slot variable makes no sense, but if there's capacity of 2 or more, it is possible that we will need to schedule multiple clients to the same slot.

    The script then works out whether to decrement the tokens_left_for_slot value, or to increment the slot value wrt. the frequency variable.

    Finally, we store the bucket state again using SETEX. This allows us to store the state and set expiry at the same time. The default expiry is 30 at the time of writing, but could be made configurable.

    One thing to note, is that this would not work if it wasn't for the fact that redis is single threaded, so Lua scripts on Redis are FIFO. Without this we would need locks and a lot more logic.

  • Then we just sleep. Simple!

Benchmarks

When testing locally:

  • processing 100 nodes with the semaphore implementation takes ~6ms
  • processing 100 nodes with the token bucket implementation takes ~4ms

Contributing

Please do!

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distributions

self_limiters-0.0.16-cp38-abi3-win_amd64.whl (665.6 kB view details)

Uploaded CPython 3.8+ Windows x86-64

self_limiters-0.0.16-cp38-abi3-win32.whl (620.1 kB view details)

Uploaded CPython 3.8+ Windows x86

self_limiters-0.0.16-cp38-abi3-musllinux_1_1_x86_64.whl (2.0 MB view details)

Uploaded CPython 3.8+ musllinux: musl 1.1+ x86-64

self_limiters-0.0.16-cp38-abi3-musllinux_1_1_aarch64.whl (1.9 MB view details)

Uploaded CPython 3.8+ musllinux: musl 1.1+ ARM64

self_limiters-0.0.16-cp38-abi3-manylinux_2_24_armv7l.whl (1.6 MB view details)

Uploaded CPython 3.8+ manylinux: glibc 2.24+ ARMv7l

self_limiters-0.0.16-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (1.6 MB view details)

Uploaded CPython 3.8+ manylinux: glibc 2.17+ ARM64

self_limiters-0.0.16-cp38-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl (1.9 MB view details)

Uploaded CPython 3.8+ manylinux: glibc 2.12+ x86-64

self_limiters-0.0.16-cp38-abi3-manylinux_2_12_i686.manylinux2010_i686.whl (1.9 MB view details)

Uploaded CPython 3.8+ manylinux: glibc 2.12+ i686

self_limiters-0.0.16-cp38-abi3-macosx_11_0_arm64.whl (642.5 kB view details)

Uploaded CPython 3.8+ macOS 11.0+ ARM64

self_limiters-0.0.16-cp38-abi3-macosx_10_7_x86_64.whl (681.2 kB view details)

Uploaded CPython 3.8+ macOS 10.7+ x86-64

File details

Details for the file self_limiters-0.0.16-cp38-abi3-win_amd64.whl.

File metadata

File hashes

Hashes for self_limiters-0.0.16-cp38-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 b3af966f08792b71134f1b6043d6723c1887eebf4a256b4bd8286150c3ec0a50
MD5 b71f1ca8cbb744d477df39ae0d876c91
BLAKE2b-256 903dca51d2c8b30d5ea9eb6efb088630b0846d21fd1072bb8bf8ed4734097e3e

See more details on using hashes here.

File details

Details for the file self_limiters-0.0.16-cp38-abi3-win32.whl.

File metadata

File hashes

Hashes for self_limiters-0.0.16-cp38-abi3-win32.whl
Algorithm Hash digest
SHA256 4f49687c9dc4c5ef63f9474dec679226b13f47d6fff697f572a594d23fb70efe
MD5 60880e98248d944eb5f408f4c4a211b2
BLAKE2b-256 0c8f8121cc67f27979e74c166d5d3a9ffa700337e98d642f5304918e351a23f9

See more details on using hashes here.

File details

Details for the file self_limiters-0.0.16-cp38-abi3-musllinux_1_1_x86_64.whl.

File metadata

File hashes

Hashes for self_limiters-0.0.16-cp38-abi3-musllinux_1_1_x86_64.whl
Algorithm Hash digest
SHA256 ffbd81ddbe53b09474863e4d3d78b6a88087b2a2a36b0e06e991f40d9bc268d6
MD5 48a9e3381276fd9c89bc6d85de703fc2
BLAKE2b-256 d6faf2ae32ee80f411e9b2b1a8758a1fef1d54a214cce9f76aa0f2c8bcaecc8c

See more details on using hashes here.

File details

Details for the file self_limiters-0.0.16-cp38-abi3-musllinux_1_1_aarch64.whl.

File metadata

File hashes

Hashes for self_limiters-0.0.16-cp38-abi3-musllinux_1_1_aarch64.whl
Algorithm Hash digest
SHA256 4dbfac6ed7c0a28696c895d55650724d3abc946d924351d93dd94168f04ebab8
MD5 c2ad5aad40eeeddaf20d8bb968d8ec07
BLAKE2b-256 5e656f37eb2d996169952c56a9cde8273f134f4cefcf1a848a09b098dd8e84c3

See more details on using hashes here.

File details

Details for the file self_limiters-0.0.16-cp38-abi3-manylinux_2_24_armv7l.whl.

File metadata

File hashes

Hashes for self_limiters-0.0.16-cp38-abi3-manylinux_2_24_armv7l.whl
Algorithm Hash digest
SHA256 ae0d82903bd10d060d9c7704032ce0e53e89e8392e65a95254fa88f84ae35069
MD5 10451ca12b6eed20928ce0e6276d1acd
BLAKE2b-256 ea91e190e33da0b445a18a39e022cedb22bee393fa8fbbf419cfe652847ae552

See more details on using hashes here.

File details

Details for the file self_limiters-0.0.16-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for self_limiters-0.0.16-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 721d05085029f0361c169fd58369f92060bdc69f0b396cb79fd15ccb450eb609
MD5 6c89d7941419b5d7b5eb75ee0342cf8a
BLAKE2b-256 8cc81e83762da049e42269b833bc092c2873cb0fac7ccc87030be8b59823f3a4

See more details on using hashes here.

File details

Details for the file self_limiters-0.0.16-cp38-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl.

File metadata

File hashes

Hashes for self_limiters-0.0.16-cp38-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl
Algorithm Hash digest
SHA256 f900243edbd9b5b182151c0aed160fd5686549a7db38d88718a6a86129a2c961
MD5 26ac5cc3dcb60e1c7bcf3f445a21875d
BLAKE2b-256 c9e36ad634a8063954c9eb809f54e598098591b8d8d6e401a2b2e6bfecefe512

See more details on using hashes here.

File details

Details for the file self_limiters-0.0.16-cp38-abi3-manylinux_2_12_i686.manylinux2010_i686.whl.

File metadata

File hashes

Hashes for self_limiters-0.0.16-cp38-abi3-manylinux_2_12_i686.manylinux2010_i686.whl
Algorithm Hash digest
SHA256 74d05c9a25be807ced5f0d2df9f1a027c94b07ca57fcb06afada6ac256441924
MD5 00bb7b1618f3b2ee7ef3a578e6535391
BLAKE2b-256 5e3b64ba1221c97222303b5445d2bb286d3b1ac4ec58d18dc4b738f8d6aa0863

See more details on using hashes here.

File details

Details for the file self_limiters-0.0.16-cp38-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for self_limiters-0.0.16-cp38-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 d345e437d7fe744a4370788e357d734711de754f1253168e5034fe84ba205601
MD5 65ff204e2ebbb467d68066674befc7fc
BLAKE2b-256 365bab67ed3bdda74561f53f898c22281fd2f02d282b934fd14dd19886f009f8

See more details on using hashes here.

File details

Details for the file self_limiters-0.0.16-cp38-abi3-macosx_10_7_x86_64.whl.

File metadata

File hashes

Hashes for self_limiters-0.0.16-cp38-abi3-macosx_10_7_x86_64.whl
Algorithm Hash digest
SHA256 6f1cbf2bfb76844e6b0d806604299d8a85f95bf8f4703624b5a06b4e9554d82c
MD5 5a062c05332644bb83e0808fecc02bbe
BLAKE2b-256 ba5db347cb807abebade317c8eb1380a15e71667edff62a10c5cb01d9f924ac8

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page