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

Uploaded CPython 3.8+ Windows x86-64

self_limiters-0.0.19-cp38-abi3-win32.whl (620.3 kB view details)

Uploaded CPython 3.8+ Windows x86

self_limiters-0.0.19-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.19-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.19-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.19-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.19-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.19-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.19-cp38-abi3-macosx_11_0_arm64.whl (642.6 kB view details)

Uploaded CPython 3.8+ macOS 11.0+ ARM64

self_limiters-0.0.19-cp38-abi3-macosx_10_7_x86_64.whl (680.2 kB view details)

Uploaded CPython 3.8+ macOS 10.7+ x86-64

File details

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

File metadata

File hashes

Hashes for self_limiters-0.0.19-cp38-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 132f90f696eb7929ce79eb0e8dd3e3a206d6ab1c577fa3f151da19e2ca6e3fa9
MD5 bb6186f5fea4e9276bc3d6cf5c5d6325
BLAKE2b-256 130edbde72eb44efe7c41f5f710ae1c6b37b286191d0788efbd6f693333d48d9

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for self_limiters-0.0.19-cp38-abi3-win32.whl
Algorithm Hash digest
SHA256 9552b109e63a06df4dd1e02125c717790723836c8c6f4e98aa311945a2e5153b
MD5 ea090da29afc87612135f5b39784e725
BLAKE2b-256 7c218220bd4808617ab9a207908a23041a23790230893a2039b5dddd9dd975f4

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for self_limiters-0.0.19-cp38-abi3-musllinux_1_1_x86_64.whl
Algorithm Hash digest
SHA256 8e93c6573d9a1e46bdd6b7527f83e553d1fc2d04e762ea4dbbc4be4491bf9a4a
MD5 0af728475716a0ded3b7930d64cbf7e3
BLAKE2b-256 c3fe0ba9f6bf5152727f62a9a2cb7cd7770a36d74a099d76112a89035c2cb0b3

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for self_limiters-0.0.19-cp38-abi3-musllinux_1_1_aarch64.whl
Algorithm Hash digest
SHA256 f7f3cc1175699f860b580c64f08ab9d47b42381e59ab8a770ee23a395eba09a8
MD5 42240ec0678dd5caf79f9c1d7e748aea
BLAKE2b-256 349223d4b752f1abe370369c5067c392fc175bbf9ab944ba43f4d83715ab33ae

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for self_limiters-0.0.19-cp38-abi3-manylinux_2_24_armv7l.whl
Algorithm Hash digest
SHA256 0051fa3e3a846d522ac22e286dc2b2b8d1316a628902a7cd75830d1c8e77ce67
MD5 3f8ece9d9fff8114ae5db746bf2b2638
BLAKE2b-256 4ea6a9192b2c006fb9d158ef0064d27617a5e65d8d5ff95daa7852af73b4b66d

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for self_limiters-0.0.19-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 ab4322e5d3b29b22e4d15f98c318cb3d9421ab72b0d8ad96fef360acbe07911c
MD5 9b1afd286365a0c2f9cfd9fbc258d835
BLAKE2b-256 9f05962f4b25a6478fd4bd9bd660bdf7ce46212a811e935155b62153a3e6a3e5

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for self_limiters-0.0.19-cp38-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl
Algorithm Hash digest
SHA256 c7e5a29daf9629232cfca18f3edeaedfbaaa9f79fa70fd1093b4505bba59715e
MD5 7f75094b080b707b37bbef8308568dd6
BLAKE2b-256 1f8f12e1ba0eeb91fa117195d47ae87d63271f178a3f257875bea81d3f276469

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for self_limiters-0.0.19-cp38-abi3-manylinux_2_12_i686.manylinux2010_i686.whl
Algorithm Hash digest
SHA256 39eb002796d344c31c153af3ed1e2f1075bdd541483411076e925ace06475803
MD5 bd07cdc589b9c94157afe695918bdca5
BLAKE2b-256 af08518c64db2fef680eeec108d0072ecc1c4e5991c1f504ecc8aeea15f91bb4

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for self_limiters-0.0.19-cp38-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 1b5cb69672e2ab0b5bc5687b1ef34304107dd0ea53e3a2a503c784f9e605d6ee
MD5 983eaeca22ab96de9347c78acf9c1bbb
BLAKE2b-256 9e55ab41033d3dfdde15c2b2ecfa2123f6d43fb1b2310c5e26d828e673a9d189

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for self_limiters-0.0.19-cp38-abi3-macosx_10_7_x86_64.whl
Algorithm Hash digest
SHA256 20bee05eeec5b7cb5fc22f1e5e9b11a9e5af7622ee48d541ff4b7da9f05f8203
MD5 1a7e29cd2e6260494eb7ef9a97fb8f94
BLAKE2b-256 0b3c3f6ad2d1241433cdb2a3fea7323e5f583916c359088ddd6358de9dc3ebd8

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