Provdes distributed async client rate limiters for using Redis
Project description
Distributed async rate limiters for clients
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:
- Concurrency, using a distributed semaphore
- Time, using a distributed token bucket
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
-
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.) -
If the queue needs to be created we call
RPUSH
with the number of arguments equal to thecapacity
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 themax_sleep
based on the initialized semaphore instance setting. If nothing was passed we allow sleeping forever. -
On
__aexit__
we call another script toRPUSH
a1
value back into the queue and set an expiry on the queue and the value we calledSETNX
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
-
The Lua script first
GET
s 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 atokens_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.
-
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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distributions
File details
Details for the file self_limiters-0.0.7-cp38-abi3-win_amd64.whl
.
File metadata
- Download URL: self_limiters-0.0.7-cp38-abi3-win_amd64.whl
- Upload date:
- Size: 641.3 kB
- Tags: CPython 3.8+, Windows x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/4.0.1 CPython/3.10.6
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | aa679e8a1e7d9a3e39db827779b7fbde61b74ed63bd57c7c951424f30b7daedb |
|
MD5 | bffa57e37690148ed1c466deea26f115 |
|
BLAKE2b-256 | f520aa40ae475b04d1956665b2650a5b76eb15b008c0d698292216549df67c1b |
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
Algorithm | Hash digest | |
---|---|---|
SHA256 | cb7d07c881ca1fbff88e35a41f4ca5798ec948b9f91768865b046bcf292a6472 |
|
MD5 | 39a89de3acdfdf8c7e8053a0dd5e9e2e |
|
BLAKE2b-256 | 8487851011f721b8d89577d2b078ae22e79838a53469be17bc8760795c5071fc |
File details
Details for the file self_limiters-0.0.7-cp38-abi3-musllinux_1_1_x86_64.whl
.
File metadata
- Download URL: self_limiters-0.0.7-cp38-abi3-musllinux_1_1_x86_64.whl
- Upload date:
- Size: 904.7 kB
- Tags: CPython 3.8+, musllinux: musl 1.1+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/4.0.1 CPython/3.10.6
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | fbdbb4da4fa501029e9978e0793a11868899a381824ec9c548c43deeb47798ea |
|
MD5 | f15538a28214261c6b4534dfb3348da3 |
|
BLAKE2b-256 | 90aba074d8d572225f487601058c8dfdf5c0f4f056e6f3998d6c99b79cbbed03 |
File details
Details for the file self_limiters-0.0.7-cp38-abi3-musllinux_1_1_aarch64.whl
.
File metadata
- Download URL: self_limiters-0.0.7-cp38-abi3-musllinux_1_1_aarch64.whl
- Upload date:
- Size: 851.1 kB
- Tags: CPython 3.8+, musllinux: musl 1.1+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/4.0.1 CPython/3.10.6
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | a6ed03425b9fdf555c46a20e517c3a6973ca81b71bf3681b36a63747bd3e9c25 |
|
MD5 | 642fe1e4b6933e1395953bace51abae5 |
|
BLAKE2b-256 | 89d57c6facadf322a6c406651feebe75b99c697d65084134ce96d06184874d88 |
File details
Details for the file self_limiters-0.0.7-cp38-abi3-manylinux_2_24_armv7l.whl
.
File metadata
- Download URL: self_limiters-0.0.7-cp38-abi3-manylinux_2_24_armv7l.whl
- Upload date:
- Size: 659.9 kB
- Tags: CPython 3.8+, manylinux: glibc 2.24+ ARMv7l
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/4.0.1 CPython/3.10.6
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | fb792958c76e611bafd2e7eaed2d93737ce86c24f7a2d3a060041791625054d6 |
|
MD5 | 135e69efa07dff61514e01fa5e16a0a9 |
|
BLAKE2b-256 | 10be5ccd45e24350391fa0d3b5794871bcceec69e09f9e8c339d73be092c3d31 |
File details
Details for the file self_limiters-0.0.7-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
.
File metadata
- Download URL: self_limiters-0.0.7-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
- Upload date:
- Size: 677.9 kB
- Tags: CPython 3.8+, manylinux: glibc 2.17+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/4.0.1 CPython/3.10.6
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | ad89850812d86d285c7fcbd232300e327edd269346922fcf5775b79ed99ff43d |
|
MD5 | be08589880b16e8c567c5a945fc57e3f |
|
BLAKE2b-256 | adaacde6666a3841aac174c3aa72c3de9360537c33ce3a95153df0c854b0143b |
File details
Details for the file self_limiters-0.0.7-cp38-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl
.
File metadata
- Download URL: self_limiters-0.0.7-cp38-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl
- Upload date:
- Size: 716.4 kB
- Tags: CPython 3.8+, manylinux: glibc 2.12+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/4.0.1 CPython/3.10.6
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | f6c9242e381a7a31656d2469f28a5c11fbe38ce63f082b7cbf979fbc3b63d7b6 |
|
MD5 | 3129a42dde7f53a123152c84f36a2d13 |
|
BLAKE2b-256 | c330ffbb521c7d03c1c67747c3af0556a0a488d0091dd6b6931004e172ac652f |
File details
Details for the file self_limiters-0.0.7-cp38-abi3-manylinux_2_12_i686.manylinux2010_i686.whl
.
File metadata
- Download URL: self_limiters-0.0.7-cp38-abi3-manylinux_2_12_i686.manylinux2010_i686.whl
- Upload date:
- Size: 773.3 kB
- Tags: CPython 3.8+, manylinux: glibc 2.12+ i686
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/4.0.1 CPython/3.10.6
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 2483bbfdaf5aeb79292fa35335a107fe284e99cc4a09776f5e11cca500fd54df |
|
MD5 | cceabf303d19b1377e4c1f287ad2707a |
|
BLAKE2b-256 | 33843850f919386dcfbbca9f356af42fc5d824d6f2c9bdd674572d4ca8098f3b |
File details
Details for the file self_limiters-0.0.7-cp38-abi3-macosx_11_0_arm64.whl
.
File metadata
- Download URL: self_limiters-0.0.7-cp38-abi3-macosx_11_0_arm64.whl
- Upload date:
- Size: 622.2 kB
- Tags: CPython 3.8+, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/4.0.1 CPython/3.10.6
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | bb64ded0e0d762f370886616475be93308ad91cecf67757064799e9d5fe7a745 |
|
MD5 | 186c261f053818f112721eea6013f39a |
|
BLAKE2b-256 | 9e929760517136f2898728f2a078b61b3a3dcd1d70f9e46634ccd151b50dc09b |
File details
Details for the file self_limiters-0.0.7-cp38-abi3-macosx_10_7_x86_64.whl
.
File metadata
- Download URL: self_limiters-0.0.7-cp38-abi3-macosx_10_7_x86_64.whl
- Upload date:
- Size: 658.6 kB
- Tags: CPython 3.8+, macOS 10.7+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/4.0.1 CPython/3.10.6
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 09ac4423b53c844bcf78f398a057b80e99092fdf56585cb892d431b33c46a4b5 |
|
MD5 | 25bb7d3fa098ce8a38abde25118ccad2 |
|
BLAKE2b-256 | 4d5aca4b84db07ad6f35e001c268b254c229d44d29bd4885f56cede90dce08aa |