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


A self-limiting source produces traffic which never exceeds some upper bound. This is helpful when interacting with rate limited resources, or to prevent burstiness.

self-limiters provides a way to police your outgoing traffic with respect to:

To use this package, you'll need to be running an async stack, and have redis available on Python 3.8 or above.

Installation

pip install self-limiters

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 take 4. One benefit of this is that it eliminates the latency for each request saved. However, the biggest benefit is while the lua script is running, our python app event-loop is freed up to do other things.

The flow of the semaphore implementation is:

  • Run initial script to create semaphore if needed
  • Run BLPOP to wait for the semaphore to return
  • Run script to "release" the semaphore by adding back capacity

All of these are non-blocking.

The flow for the token bucket implementation is:

  • Run initial 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.

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 goes roughly like this:

Flow breakdown
  1. The Lua script will call SETNX on the name of the queue plus a postfix. 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 have a separate entry for indicating whether the list should be created or not. It would be great if we could use EXISTS on the list directly instead, but a list is deleted when all elements are popped, so I don't see another way of achieving this. Contributions are welcome if you do.)

  2. 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.

  3. 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.

  4. On __aexit__ we call another script to RPUSH a 1 value back into the queue and set an expiry on the queue and the 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.

Usage

The utility is implemented as a context manager in Python. Here is an example of a semaphore which will allow 10 concurrent requests:

from self_limiters import semaphore


# Instantiate a semaphore that will allow 10 concurrent requests
concurrency_limited_queue = semaphore(
    name="unique-resource-name",
    capacity=10,
    redis_url="redis://localhost:6379"
)

while True:
    async with concurrency_limited_queue:
        client.get(...)

The token bucket implementation

The token bucket implementation is useful when you need to limit a process to a certain number of actions per unit of time. For example, 1 request per minute.

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 code flow goes:

Flow breakdown
  1. The Lua script first GETs the state of the bucket. That means, the last slot that was 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 on 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.

  2. Then we just sleep!

Usage

This is also implemented as a context manager in Python and can be used roughly as follows:

from self_limiters import TokenBucket

# Instantiate a bucket that will allow 10 requests per minute
time_limited_queue = TokenBucket(
    name="unique-resource-name",
    capacity=10,
    refill_frequency=60,
    refill_amount=10,
    redis_url="redis://localhost:6379"
)

while True:
    async with time_limited_queue:
        # Perform the rate-limited work immediately
        client.get(...)

Benchmarks

When testing locally:

  • processing 100 nodes with the semaphore implementation takes ~13ms
  • processing 100 nodes with the token bucket implementation takes ~7ms

Contributing

Debugging Lua scripts

Assuming you have a redis server running at :6389 you can debug a lua script by calling redis-cli -u redis://127.0.0.1:6389 --ldb --eval src/semaphore/rpushnx.lua x 1.

Just type help in the debugger for options.

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

Uploaded CPython 3.8+ Windows x86-64

self_limiters-0.0.7-cp38-abi3-win32.whl (597.4 kB view details)

Uploaded CPython 3.8+ Windows x86

self_limiters-0.0.7-cp38-abi3-musllinux_1_1_x86_64.whl (904.7 kB view details)

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

self_limiters-0.0.7-cp38-abi3-musllinux_1_1_aarch64.whl (851.1 kB view details)

Uploaded CPython 3.8+ musllinux: musl 1.1+ ARM64

self_limiters-0.0.7-cp38-abi3-manylinux_2_24_armv7l.whl (659.9 kB view details)

Uploaded CPython 3.8+ manylinux: glibc 2.24+ ARMv7l

self_limiters-0.0.7-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (677.9 kB view details)

Uploaded CPython 3.8+ manylinux: glibc 2.17+ ARM64

self_limiters-0.0.7-cp38-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl (716.4 kB view details)

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

self_limiters-0.0.7-cp38-abi3-manylinux_2_12_i686.manylinux2010_i686.whl (773.3 kB view details)

Uploaded CPython 3.8+ manylinux: glibc 2.12+ i686

self_limiters-0.0.7-cp38-abi3-macosx_11_0_arm64.whl (622.2 kB view details)

Uploaded CPython 3.8+ macOS 11.0+ ARM64

self_limiters-0.0.7-cp38-abi3-macosx_10_7_x86_64.whl (658.6 kB view details)

Uploaded CPython 3.8+ macOS 10.7+ x86-64

