token-bucket 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- token_bucket/__init__.py +18 -0
- token_bucket/limiter.py +129 -0
- token_bucket/py.typed +0 -0
- token_bucket/storage.py +179 -0
- token_bucket/storage_base.py +69 -0
- token_bucket/version.py +4 -0
- token_bucket-0.4.0.dist-info/METADATA +71 -0
- token_bucket-0.4.0.dist-info/RECORD +11 -0
- token_bucket-0.4.0.dist-info/WHEEL +5 -0
- token_bucket-0.4.0.dist-info/licenses/LICENSE +17 -0
- token_bucket-0.4.0.dist-info/top_level.txt +1 -0
token_bucket/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""token_bucket package."""
|
|
2
|
+
|
|
3
|
+
# NOTE(kgriffs): The following imports are to be used by consumers of
|
|
4
|
+
# the token_bucket package; modules within the package itself should
|
|
5
|
+
# not use this "front-door" module, but rather import using the
|
|
6
|
+
# fully-qualified paths.
|
|
7
|
+
|
|
8
|
+
from .limiter import Limiter
|
|
9
|
+
from .storage import MemoryStorage
|
|
10
|
+
from .storage_base import StorageBase
|
|
11
|
+
from .version import __version__
|
|
12
|
+
|
|
13
|
+
__all__ = (
|
|
14
|
+
'Limiter',
|
|
15
|
+
'MemoryStorage',
|
|
16
|
+
'StorageBase',
|
|
17
|
+
'__version__',
|
|
18
|
+
)
|
token_bucket/limiter.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Copyright 2016 by Rackspace Hosting, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from .storage_base import StorageBase
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Limiter(object):
|
|
19
|
+
"""Limits demand for a finite resource via keyed token buckets.
|
|
20
|
+
|
|
21
|
+
A limiter manages a set of token buckets that have an identical
|
|
22
|
+
rate, capacity, and storage backend. Each bucket is referenced
|
|
23
|
+
by a key, allowing for the independent tracking and limiting
|
|
24
|
+
of multiple consumers of a resource.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
rate (float): Number of tokens per second to add to the
|
|
28
|
+
bucket. Over time, the number of tokens that can be
|
|
29
|
+
consumed is limited by this rate. Each token represents
|
|
30
|
+
some percentage of a finite resource that may be
|
|
31
|
+
utilized by a consumer.
|
|
32
|
+
capacity (int): Maximum number of tokens that the bucket
|
|
33
|
+
can hold. Once the bucket is full, additional tokens
|
|
34
|
+
are discarded.
|
|
35
|
+
|
|
36
|
+
The bucket capacity has a direct impact on burst duration.
|
|
37
|
+
Let M be the maximum possible token request rate, r the
|
|
38
|
+
token generation rate (tokens/sec), and b the bucket
|
|
39
|
+
capacity.
|
|
40
|
+
|
|
41
|
+
If r < M the maximum burst duration, in seconds, is:
|
|
42
|
+
|
|
43
|
+
T = b / (M - r)
|
|
44
|
+
|
|
45
|
+
Otherwise, if r >= M, it is not possible to exceed the
|
|
46
|
+
replenishment rate, and therefore a consumer can burst
|
|
47
|
+
at full speed indefinitely.
|
|
48
|
+
|
|
49
|
+
The maximum number of tokens that any one burst may
|
|
50
|
+
consume is:
|
|
51
|
+
|
|
52
|
+
T * M
|
|
53
|
+
|
|
54
|
+
See also: https://en.wikipedia.org/wiki/Token_bucket#Burst_size
|
|
55
|
+
storage (token_bucket.StorageBase): A storage engine to use for
|
|
56
|
+
persisting the token bucket data. The following engines are
|
|
57
|
+
available out of the box:
|
|
58
|
+
|
|
59
|
+
token_bucket.MemoryStorage
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
__slots__ = (
|
|
63
|
+
'_rate',
|
|
64
|
+
'_capacity',
|
|
65
|
+
'_storage',
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def __init__(self, rate: float, capacity: int, storage: StorageBase) -> None:
|
|
69
|
+
if not isinstance(rate, (float, int)):
|
|
70
|
+
raise TypeError('rate must be an int or float')
|
|
71
|
+
|
|
72
|
+
if rate <= 0:
|
|
73
|
+
raise ValueError('rate must be > 0')
|
|
74
|
+
|
|
75
|
+
if not isinstance(capacity, int):
|
|
76
|
+
raise TypeError('capacity must be an int')
|
|
77
|
+
|
|
78
|
+
if capacity < 1:
|
|
79
|
+
raise ValueError('capacity must be >= 1')
|
|
80
|
+
|
|
81
|
+
if not isinstance(storage, StorageBase):
|
|
82
|
+
raise TypeError('storage must be a subclass of StorageBase')
|
|
83
|
+
|
|
84
|
+
self._rate = rate
|
|
85
|
+
self._capacity = capacity
|
|
86
|
+
self._storage = storage
|
|
87
|
+
|
|
88
|
+
def consume(self, key: str | bytes, num_tokens: int = 1) -> bool:
|
|
89
|
+
"""Attempt to take one or more tokens from a bucket.
|
|
90
|
+
|
|
91
|
+
If the specified token bucket does not yet exist, it will be
|
|
92
|
+
created and initialized to full capacity before proceeding.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
key (bytes): A string or bytes object that specifies the
|
|
96
|
+
token bucket to consume from. If a global limit is
|
|
97
|
+
desired for all consumers, the same key may be used
|
|
98
|
+
for every call to consume(). Otherwise, a key based on
|
|
99
|
+
consumer identity may be used to segregate limits.
|
|
100
|
+
Keyword Args:
|
|
101
|
+
num_tokens (int): The number of tokens to attempt to
|
|
102
|
+
consume, defaulting to 1 if not specified. It may
|
|
103
|
+
be appropriate to ask for more than one token according
|
|
104
|
+
to the proportion of the resource that a given request
|
|
105
|
+
will use, relative to other requests for the same
|
|
106
|
+
resource.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
bool: True if the requested number of tokens were removed
|
|
110
|
+
from the bucket (conforming), otherwise False (non-
|
|
111
|
+
conforming). The entire number of tokens requested must
|
|
112
|
+
be available in the bucket to be conforming. Otherwise,
|
|
113
|
+
no tokens will be removed (it's all or nothing).
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
if not key:
|
|
117
|
+
if key is None:
|
|
118
|
+
raise TypeError('key may not be None')
|
|
119
|
+
|
|
120
|
+
raise ValueError('key must not be a non-empty string or bytestring')
|
|
121
|
+
|
|
122
|
+
if num_tokens is None:
|
|
123
|
+
raise TypeError('num_tokens may not be None')
|
|
124
|
+
|
|
125
|
+
if num_tokens < 1:
|
|
126
|
+
raise ValueError('num_tokens must be >= 1')
|
|
127
|
+
|
|
128
|
+
self._storage.replenish(key, self._rate, self._capacity)
|
|
129
|
+
return self._storage.consume(key, num_tokens)
|
token_bucket/py.typed
ADDED
|
File without changes
|
token_bucket/storage.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# Copyright 2016 by Rackspace Hosting, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
import time
|
|
16
|
+
|
|
17
|
+
from .storage_base import StorageBase
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class MemoryStorage(StorageBase):
|
|
21
|
+
"""In-memory token bucket storage engine.
|
|
22
|
+
|
|
23
|
+
This storage engine is suitable for multi-threaded applications. For
|
|
24
|
+
performance reasons, race conditions are mitigated but not completely
|
|
25
|
+
eliminated. The remaining effects have the result of reducing the
|
|
26
|
+
effective bucket capacity by a negligible amount. In practice this
|
|
27
|
+
won't be noticeable for the vast majority of applications, but in
|
|
28
|
+
the case that it is, the situation can be remedied by simply
|
|
29
|
+
increasing the bucket capacity by a few tokens.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self) -> None:
|
|
33
|
+
# NOTE(vytas): Each bucket is a list of two items: the current
|
|
34
|
+
# token count and the timestamp of the last replenishment.
|
|
35
|
+
self._buckets: dict[str | bytes, list[float]] = {}
|
|
36
|
+
|
|
37
|
+
def get_token_count(self, key: str | bytes) -> float:
|
|
38
|
+
"""Query the current token count for the given bucket.
|
|
39
|
+
|
|
40
|
+
Note that the bucket is not replenished first, so the count
|
|
41
|
+
will be what it was the last time replenish() was called.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
key (str): Name of the bucket to query.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
float: Number of tokens currently in the bucket (may be
|
|
48
|
+
fractional).
|
|
49
|
+
"""
|
|
50
|
+
try:
|
|
51
|
+
return self._buckets[key][0]
|
|
52
|
+
except KeyError:
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
return 0
|
|
56
|
+
|
|
57
|
+
def replenish(self, key: str | bytes, rate: float, capacity: int) -> None:
|
|
58
|
+
"""Add tokens to a bucket per the given rate.
|
|
59
|
+
|
|
60
|
+
This method is exposed for use by the token_bucket.Limiter
|
|
61
|
+
class.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
# NOTE(kgriffs): Correctness of this algorithm assumes
|
|
66
|
+
# that the calculation of the current time is performed
|
|
67
|
+
# in the same order as the updates based on that
|
|
68
|
+
# timestamp, across all threads. If an older "now"
|
|
69
|
+
# completes before a newer "now", the lower token
|
|
70
|
+
# count will overwrite the newer, effectively reducing
|
|
71
|
+
# the bucket's capacity temporarily, by a minor amount.
|
|
72
|
+
#
|
|
73
|
+
# While a lock could be used to fix this race condition,
|
|
74
|
+
# one isn't used here for the following reasons:
|
|
75
|
+
#
|
|
76
|
+
# 1. The condition above will rarely occur, since
|
|
77
|
+
# the window of opportunity is quite small and
|
|
78
|
+
# even so requires many threads contending for a
|
|
79
|
+
# relatively small number of bucket keys.
|
|
80
|
+
# 2. When the condition does occur, the difference
|
|
81
|
+
# in timestamps will be quite small, resulting in
|
|
82
|
+
# a negligible loss in tokens.
|
|
83
|
+
# 3. Depending on the order in which instructions
|
|
84
|
+
# are interleaved between threads, the condition
|
|
85
|
+
# can be detected and mitigated by comparing
|
|
86
|
+
# timestamps. This mitigation is implemented below,
|
|
87
|
+
# and serves to further minimize the effect of this
|
|
88
|
+
# race condition to negligible levels.
|
|
89
|
+
# 4. While locking introduces only a small amount of
|
|
90
|
+
# overhead (less than a microsecond), there's no
|
|
91
|
+
# reason to waste those CPU cycles in light of the
|
|
92
|
+
# points above.
|
|
93
|
+
# 5. If a lock were used, it would only be held for
|
|
94
|
+
# a microsecond or less. We are unlikely to see
|
|
95
|
+
# much contention for the lock during such a short
|
|
96
|
+
# time window, but we might as well remove the
|
|
97
|
+
# possibility in light of the points above.
|
|
98
|
+
|
|
99
|
+
tokens_in_bucket, last_replenished_at = self._buckets[key]
|
|
100
|
+
|
|
101
|
+
now = time.monotonic()
|
|
102
|
+
|
|
103
|
+
# NOTE(kgriffs): This will detect many, but not all,
|
|
104
|
+
# manifestations of the race condition. If a later
|
|
105
|
+
# timestamp was already used to update the bucket, don't
|
|
106
|
+
# regress by setting the token count to a smaller number.
|
|
107
|
+
if now < last_replenished_at: # pragma: no cover
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
self._buckets[key] = [
|
|
111
|
+
# Limit to capacity
|
|
112
|
+
min(
|
|
113
|
+
capacity,
|
|
114
|
+
# NOTE(kgriffs): The new value is the current number
|
|
115
|
+
# of tokens in the bucket plus the number of
|
|
116
|
+
# tokens generated since last time. Fractional
|
|
117
|
+
# tokens are permitted in order to improve
|
|
118
|
+
# accuracy (now is a float, and rate may be also).
|
|
119
|
+
tokens_in_bucket + (rate * (now - last_replenished_at)),
|
|
120
|
+
),
|
|
121
|
+
# Update the timestamp for use next time
|
|
122
|
+
now,
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
except KeyError:
|
|
126
|
+
self._buckets[key] = [capacity, time.monotonic()]
|
|
127
|
+
|
|
128
|
+
def consume(self, key: str | bytes, num_tokens: int) -> bool:
|
|
129
|
+
"""Attempt to take one or more tokens from a bucket.
|
|
130
|
+
|
|
131
|
+
This method is exposed for use by the token_bucket.Limiter
|
|
132
|
+
class.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
# NOTE(kgriffs): Assume that the key will be present, since
|
|
136
|
+
# replenish() will always be called before consume().
|
|
137
|
+
tokens_in_bucket = self._buckets[key][0]
|
|
138
|
+
if tokens_in_bucket < num_tokens:
|
|
139
|
+
return False
|
|
140
|
+
|
|
141
|
+
# NOTE(kgriffs): In a multi-threaded application, it is
|
|
142
|
+
# possible for two threads to interleave such that they
|
|
143
|
+
# both pass the check above, while in reality if executed
|
|
144
|
+
# linearly, the second thread would not pass the check
|
|
145
|
+
# since the first thread was able to consume the remaining
|
|
146
|
+
# tokens in the bucket.
|
|
147
|
+
#
|
|
148
|
+
# When this race condition occurs, the count in the bucket
|
|
149
|
+
# will go negative, effectively resulting in a slight
|
|
150
|
+
# reduction in capacity.
|
|
151
|
+
#
|
|
152
|
+
# While a lock could be used to fix this race condition,
|
|
153
|
+
# one isn't used here for the following reasons:
|
|
154
|
+
#
|
|
155
|
+
# 1. The condition above will rarely occur, since
|
|
156
|
+
# the window of opportunity is quite small.
|
|
157
|
+
# 2. When the condition does occur, the tokens will
|
|
158
|
+
# usually be quickly replenished since the rate tends
|
|
159
|
+
# to be much larger relative to the number of tokens
|
|
160
|
+
# that are consumed by any one request, and due to (1)
|
|
161
|
+
# the condition is very rarely likely to happen
|
|
162
|
+
# multiple times in a row.
|
|
163
|
+
# 3. In the case of bursting across a large number of
|
|
164
|
+
# threads, the likelihood for this race condition
|
|
165
|
+
# will increase. Even so, the burst will be quickly
|
|
166
|
+
# negated as requests become non-conforming, allowing
|
|
167
|
+
# the bucket to be replenished.
|
|
168
|
+
# 4. While locking introduces only a small amount of
|
|
169
|
+
# overhead (less than a microsecond), there's no
|
|
170
|
+
# reason to waste those CPU cycles in light of the
|
|
171
|
+
# points above.
|
|
172
|
+
# 5. If a lock were used, it would only be held for
|
|
173
|
+
# less than a microsecond. We are unlikely to see
|
|
174
|
+
# much contention for the lock during such a short
|
|
175
|
+
# time window, but we might as well remove the
|
|
176
|
+
# possibility given the points above.
|
|
177
|
+
|
|
178
|
+
self._buckets[key][0] -= num_tokens
|
|
179
|
+
return True
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Copyright 2016 by Rackspace Hosting, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
import abc
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class StorageBase(abc.ABC):
|
|
19
|
+
@abc.abstractmethod
|
|
20
|
+
def get_token_count(self, key: str | bytes) -> float:
|
|
21
|
+
"""Query the current token count for the given bucket.
|
|
22
|
+
|
|
23
|
+
Note that the bucket is not replenished first, so the count
|
|
24
|
+
will be what it was the last time replenish() was called.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
key (str): Name of the bucket to query.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
float: Number of tokens currently in the bucket (may be
|
|
31
|
+
fractional).
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
@abc.abstractmethod
|
|
35
|
+
def replenish(self, key: str | bytes, rate: float, capacity: int) -> None:
|
|
36
|
+
"""Add tokens to a bucket per the given rate.
|
|
37
|
+
|
|
38
|
+
Conceptually, tokens are added to the bucket at a rate of one
|
|
39
|
+
every 1/rate seconds. To accomplish this without requiring an
|
|
40
|
+
out-of-band timer, ``replenish()`` simply calculates the number
|
|
41
|
+
of tokens that should have been added since the last time the
|
|
42
|
+
bucket was replenished.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
key (str): Name of the bucket to replenish.
|
|
46
|
+
rate (float): Number of tokens per second to add to the
|
|
47
|
+
bucket. Over time, the number of tokens that can be
|
|
48
|
+
consumed is limited by this rate.
|
|
49
|
+
capacity (int): Maximum number of tokens that the bucket
|
|
50
|
+
can hold. Once the bucket if full, additional tokens
|
|
51
|
+
are discarded.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
@abc.abstractmethod
|
|
55
|
+
def consume(self, key: str | bytes, num_tokens: int) -> bool:
|
|
56
|
+
"""Attempt to take one or more tokens from a bucket.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
key (str): Name of the bucket to replenish.
|
|
60
|
+
num_tokens (int): Number of tokens to try to consume from
|
|
61
|
+
the bucket. If the bucket contains fewer than the
|
|
62
|
+
requested number, no tokens are removed (i.e., it's all
|
|
63
|
+
or nothing).
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
bool: True if the requested number of tokens were removed
|
|
67
|
+
from the bucket (conforming), otherwise False (non-
|
|
68
|
+
conforming).
|
|
69
|
+
"""
|
token_bucket/version.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: token_bucket
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Very fast implementation of the token bucket algorithm.
|
|
5
|
+
Author-email: kgriffs <mail@kgriffs.com>
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/falconry/token-bucket
|
|
8
|
+
Keywords: web,http,https,cloud,rate,limiting,token,bucket,throttling
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
|
+
Classifier: Environment :: Web Environment
|
|
11
|
+
Classifier: Natural Language :: English
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: System Administrators
|
|
14
|
+
Classifier: Operating System :: MacOS :: MacOS X
|
|
15
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
16
|
+
Classifier: Operating System :: POSIX
|
|
17
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
19
|
+
Classifier: Programming Language :: Python
|
|
20
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
21
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
22
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
27
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
28
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
29
|
+
Classifier: Typing :: Typed
|
|
30
|
+
Requires-Python: >=3.10
|
|
31
|
+
Description-Content-Type: text/x-rst
|
|
32
|
+
License-File: LICENSE
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
|tests| |PyPI| |python-versions| |codecov|
|
|
36
|
+
|
|
37
|
+
A Token Bucket Implementation for Python Web Apps
|
|
38
|
+
=================================================
|
|
39
|
+
|
|
40
|
+
The ``token-bucket`` package provides an implementation of the
|
|
41
|
+
`token bucket algorithm <https://en.wikipedia.org/wiki/Token_bucket>`_
|
|
42
|
+
suitable for use in web applications for shaping or policing request
|
|
43
|
+
rates. This implementation does not require the use of an independent
|
|
44
|
+
timer thread to manage the bucket state.
|
|
45
|
+
|
|
46
|
+
Compared to other rate-limiting algorithms that use a simple counter,
|
|
47
|
+
the token bucket algorithm provides the following advantages:
|
|
48
|
+
|
|
49
|
+
* The thundering herd problem is avoided since bucket capacity is
|
|
50
|
+
replenished gradually, rather than being immediately refilled at the
|
|
51
|
+
beginning of each epoch as is common with simple fixed window
|
|
52
|
+
counters.
|
|
53
|
+
* Burst duration can be explicitly controlled.
|
|
54
|
+
|
|
55
|
+
Moving window algorithms are resistant to bursting, but at the cost of
|
|
56
|
+
additional processing and memory overhead vs. the token bucket
|
|
57
|
+
algorithm which uses a simple, fast counter per key. The latter approach
|
|
58
|
+
does allow for bursting, but only for a controlled duration.
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
.. |tests| image:: https://github.com/falconry/token-bucket/actions/workflows/tests.yaml/badge.svg
|
|
62
|
+
:target: https://github.com/falconry/token-bucket/actions/workflows/tests.yaml
|
|
63
|
+
|
|
64
|
+
.. |PyPI| image:: https://img.shields.io/pypi/v/token-bucket.svg
|
|
65
|
+
:target: https://pypi.org/project/token-bucket/
|
|
66
|
+
|
|
67
|
+
.. |python-versions| image:: https://img.shields.io/pypi/pyversions/token-bucket.svg
|
|
68
|
+
:target: https://pypi.org/project/token-bucket/
|
|
69
|
+
|
|
70
|
+
.. |codecov| image:: https://codecov.io/gh/falconry/token-bucket/branch/master/graph/badge.svg
|
|
71
|
+
:target: https://codecov.io/gh/falconry/token-bucket
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
token_bucket/__init__.py,sha256=mctCom07M8QG3g-79vDUv5acVvdhz4MBEaItORmoMRw,488
|
|
2
|
+
token_bucket/limiter.py,sha256=w1mfIA3UqggCqoiJJKVOk95UhilyHmu_750wXoTHXX4,4943
|
|
3
|
+
token_bucket/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
token_bucket/storage.py,sha256=IDVKQs-zNAO65o-6B1aFU5MgTw9fIBxV1hDUsKYqUys,8170
|
|
5
|
+
token_bucket/storage_base.py,sha256=tTD6oMYt7snCgvncHsWkrWtan10GfEp4s-kE7jlbeDQ,2661
|
|
6
|
+
token_bucket/version.py,sha256=B8-Xybi3gzZ2G19ugV8xmMyhQG-2Ijy-alDLN3rccXA,85
|
|
7
|
+
token_bucket-0.4.0.dist-info/licenses/LICENSE,sha256=LZOp0uraJMtDEzjUG7onDQn6Aefz6yX_XglmMvb94VE,745
|
|
8
|
+
token_bucket-0.4.0.dist-info/METADATA,sha256=4l6NXMoMV94ZEDLOsgWrf-zBSypSZdZgedpNPg5Z3Ps,3192
|
|
9
|
+
token_bucket-0.4.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
10
|
+
token_bucket-0.4.0.dist-info/top_level.txt,sha256=NhXGdH_jH4KpgC9DU6CDq1VoU6q3u1zWQaTnyXE2bIo,13
|
|
11
|
+
token_bucket-0.4.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Copyright by various authors, as noted in the individual source files.
|
|
2
|
+
|
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
you may not use this file except in compliance with the License.
|
|
5
|
+
You may obtain a copy of the License at
|
|
6
|
+
|
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
|
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
See the License for the specific language governing permissions and
|
|
13
|
+
limitations under the License.
|
|
14
|
+
|
|
15
|
+
By contributing to this project, you agree to also license your source
|
|
16
|
+
code under the terms of the Apache License, Version 2.0, as described
|
|
17
|
+
above.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
token_bucket
|