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.13-cp38-abi3-win_amd64.whl (643.0 kB view details)

Uploaded CPython 3.8+ Windows x86-64

self_limiters-0.0.13-cp38-abi3-win32.whl (597.8 kB view details)

Uploaded CPython 3.8+ Windows x86

self_limiters-0.0.13-cp38-abi3-musllinux_1_1_x86_64.whl (906.2 kB view details)

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

self_limiters-0.0.13-cp38-abi3-musllinux_1_1_aarch64.whl (852.2 kB view details)

Uploaded CPython 3.8+ musllinux: musl 1.1+ ARM64

self_limiters-0.0.13-cp38-abi3-manylinux_2_24_armv7l.whl (660.9 kB view details)

Uploaded CPython 3.8+ manylinux: glibc 2.24+ ARMv7l

self_limiters-0.0.13-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (679.6 kB view details)

Uploaded CPython 3.8+ manylinux: glibc 2.17+ ARM64

self_limiters-0.0.13-cp38-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl (717.3 kB view details)

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

self_limiters-0.0.13-cp38-abi3-manylinux_2_12_i686.manylinux2010_i686.whl (773.9 kB view details)

Uploaded CPython 3.8+ manylinux: glibc 2.12+ i686

self_limiters-0.0.13-cp38-abi3-macosx_11_0_arm64.whl (623.5 kB view details)

Uploaded CPython 3.8+ macOS 11.0+ ARM64

self_limiters-0.0.13-cp38-abi3-macosx_10_7_x86_64.whl (659.2 kB view details)

Uploaded CPython 3.8+ macOS 10.7+ x86-64

File details

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

File metadata

File hashes

Hashes for self_limiters-0.0.13-cp38-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 afaaca9f4b7202bff3d5056510573ddbfda3a69fa4d1e54fff34ad8398ec3c0a
MD5 329805906f5c2724f26f1f013a95c24d
BLAKE2b-256 32280f8f390bea56ce37f33160ce150b7f4cc2c364011e48785c140c73a9c71f

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for self_limiters-0.0.13-cp38-abi3-win32.whl
Algorithm Hash digest
SHA256 860b20425176fc781db46b6910a5e76e6d987f7d086cd027a1889266a13dd6ab
MD5 07f919b9290b4c7f2677823980623ea0
BLAKE2b-256 10f52ab03faabe492483d37addd725193f4eb34ff8f355d6eade300823f2670b

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for self_limiters-0.0.13-cp38-abi3-musllinux_1_1_x86_64.whl
Algorithm Hash digest
SHA256 9545823cb15850048f7566c23f3e854d3d2da332b25a9028bd0172d6fffceb16
MD5 77b5ab943e30a45ece046f45d05f32f5
BLAKE2b-256 07c77691d8ae7eca4201a84eae000d2dcd57d0e0e0a7516e386c54fa05b0d449

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for self_limiters-0.0.13-cp38-abi3-musllinux_1_1_aarch64.whl
Algorithm Hash digest
SHA256 1a6b555c176d47f40c6f6445cc40d1904129cfd848d211e1c23c8a0dd91812e7
MD5 f401d2b74405ef7ff40fd7deb872ca9a
BLAKE2b-256 dc87fd4bf3b59b2b8987d25dcfca001275eacf0c6007ced336e006d9231c4b2a

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for self_limiters-0.0.13-cp38-abi3-manylinux_2_24_armv7l.whl
Algorithm Hash digest
SHA256 01e1115cdae7e32fcf7a886ad26f9ede09d40d6cc1a0046922fb0df4cbe5567a
MD5 811b2aec5c25557d234b3f36a4658738
BLAKE2b-256 83af4c1843dadf78cee7c44025a1fb69bbf7495a84471d0555888ae33502423c

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for self_limiters-0.0.13-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 9112c983271eb8c9b85d74e5121ea86fe45ec85dd232b39ea156bf864efd2e97
MD5 33782dc422537953d80f67bff4855bab
BLAKE2b-256 faf8f9e503afb82e81798a1a71a3669b8803c45c69fd69b6b3f722720bdb6ea7

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for self_limiters-0.0.13-cp38-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl
Algorithm Hash digest
SHA256 f871ff4379182ac42690e0931f480ce6f4bf45624e13882287ef11ee4973b3aa
MD5 758984a6394a556f9f3f1fcac971c16e
BLAKE2b-256 2050b5cd19a0df13e44fd0ac9f8c5219e79e2df9a6176898be98a237e11a4f38

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for self_limiters-0.0.13-cp38-abi3-manylinux_2_12_i686.manylinux2010_i686.whl
Algorithm Hash digest
SHA256 2160f33d501548a84d5d05f461eeeed7fc9e6564d597d4f7c86d051722380935
MD5 805d29021f1ef93e41c6ce08591c236b
BLAKE2b-256 13d7ccc0035bc71fd347e876a269891310f629b07218d4a3f68057642365817b

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for self_limiters-0.0.13-cp38-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 13f033688fc40ea9ffd720734ba11f4d7fbdb7326e0ce937443a8d03b1f066f1
MD5 d1d1185081abf310f30958da8c096b0a
BLAKE2b-256 c9c132ddb3a09e8c329fc7d00c669ecbcecaf2bd906a86ce80287e10ba004beb

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for self_limiters-0.0.13-cp38-abi3-macosx_10_7_x86_64.whl
Algorithm Hash digest
SHA256 afc61f20e2dfa80e46fbd324288ed6411e7d00396d019396cd65550ed0b231fe
MD5 b89b31f32790263c2b8a248d6cb56d5e
BLAKE2b-256 6b8c92abcd71effd5802da314de7a898b4dccd073343fd4b6491b3811d903ca9

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