File details

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

File metadata

File hashes

Hashes for self_limiters-0.0.7-cp38-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 aa679e8a1e7d9a3e39db827779b7fbde61b74ed63bd57c7c951424f30b7daedb
MD5 bffa57e37690148ed1c466deea26f115
BLAKE2b-256 f520aa40ae475b04d1956665b2650a5b76eb15b008c0d698292216549df67c1b

See more details on using hashes here.

File details

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

File metadata

  • Download URL: self_limiters-0.0.7-cp38-abi3-win32.whl
  • Upload date:
  • Size: 597.4 kB
  • Tags: CPython 3.8+, Windows x86
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/4.0.1 CPython/3.10.6

File hashes

Hashes for self_limiters-0.0.7-cp38-abi3-win32.whl
Algorithm Hash digest
SHA256 cb7d07c881ca1fbff88e35a41f4ca5798ec948b9f91768865b046bcf292a6472
MD5 39a89de3acdfdf8c7e8053a0dd5e9e2e
BLAKE2b-256 8487851011f721b8d89577d2b078ae22e79838a53469be17bc8760795c5071fc

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for self_limiters-0.0.7-cp38-abi3-musllinux_1_1_x86_64.whl
Algorithm Hash digest
SHA256 fbdbb4da4fa501029e9978e0793a11868899a381824ec9c548c43deeb47798ea
MD5 f15538a28214261c6b4534dfb3348da3
BLAKE2b-256 90aba074d8d572225f487601058c8dfdf5c0f4f056e6f3998d6c99b79cbbed03

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for self_limiters-0.0.7-cp38-abi3-musllinux_1_1_aarch64.whl
Algorithm Hash digest
SHA256 a6ed03425b9fdf555c46a20e517c3a6973ca81b71bf3681b36a63747bd3e9c25
MD5 642fe1e4b6933e1395953bace51abae5
BLAKE2b-256 89d57c6facadf322a6c406651feebe75b99c697d65084134ce96d06184874d88

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for self_limiters-0.0.7-cp38-abi3-manylinux_2_24_armv7l.whl
Algorithm Hash digest
SHA256 fb792958c76e611bafd2e7eaed2d93737ce86c24f7a2d3a060041791625054d6
MD5 135e69efa07dff61514e01fa5e16a0a9
BLAKE2b-256 10be5ccd45e24350391fa0d3b5794871bcceec69e09f9e8c339d73be092c3d31

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for self_limiters-0.0.7-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 ad89850812d86d285c7fcbd232300e327edd269346922fcf5775b79ed99ff43d
MD5 be08589880b16e8c567c5a945fc57e3f
BLAKE2b-256 adaacde6666a3841aac174c3aa72c3de9360537c33ce3a95153df0c854b0143b

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for self_limiters-0.0.7-cp38-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl
Algorithm Hash digest
SHA256 f6c9242e381a7a31656d2469f28a5c11fbe38ce63f082b7cbf979fbc3b63d7b6
MD5 3129a42dde7f53a123152c84f36a2d13
BLAKE2b-256 c330ffbb521c7d03c1c67747c3af0556a0a488d0091dd6b6931004e172ac652f

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for self_limiters-0.0.7-cp38-abi3-manylinux_2_12_i686.manylinux2010_i686.whl
Algorithm Hash digest
SHA256 2483bbfdaf5aeb79292fa35335a107fe284e99cc4a09776f5e11cca500fd54df
MD5 cceabf303d19b1377e4c1f287ad2707a
BLAKE2b-256 33843850f919386dcfbbca9f356af42fc5d824d6f2c9bdd674572d4ca8098f3b

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for self_limiters-0.0.7-cp38-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 bb64ded0e0d762f370886616475be93308ad91cecf67757064799e9d5fe7a745
MD5 186c261f053818f112721eea6013f39a
BLAKE2b-256 9e929760517136f2898728f2a078b61b3a3dcd1d70f9e46634ccd151b50dc09b

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for self_limiters-0.0.7-cp38-abi3-macosx_10_7_x86_64.whl
Algorithm Hash digest
SHA256 09ac4423b53c844bcf78f398a057b80e99092fdf56585cb892d431b33c46a4b5
MD5 25bb7d3fa098ce8a38abde25118ccad2
BLAKE2b-256 4d5aca4b84db07ad6f35e001c268b254c229d44d29bd4885f56cede90dce08aa

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