limits 4.0.1__py3-none-any.whl → 4.1__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.
- limits/_version.py +3 -3
- limits/aio/storage/__init__.py +2 -1
- limits/aio/storage/base.py +70 -24
- limits/aio/storage/etcd.py +6 -2
- limits/aio/storage/memcached.py +157 -33
- limits/aio/storage/memory.py +98 -13
- limits/aio/storage/mongodb.py +217 -9
- limits/aio/storage/redis.py +100 -15
- limits/aio/strategies.py +122 -1
- limits/limits.py +10 -11
- limits/resources/redis/lua_scripts/acquire_sliding_window.lua +45 -0
- limits/resources/redis/lua_scripts/sliding_window.lua +17 -0
- limits/storage/__init__.py +4 -3
- limits/storage/base.py +92 -24
- limits/storage/etcd.py +6 -2
- limits/storage/memcached.py +141 -34
- limits/storage/memory.py +97 -12
- limits/storage/mongodb.py +204 -11
- limits/storage/redis.py +159 -138
- limits/storage/redis_cluster.py +3 -3
- limits/storage/redis_sentinel.py +12 -35
- limits/storage/registry.py +3 -3
- limits/strategies.py +119 -5
- limits/typing.py +43 -15
- limits/util.py +27 -18
- limits-4.1.dist-info/METADATA +268 -0
- limits-4.1.dist-info/RECORD +39 -0
- limits-4.0.1.dist-info/METADATA +0 -192
- limits-4.0.1.dist-info/RECORD +0 -37
- {limits-4.0.1.dist-info → limits-4.1.dist-info}/LICENSE.txt +0 -0
- {limits-4.0.1.dist-info → limits-4.1.dist-info}/WHEEL +0 -0
- {limits-4.0.1.dist-info → limits-4.1.dist-info}/top_level.txt +0 -0
limits/storage/mongodb.py
CHANGED
|
@@ -3,27 +3,26 @@ from __future__ import annotations
|
|
|
3
3
|
import datetime
|
|
4
4
|
import time
|
|
5
5
|
from abc import ABC, abstractmethod
|
|
6
|
-
from typing import cast
|
|
7
6
|
|
|
8
7
|
from deprecated.sphinx import versionadded, versionchanged
|
|
9
8
|
|
|
10
9
|
from limits.typing import (
|
|
11
|
-
Dict,
|
|
12
|
-
List,
|
|
13
10
|
MongoClient,
|
|
14
11
|
MongoCollection,
|
|
15
12
|
MongoDatabase,
|
|
16
13
|
Optional,
|
|
17
|
-
Tuple,
|
|
18
14
|
Type,
|
|
19
15
|
Union,
|
|
16
|
+
cast,
|
|
20
17
|
)
|
|
21
18
|
|
|
22
19
|
from ..util import get_dependency
|
|
23
|
-
from .base import MovingWindowSupport, Storage
|
|
20
|
+
from .base import MovingWindowSupport, SlidingWindowCounterSupport, Storage
|
|
24
21
|
|
|
25
22
|
|
|
26
|
-
class MongoDBStorageBase(
|
|
23
|
+
class MongoDBStorageBase(
|
|
24
|
+
Storage, MovingWindowSupport, SlidingWindowCounterSupport, ABC
|
|
25
|
+
):
|
|
27
26
|
"""
|
|
28
27
|
Rate limit storage with MongoDB as backend.
|
|
29
28
|
|
|
@@ -48,7 +47,8 @@ class MongoDBStorageBase(Storage, MovingWindowSupport, ABC):
|
|
|
48
47
|
collections.
|
|
49
48
|
:param counter_collection_name: The collection name to use for individual counters
|
|
50
49
|
used in fixed window strategies
|
|
51
|
-
:param window_collection_name: The collection name to use for moving window
|
|
50
|
+
:param window_collection_name: The collection name to use for sliding & moving window
|
|
51
|
+
storage
|
|
52
52
|
:param wrap_exceptions: Whether to wrap storage exceptions in
|
|
53
53
|
:exc:`limits.errors.StorageError` before raising it.
|
|
54
54
|
:param options: all remaining keyword arguments are passed to the
|
|
@@ -98,7 +98,7 @@ class MongoDBStorageBase(Storage, MovingWindowSupport, ABC):
|
|
|
98
98
|
@property
|
|
99
99
|
def base_exceptions(
|
|
100
100
|
self,
|
|
101
|
-
) -> Union[Type[Exception],
|
|
101
|
+
) -> Union[Type[Exception], tuple[Type[Exception], ...]]: # pragma: no cover
|
|
102
102
|
return self.lib_errors.PyMongoError # type: ignore
|
|
103
103
|
|
|
104
104
|
def __initialize_database(self) -> None:
|
|
@@ -203,7 +203,7 @@ class MongoDBStorageBase(Storage, MovingWindowSupport, ABC):
|
|
|
203
203
|
except: # noqa: E722
|
|
204
204
|
return False
|
|
205
205
|
|
|
206
|
-
def get_moving_window(self, key: str, limit: int, expiry: int) ->
|
|
206
|
+
def get_moving_window(self, key: str, limit: int, expiry: int) -> tuple[float, int]:
|
|
207
207
|
"""
|
|
208
208
|
returns the starting point and the number of entries in the moving
|
|
209
209
|
window
|
|
@@ -257,9 +257,9 @@ class MongoDBStorageBase(Storage, MovingWindowSupport, ABC):
|
|
|
257
257
|
|
|
258
258
|
timestamp = time.time()
|
|
259
259
|
try:
|
|
260
|
-
updates:
|
|
260
|
+
updates: dict[
|
|
261
261
|
str,
|
|
262
|
-
|
|
262
|
+
dict[str, Union[datetime.datetime, dict[str, Union[list[float], int]]]],
|
|
263
263
|
] = {
|
|
264
264
|
"$push": {
|
|
265
265
|
"entries": {
|
|
@@ -289,6 +289,199 @@ class MongoDBStorageBase(Storage, MovingWindowSupport, ABC):
|
|
|
289
289
|
except self.lib.errors.DuplicateKeyError:
|
|
290
290
|
return False
|
|
291
291
|
|
|
292
|
+
def get_sliding_window(
|
|
293
|
+
self, key: str, expiry: int
|
|
294
|
+
) -> tuple[int, float, int, float]:
|
|
295
|
+
expiry_ms = expiry * 1000
|
|
296
|
+
if result := self.windows.find_one_and_update(
|
|
297
|
+
{"_id": key},
|
|
298
|
+
[
|
|
299
|
+
{
|
|
300
|
+
"$set": {
|
|
301
|
+
"previousCount": {
|
|
302
|
+
"$cond": {
|
|
303
|
+
"if": {
|
|
304
|
+
"$lte": [
|
|
305
|
+
{"$subtract": ["$expiresAt", "$$NOW"]},
|
|
306
|
+
expiry_ms,
|
|
307
|
+
]
|
|
308
|
+
},
|
|
309
|
+
"then": {"$ifNull": ["$currentCount", 0]},
|
|
310
|
+
"else": {"$ifNull": ["$previousCount", 0]},
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
"currentCount": {
|
|
314
|
+
"$cond": {
|
|
315
|
+
"if": {
|
|
316
|
+
"$lte": [
|
|
317
|
+
{"$subtract": ["$expiresAt", "$$NOW"]},
|
|
318
|
+
expiry_ms,
|
|
319
|
+
]
|
|
320
|
+
},
|
|
321
|
+
"then": 0,
|
|
322
|
+
"else": {"$ifNull": ["$currentCount", 0]},
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
"expiresAt": {
|
|
326
|
+
"$cond": {
|
|
327
|
+
"if": {
|
|
328
|
+
"$lte": [
|
|
329
|
+
{"$subtract": ["$expiresAt", "$$NOW"]},
|
|
330
|
+
expiry_ms,
|
|
331
|
+
]
|
|
332
|
+
},
|
|
333
|
+
"then": {
|
|
334
|
+
"$add": ["$expiresAt", expiry_ms],
|
|
335
|
+
},
|
|
336
|
+
"else": "$expiresAt",
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
],
|
|
342
|
+
return_document=self.lib.ReturnDocument.AFTER,
|
|
343
|
+
projection=["currentCount", "previousCount", "expiresAt"],
|
|
344
|
+
):
|
|
345
|
+
expires_at = (
|
|
346
|
+
(result["expiresAt"].replace(tzinfo=datetime.timezone.utc).timestamp())
|
|
347
|
+
if result.get("expiresAt")
|
|
348
|
+
else time.time()
|
|
349
|
+
)
|
|
350
|
+
current_ttl = max(0, expires_at - time.time())
|
|
351
|
+
prev_ttl = max(0, current_ttl - expiry if result["previousCount"] else 0)
|
|
352
|
+
|
|
353
|
+
return (
|
|
354
|
+
result["previousCount"],
|
|
355
|
+
prev_ttl,
|
|
356
|
+
result["currentCount"],
|
|
357
|
+
current_ttl,
|
|
358
|
+
)
|
|
359
|
+
return 0, 0.0, 0, 0.0
|
|
360
|
+
|
|
361
|
+
def acquire_sliding_window_entry(
|
|
362
|
+
self, key: str, limit: int, expiry: int, amount: int = 1
|
|
363
|
+
) -> bool:
|
|
364
|
+
expiry_ms = expiry * 1000
|
|
365
|
+
result = self.windows.find_one_and_update(
|
|
366
|
+
{"_id": key},
|
|
367
|
+
[
|
|
368
|
+
{
|
|
369
|
+
"$set": {
|
|
370
|
+
"previousCount": {
|
|
371
|
+
"$cond": {
|
|
372
|
+
"if": {
|
|
373
|
+
"$lte": [
|
|
374
|
+
{"$subtract": ["$expiresAt", "$$NOW"]},
|
|
375
|
+
expiry_ms,
|
|
376
|
+
]
|
|
377
|
+
},
|
|
378
|
+
"then": {"$ifNull": ["$currentCount", 0]},
|
|
379
|
+
"else": {"$ifNull": ["$previousCount", 0]},
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
}
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
"$set": {
|
|
386
|
+
"currentCount": {
|
|
387
|
+
"$cond": {
|
|
388
|
+
"if": {
|
|
389
|
+
"$lte": [
|
|
390
|
+
{"$subtract": ["$expiresAt", "$$NOW"]},
|
|
391
|
+
expiry_ms,
|
|
392
|
+
]
|
|
393
|
+
},
|
|
394
|
+
"then": 0,
|
|
395
|
+
"else": {"$ifNull": ["$currentCount", 0]},
|
|
396
|
+
}
|
|
397
|
+
},
|
|
398
|
+
"expiresAt": {
|
|
399
|
+
"$cond": {
|
|
400
|
+
"if": {
|
|
401
|
+
"$lte": [
|
|
402
|
+
{"$subtract": ["$expiresAt", "$$NOW"]},
|
|
403
|
+
expiry_ms,
|
|
404
|
+
]
|
|
405
|
+
},
|
|
406
|
+
"then": {
|
|
407
|
+
"$cond": {
|
|
408
|
+
"if": {"$gt": ["$expiresAt", 0]},
|
|
409
|
+
"then": {"$add": ["$expiresAt", expiry_ms]},
|
|
410
|
+
"else": {"$add": ["$$NOW", 2 * expiry_ms]},
|
|
411
|
+
}
|
|
412
|
+
},
|
|
413
|
+
"else": "$expiresAt",
|
|
414
|
+
}
|
|
415
|
+
},
|
|
416
|
+
}
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
"$set": {
|
|
420
|
+
"curWeightedCount": {
|
|
421
|
+
"$floor": {
|
|
422
|
+
"$add": [
|
|
423
|
+
{
|
|
424
|
+
"$multiply": [
|
|
425
|
+
"$previousCount",
|
|
426
|
+
{
|
|
427
|
+
"$divide": [
|
|
428
|
+
{
|
|
429
|
+
"$max": [
|
|
430
|
+
0,
|
|
431
|
+
{
|
|
432
|
+
"$subtract": [
|
|
433
|
+
"$expiresAt",
|
|
434
|
+
{
|
|
435
|
+
"$add": [
|
|
436
|
+
"$$NOW",
|
|
437
|
+
expiry_ms,
|
|
438
|
+
]
|
|
439
|
+
},
|
|
440
|
+
]
|
|
441
|
+
},
|
|
442
|
+
]
|
|
443
|
+
},
|
|
444
|
+
expiry_ms,
|
|
445
|
+
]
|
|
446
|
+
},
|
|
447
|
+
]
|
|
448
|
+
},
|
|
449
|
+
"$currentCount",
|
|
450
|
+
]
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
"$set": {
|
|
457
|
+
"currentCount": {
|
|
458
|
+
"$cond": {
|
|
459
|
+
"if": {
|
|
460
|
+
"$lte": [
|
|
461
|
+
{"$add": ["$curWeightedCount", amount]},
|
|
462
|
+
limit,
|
|
463
|
+
]
|
|
464
|
+
},
|
|
465
|
+
"then": {"$add": ["$currentCount", amount]},
|
|
466
|
+
"else": "$currentCount",
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
"$set": {
|
|
473
|
+
"_acquired": {
|
|
474
|
+
"$lte": [{"$add": ["$curWeightedCount", amount]}, limit]
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
},
|
|
478
|
+
{"$unset": ["curWeightedCount"]},
|
|
479
|
+
],
|
|
480
|
+
return_document=self.lib.ReturnDocument.AFTER,
|
|
481
|
+
upsert=True,
|
|
482
|
+
)
|
|
483
|
+
return cast(bool, result["_acquired"])
|
|
484
|
+
|
|
292
485
|
|
|
293
486
|
@versionadded(version="2.1")
|
|
294
487
|
@versionchanged(
|
limits/storage/redis.py
CHANGED
|
@@ -1,20 +1,31 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import time
|
|
4
|
-
from typing import TYPE_CHECKING
|
|
4
|
+
from typing import TYPE_CHECKING, cast
|
|
5
5
|
|
|
6
6
|
from packaging.version import Version
|
|
7
7
|
|
|
8
|
-
from limits.typing import Optional, RedisClient,
|
|
8
|
+
from limits.typing import Optional, RedisClient, Type, Union
|
|
9
9
|
|
|
10
10
|
from ..util import get_package_data
|
|
11
|
-
from .base import MovingWindowSupport, Storage
|
|
11
|
+
from .base import MovingWindowSupport, SlidingWindowCounterSupport, Storage
|
|
12
12
|
|
|
13
13
|
if TYPE_CHECKING:
|
|
14
14
|
import redis
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
class
|
|
17
|
+
class RedisStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
|
|
18
|
+
"""
|
|
19
|
+
Rate limit storage with redis as backend.
|
|
20
|
+
|
|
21
|
+
Depends on :pypi:`redis`.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
STORAGE_SCHEME = ["redis", "rediss", "redis+unix"]
|
|
25
|
+
"""The storage scheme for redis"""
|
|
26
|
+
|
|
27
|
+
DEPENDENCIES = {"redis": Version("3.0")}
|
|
28
|
+
|
|
18
29
|
RES_DIR = "resources/redis/lua_scripts"
|
|
19
30
|
|
|
20
31
|
SCRIPT_MOVING_WINDOW = get_package_data(f"{RES_DIR}/moving_window.lua")
|
|
@@ -24,123 +35,17 @@ class RedisInteractor:
|
|
|
24
35
|
SCRIPT_CLEAR_KEYS = get_package_data(f"{RES_DIR}/clear_keys.lua")
|
|
25
36
|
SCRIPT_INCR_EXPIRE = get_package_data(f"{RES_DIR}/incr_expire.lua")
|
|
26
37
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def prefixed_key(self, key: str) -> str:
|
|
33
|
-
return f"{self.PREFIX}:{key}"
|
|
34
|
-
|
|
35
|
-
def get_moving_window(self, key: str, limit: int, expiry: int) -> Tuple[float, int]:
|
|
36
|
-
"""
|
|
37
|
-
returns the starting point and the number of entries in the moving
|
|
38
|
-
window
|
|
39
|
-
|
|
40
|
-
:param key: rate limit key
|
|
41
|
-
:param expiry: expiry of entry
|
|
42
|
-
:return: (start of window, number of acquired entries)
|
|
43
|
-
"""
|
|
44
|
-
key = self.prefixed_key(key)
|
|
45
|
-
timestamp = time.time()
|
|
46
|
-
if window := self.lua_moving_window([key], [timestamp - expiry, limit]):
|
|
47
|
-
return float(window[0]), window[1]
|
|
48
|
-
|
|
49
|
-
return timestamp, 0
|
|
50
|
-
|
|
51
|
-
def _incr(
|
|
52
|
-
self,
|
|
53
|
-
key: str,
|
|
54
|
-
expiry: int,
|
|
55
|
-
connection: RedisClient,
|
|
56
|
-
elastic_expiry: bool = False,
|
|
57
|
-
amount: int = 1,
|
|
58
|
-
) -> int:
|
|
59
|
-
"""
|
|
60
|
-
increments the counter for a given rate limit key
|
|
61
|
-
|
|
62
|
-
:param connection: Redis connection
|
|
63
|
-
:param key: the key to increment
|
|
64
|
-
:param expiry: amount in seconds for the key to expire in
|
|
65
|
-
:param amount: the number to increment by
|
|
66
|
-
"""
|
|
67
|
-
key = self.prefixed_key(key)
|
|
68
|
-
value = connection.incrby(key, amount)
|
|
69
|
-
|
|
70
|
-
if elastic_expiry or value == amount:
|
|
71
|
-
connection.expire(key, expiry)
|
|
72
|
-
|
|
73
|
-
return value
|
|
74
|
-
|
|
75
|
-
def _get(self, key: str, connection: RedisClient) -> int:
|
|
76
|
-
"""
|
|
77
|
-
:param connection: Redis connection
|
|
78
|
-
:param key: the key to get the counter value for
|
|
79
|
-
"""
|
|
80
|
-
|
|
81
|
-
key = self.prefixed_key(key)
|
|
82
|
-
return int(connection.get(key) or 0)
|
|
83
|
-
|
|
84
|
-
def _clear(self, key: str, connection: RedisClient) -> None:
|
|
85
|
-
"""
|
|
86
|
-
:param key: the key to clear rate limits for
|
|
87
|
-
:param connection: Redis connection
|
|
88
|
-
"""
|
|
89
|
-
key = self.prefixed_key(key)
|
|
90
|
-
connection.delete(key)
|
|
91
|
-
|
|
92
|
-
def _acquire_entry(
|
|
93
|
-
self,
|
|
94
|
-
key: str,
|
|
95
|
-
limit: int,
|
|
96
|
-
expiry: int,
|
|
97
|
-
connection: RedisClient,
|
|
98
|
-
amount: int = 1,
|
|
99
|
-
) -> bool:
|
|
100
|
-
"""
|
|
101
|
-
:param key: rate limit key to acquire an entry in
|
|
102
|
-
:param limit: amount of entries allowed
|
|
103
|
-
:param expiry: expiry of the entry
|
|
104
|
-
:param connection: Redis connection
|
|
105
|
-
:param amount: the number of entries to acquire
|
|
106
|
-
"""
|
|
107
|
-
key = self.prefixed_key(key)
|
|
108
|
-
timestamp = time.time()
|
|
109
|
-
acquired = self.lua_acquire_window([key], [timestamp, limit, expiry, amount])
|
|
110
|
-
|
|
111
|
-
return bool(acquired)
|
|
112
|
-
|
|
113
|
-
def _get_expiry(self, key: str, connection: RedisClient) -> float:
|
|
114
|
-
"""
|
|
115
|
-
:param key: the key to get the expiry for
|
|
116
|
-
:param connection: Redis connection
|
|
117
|
-
"""
|
|
118
|
-
|
|
119
|
-
key = self.prefixed_key(key)
|
|
120
|
-
return max(connection.ttl(key), 0) + time.time()
|
|
121
|
-
|
|
122
|
-
def _check(self, connection: RedisClient) -> bool:
|
|
123
|
-
"""
|
|
124
|
-
:param connection: Redis connection
|
|
125
|
-
check if storage is healthy
|
|
126
|
-
"""
|
|
127
|
-
try:
|
|
128
|
-
return connection.ping()
|
|
129
|
-
except: # noqa
|
|
130
|
-
return False
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
class RedisStorage(RedisInteractor, Storage, MovingWindowSupport):
|
|
134
|
-
"""
|
|
135
|
-
Rate limit storage with redis as backend.
|
|
136
|
-
|
|
137
|
-
Depends on :pypi:`redis`.
|
|
138
|
-
"""
|
|
38
|
+
SCRIPT_SLIDING_WINDOW = get_package_data(f"{RES_DIR}/sliding_window.lua")
|
|
39
|
+
SCRIPT_ACQUIRE_SLIDING_WINDOW = get_package_data(
|
|
40
|
+
f"{RES_DIR}/acquire_sliding_window.lua"
|
|
41
|
+
)
|
|
139
42
|
|
|
140
|
-
|
|
141
|
-
"
|
|
43
|
+
lua_moving_window: "redis.commands.core.Script"
|
|
44
|
+
lua_acquire_moving_window: "redis.commands.core.Script"
|
|
45
|
+
lua_sliding_window: "redis.commands.core.Script"
|
|
46
|
+
lua_acquire_sliding_window: "redis.commands.core.Script"
|
|
142
47
|
|
|
143
|
-
|
|
48
|
+
PREFIX = "LIMITS"
|
|
144
49
|
|
|
145
50
|
def __init__(
|
|
146
51
|
self,
|
|
@@ -179,73 +84,189 @@ class RedisStorage(RedisInteractor, Storage, MovingWindowSupport):
|
|
|
179
84
|
@property
|
|
180
85
|
def base_exceptions(
|
|
181
86
|
self,
|
|
182
|
-
) -> Union[Type[Exception],
|
|
87
|
+
) -> Union[Type[Exception], tuple[Type[Exception], ...]]: # pragma: no cover
|
|
183
88
|
return self.dependency.RedisError # type: ignore[no-any-return]
|
|
184
89
|
|
|
185
90
|
def initialize_storage(self, _uri: str) -> None:
|
|
186
|
-
self.lua_moving_window = self.
|
|
187
|
-
|
|
91
|
+
self.lua_moving_window = self.get_connection().register_script(
|
|
92
|
+
self.SCRIPT_MOVING_WINDOW
|
|
93
|
+
)
|
|
94
|
+
self.lua_acquire_moving_window = self.get_connection().register_script(
|
|
188
95
|
self.SCRIPT_ACQUIRE_MOVING_WINDOW
|
|
189
96
|
)
|
|
190
|
-
self.lua_clear_keys = self.
|
|
191
|
-
|
|
192
|
-
|
|
97
|
+
self.lua_clear_keys = self.get_connection().register_script(
|
|
98
|
+
self.SCRIPT_CLEAR_KEYS
|
|
99
|
+
)
|
|
100
|
+
self.lua_incr_expire = self.get_connection().register_script(
|
|
101
|
+
self.SCRIPT_INCR_EXPIRE
|
|
102
|
+
)
|
|
103
|
+
self.lua_sliding_window = self.get_connection().register_script(
|
|
104
|
+
self.SCRIPT_SLIDING_WINDOW
|
|
193
105
|
)
|
|
106
|
+
self.lua_acquire_sliding_window = self.get_connection().register_script(
|
|
107
|
+
self.SCRIPT_ACQUIRE_SLIDING_WINDOW
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def get_connection(self, readonly: bool = False) -> RedisClient:
|
|
111
|
+
return cast(RedisClient, self.storage)
|
|
112
|
+
|
|
113
|
+
def _current_window_key(self, key: str) -> str:
|
|
114
|
+
"""
|
|
115
|
+
Return the current window's storage key (Sliding window strategy)
|
|
116
|
+
|
|
117
|
+
Contrary to other strategies that have one key per rate limit item,
|
|
118
|
+
this strategy has two keys per rate limit item than must be on the same machine.
|
|
119
|
+
To keep the current key and the previous key on the same Redis cluster node,
|
|
120
|
+
curly braces are added.
|
|
121
|
+
|
|
122
|
+
Eg: "{constructed_key}"
|
|
123
|
+
"""
|
|
124
|
+
return f"{{{key}}}"
|
|
125
|
+
|
|
126
|
+
def _previous_window_key(self, key: str) -> str:
|
|
127
|
+
"""
|
|
128
|
+
Return the previous window's storage key (Sliding window strategy).
|
|
129
|
+
|
|
130
|
+
Curvy braces are added on the common pattern with the current window's key,
|
|
131
|
+
so the current and the previous key are stored on the same Redis cluster node.
|
|
132
|
+
|
|
133
|
+
Eg: "{constructed_key}/-1"
|
|
134
|
+
"""
|
|
135
|
+
return f"{self._current_window_key(key)}/-1"
|
|
136
|
+
|
|
137
|
+
def prefixed_key(self, key: str) -> str:
|
|
138
|
+
return f"{self.PREFIX}:{key}"
|
|
139
|
+
|
|
140
|
+
def get_moving_window(self, key: str, limit: int, expiry: int) -> tuple[float, int]:
|
|
141
|
+
"""
|
|
142
|
+
returns the starting point and the number of entries in the moving
|
|
143
|
+
window
|
|
144
|
+
|
|
145
|
+
:param key: rate limit key
|
|
146
|
+
:param expiry: expiry of entry
|
|
147
|
+
:return: (start of window, number of acquired entries)
|
|
148
|
+
"""
|
|
149
|
+
key = self.prefixed_key(key)
|
|
150
|
+
timestamp = time.time()
|
|
151
|
+
if window := self.lua_moving_window([key], [timestamp - expiry, limit]):
|
|
152
|
+
return float(window[0]), window[1]
|
|
153
|
+
|
|
154
|
+
return timestamp, 0
|
|
155
|
+
|
|
156
|
+
def get_sliding_window(
|
|
157
|
+
self, key: str, expiry: int
|
|
158
|
+
) -> tuple[int, float, int, float]:
|
|
159
|
+
previous_key = self.prefixed_key(self._previous_window_key(key))
|
|
160
|
+
current_key = self.prefixed_key(self._current_window_key(key))
|
|
161
|
+
if window := self.lua_sliding_window([previous_key, current_key], [expiry]):
|
|
162
|
+
return (
|
|
163
|
+
int(window[0] or 0),
|
|
164
|
+
max(0, float(window[1] or 0)) / 1000,
|
|
165
|
+
int(window[2] or 0),
|
|
166
|
+
max(0, float(window[3] or 0)) / 1000,
|
|
167
|
+
)
|
|
168
|
+
return 0, 0.0, 0, 0.0
|
|
194
169
|
|
|
195
170
|
def incr(
|
|
196
|
-
self,
|
|
171
|
+
self,
|
|
172
|
+
key: str,
|
|
173
|
+
expiry: int,
|
|
174
|
+
elastic_expiry: bool = False,
|
|
175
|
+
amount: int = 1,
|
|
197
176
|
) -> int:
|
|
198
177
|
"""
|
|
199
178
|
increments the counter for a given rate limit key
|
|
200
179
|
|
|
180
|
+
|
|
201
181
|
:param key: the key to increment
|
|
202
182
|
:param expiry: amount in seconds for the key to expire in
|
|
203
183
|
:param amount: the number to increment by
|
|
204
184
|
"""
|
|
205
|
-
|
|
185
|
+
key = self.prefixed_key(key)
|
|
206
186
|
if elastic_expiry:
|
|
207
|
-
|
|
187
|
+
value = self.get_connection().incrby(key, amount)
|
|
188
|
+
self.get_connection().expire(key, expiry)
|
|
189
|
+
return value
|
|
208
190
|
else:
|
|
209
|
-
key = self.prefixed_key(key)
|
|
210
191
|
return int(self.lua_incr_expire([key], [expiry, amount]))
|
|
211
192
|
|
|
212
193
|
def get(self, key: str) -> int:
|
|
213
194
|
"""
|
|
195
|
+
|
|
214
196
|
:param key: the key to get the counter value for
|
|
215
197
|
"""
|
|
216
198
|
|
|
217
|
-
|
|
199
|
+
key = self.prefixed_key(key)
|
|
200
|
+
return int(self.get_connection(True).get(key) or 0)
|
|
218
201
|
|
|
219
202
|
def clear(self, key: str) -> None:
|
|
220
203
|
"""
|
|
221
204
|
:param key: the key to clear rate limits for
|
|
222
205
|
"""
|
|
206
|
+
key = self.prefixed_key(key)
|
|
207
|
+
self.get_connection().delete(key)
|
|
223
208
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
209
|
+
def acquire_entry(
|
|
210
|
+
self,
|
|
211
|
+
key: str,
|
|
212
|
+
limit: int,
|
|
213
|
+
expiry: int,
|
|
214
|
+
amount: int = 1,
|
|
215
|
+
) -> bool:
|
|
227
216
|
"""
|
|
228
217
|
:param key: rate limit key to acquire an entry in
|
|
229
218
|
:param limit: amount of entries allowed
|
|
230
219
|
:param expiry: expiry of the entry
|
|
231
|
-
|
|
220
|
+
|
|
221
|
+
:param amount: the number of entries to acquire
|
|
232
222
|
"""
|
|
223
|
+
key = self.prefixed_key(key)
|
|
224
|
+
timestamp = time.time()
|
|
225
|
+
acquired = self.lua_acquire_moving_window(
|
|
226
|
+
[key], [timestamp, limit, expiry, amount]
|
|
227
|
+
)
|
|
233
228
|
|
|
234
|
-
return
|
|
229
|
+
return bool(acquired)
|
|
230
|
+
|
|
231
|
+
def acquire_sliding_window_entry(
|
|
232
|
+
self,
|
|
233
|
+
key: str,
|
|
234
|
+
limit: int,
|
|
235
|
+
expiry: int,
|
|
236
|
+
amount: int = 1,
|
|
237
|
+
) -> bool:
|
|
238
|
+
"""
|
|
239
|
+
Acquire an entry. Shift the current window to the previous window if it expired.
|
|
240
|
+
|
|
241
|
+
:param key: rate limit key to acquire an entry in
|
|
242
|
+
:param limit: amount of entries allowed
|
|
243
|
+
:param expiry: expiry of the entry
|
|
244
|
+
:param amount: the number of entries to acquire
|
|
245
|
+
"""
|
|
246
|
+
previous_key = self.prefixed_key(self._previous_window_key(key))
|
|
247
|
+
current_key = self.prefixed_key(self._current_window_key(key))
|
|
248
|
+
acquired = self.lua_acquire_sliding_window(
|
|
249
|
+
[previous_key, current_key], [limit, expiry, amount]
|
|
250
|
+
)
|
|
251
|
+
return bool(acquired)
|
|
235
252
|
|
|
236
253
|
def get_expiry(self, key: str) -> float:
|
|
237
254
|
"""
|
|
238
255
|
:param key: the key to get the expiry for
|
|
256
|
+
|
|
239
257
|
"""
|
|
240
258
|
|
|
241
|
-
|
|
259
|
+
key = self.prefixed_key(key)
|
|
260
|
+
return max(self.get_connection(True).ttl(key), 0) + time.time()
|
|
242
261
|
|
|
243
262
|
def check(self) -> bool:
|
|
244
263
|
"""
|
|
245
264
|
check if storage is healthy
|
|
246
265
|
"""
|
|
247
|
-
|
|
248
|
-
|
|
266
|
+
try:
|
|
267
|
+
return self.get_connection().ping()
|
|
268
|
+
except: # noqa
|
|
269
|
+
return False
|
|
249
270
|
|
|
250
271
|
def reset(self) -> Optional[int]:
|
|
251
272
|
"""
|
limits/storage/redis_cluster.py
CHANGED
|
@@ -4,7 +4,7 @@ from deprecated.sphinx import versionchanged
|
|
|
4
4
|
from packaging.version import Version
|
|
5
5
|
|
|
6
6
|
from limits.storage.redis import RedisStorage
|
|
7
|
-
from limits.typing import
|
|
7
|
+
from limits.typing import Optional, Union
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
@versionchanged(
|
|
@@ -34,7 +34,7 @@ class RedisClusterStorage(RedisStorage):
|
|
|
34
34
|
STORAGE_SCHEME = ["redis+cluster"]
|
|
35
35
|
"""The storage scheme for redis cluster"""
|
|
36
36
|
|
|
37
|
-
DEFAULT_OPTIONS:
|
|
37
|
+
DEFAULT_OPTIONS: dict[str, Union[float, str, bool]] = {
|
|
38
38
|
"max_connections": 1000,
|
|
39
39
|
}
|
|
40
40
|
"Default options passed to the :class:`~redis.cluster.RedisCluster`"
|
|
@@ -60,7 +60,7 @@ class RedisClusterStorage(RedisStorage):
|
|
|
60
60
|
available or if the redis cluster cannot be reached.
|
|
61
61
|
"""
|
|
62
62
|
parsed = urllib.parse.urlparse(uri)
|
|
63
|
-
parsed_auth:
|
|
63
|
+
parsed_auth: dict[str, Union[float, str, bool]] = {}
|
|
64
64
|
|
|
65
65
|
if parsed.username:
|
|
66
66
|
parsed_auth["username"] = parsed.username
|