limits 4.7.3__py3-none-any.whl → 5.0.0rc2__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 +0 -2
- limits/aio/storage/base.py +1 -5
- limits/aio/storage/memcached/__init__.py +184 -0
- limits/aio/storage/memcached/bridge.py +73 -0
- limits/aio/storage/memcached/emcache.py +112 -0
- limits/aio/storage/memcached/memcachio.py +104 -0
- limits/aio/storage/memory.py +41 -48
- limits/aio/storage/mongodb.py +26 -31
- limits/aio/storage/redis/__init__.py +2 -4
- limits/aio/storage/redis/bridge.py +0 -1
- limits/aio/storage/redis/coredis.py +2 -6
- limits/aio/storage/redis/redispy.py +1 -8
- limits/aio/strategies.py +1 -28
- limits/resources/redis/lua_scripts/acquire_moving_window.lua +5 -2
- limits/resources/redis/lua_scripts/moving_window.lua +23 -14
- limits/storage/__init__.py +0 -2
- limits/storage/base.py +1 -5
- limits/storage/memcached.py +8 -29
- limits/storage/memory.py +16 -35
- limits/storage/mongodb.py +25 -34
- limits/storage/redis.py +1 -7
- limits/strategies.py +1 -31
- limits/typing.py +1 -50
- {limits-4.7.3.dist-info → limits-5.0.0rc2.dist-info}/METADATA +8 -14
- limits-5.0.0rc2.dist-info/RECORD +44 -0
- limits/aio/storage/etcd.py +0 -146
- limits/aio/storage/memcached.py +0 -281
- limits/storage/etcd.py +0 -139
- limits-4.7.3.dist-info/RECORD +0 -43
- {limits-4.7.3.dist-info → limits-5.0.0rc2.dist-info}/WHEEL +0 -0
- {limits-4.7.3.dist-info → limits-5.0.0rc2.dist-info}/licenses/LICENSE.txt +0 -0
- {limits-4.7.3.dist-info → limits-5.0.0rc2.dist-info}/top_level.txt +0 -0
limits/aio/storage/mongodb.py
CHANGED
|
@@ -169,16 +169,12 @@ class MongoDBStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
|
|
|
169
169
|
|
|
170
170
|
return counter and counter["count"] or 0
|
|
171
171
|
|
|
172
|
-
async def incr(
|
|
173
|
-
self, key: str, expiry: int, elastic_expiry: bool = False, amount: int = 1
|
|
174
|
-
) -> int:
|
|
172
|
+
async def incr(self, key: str, expiry: int, amount: int = 1) -> int:
|
|
175
173
|
"""
|
|
176
174
|
increments the counter for a given rate limit key
|
|
177
175
|
|
|
178
176
|
:param key: the key to increment
|
|
179
177
|
:param expiry: amount in seconds for the key to expire in
|
|
180
|
-
:param elastic_expiry: whether to keep extending the rate limit
|
|
181
|
-
window every hit.
|
|
182
178
|
:param amount: the number to increment by
|
|
183
179
|
"""
|
|
184
180
|
await self.create_indices()
|
|
@@ -205,7 +201,7 @@ class MongoDBStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
|
|
|
205
201
|
"$cond": {
|
|
206
202
|
"if": {"$lt": ["$expireAt", "$$NOW"]},
|
|
207
203
|
"then": expiration,
|
|
208
|
-
"else":
|
|
204
|
+
"else": "$expireAt",
|
|
209
205
|
}
|
|
210
206
|
},
|
|
211
207
|
}
|
|
@@ -241,15 +237,16 @@ class MongoDBStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
|
|
|
241
237
|
:param int expiry: expiry of entry
|
|
242
238
|
:return: (start of window, number of acquired entries)
|
|
243
239
|
"""
|
|
240
|
+
|
|
244
241
|
timestamp = time.time()
|
|
245
|
-
if
|
|
246
|
-
await self.database[self.__collection_mapping["windows"]]
|
|
242
|
+
if (
|
|
243
|
+
result := await self.database[self.__collection_mapping["windows"]]
|
|
247
244
|
.aggregate(
|
|
248
245
|
[
|
|
249
246
|
{"$match": {"_id": key}},
|
|
250
247
|
{
|
|
251
248
|
"$project": {
|
|
252
|
-
"
|
|
249
|
+
"filteredEntries": {
|
|
253
250
|
"$filter": {
|
|
254
251
|
"input": "$entries",
|
|
255
252
|
"as": "entry",
|
|
@@ -258,12 +255,10 @@ class MongoDBStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
|
|
|
258
255
|
}
|
|
259
256
|
}
|
|
260
257
|
},
|
|
261
|
-
{"$unwind": "$entries"},
|
|
262
258
|
{
|
|
263
|
-
"$
|
|
264
|
-
"
|
|
265
|
-
"
|
|
266
|
-
"count": {"$sum": 1},
|
|
259
|
+
"$project": {
|
|
260
|
+
"min": {"$min": "$filteredEntries"},
|
|
261
|
+
"count": {"$size": "$filteredEntries"},
|
|
267
262
|
}
|
|
268
263
|
},
|
|
269
264
|
]
|
|
@@ -337,7 +332,7 @@ class MongoDBStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
|
|
|
337
332
|
"$cond": {
|
|
338
333
|
"if": {
|
|
339
334
|
"$lte": [
|
|
340
|
-
{"$subtract": ["$
|
|
335
|
+
{"$subtract": ["$expireAt", "$$NOW"]},
|
|
341
336
|
expiry_ms,
|
|
342
337
|
]
|
|
343
338
|
},
|
|
@@ -353,7 +348,7 @@ class MongoDBStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
|
|
|
353
348
|
"$cond": {
|
|
354
349
|
"if": {
|
|
355
350
|
"$lte": [
|
|
356
|
-
{"$subtract": ["$
|
|
351
|
+
{"$subtract": ["$expireAt", "$$NOW"]},
|
|
357
352
|
expiry_ms,
|
|
358
353
|
]
|
|
359
354
|
},
|
|
@@ -361,22 +356,22 @@ class MongoDBStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
|
|
|
361
356
|
"else": {"$ifNull": ["$currentCount", 0]},
|
|
362
357
|
}
|
|
363
358
|
},
|
|
364
|
-
"
|
|
359
|
+
"expireAt": {
|
|
365
360
|
"$cond": {
|
|
366
361
|
"if": {
|
|
367
362
|
"$lte": [
|
|
368
|
-
{"$subtract": ["$
|
|
363
|
+
{"$subtract": ["$expireAt", "$$NOW"]},
|
|
369
364
|
expiry_ms,
|
|
370
365
|
]
|
|
371
366
|
},
|
|
372
367
|
"then": {
|
|
373
368
|
"$cond": {
|
|
374
|
-
"if": {"$gt": ["$
|
|
375
|
-
"then": {"$add": ["$
|
|
369
|
+
"if": {"$gt": ["$expireAt", 0]},
|
|
370
|
+
"then": {"$add": ["$expireAt", expiry_ms]},
|
|
376
371
|
"else": {"$add": ["$$NOW", 2 * expiry_ms]},
|
|
377
372
|
}
|
|
378
373
|
},
|
|
379
|
-
"else": "$
|
|
374
|
+
"else": "$expireAt",
|
|
380
375
|
}
|
|
381
376
|
},
|
|
382
377
|
}
|
|
@@ -396,7 +391,7 @@ class MongoDBStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
|
|
|
396
391
|
0,
|
|
397
392
|
{
|
|
398
393
|
"$subtract": [
|
|
399
|
-
"$
|
|
394
|
+
"$expireAt",
|
|
400
395
|
{
|
|
401
396
|
"$add": [
|
|
402
397
|
"$$NOW",
|
|
@@ -464,7 +459,7 @@ class MongoDBStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
|
|
|
464
459
|
"$cond": {
|
|
465
460
|
"if": {
|
|
466
461
|
"$lte": [
|
|
467
|
-
{"$subtract": ["$
|
|
462
|
+
{"$subtract": ["$expireAt", "$$NOW"]},
|
|
468
463
|
expiry_ms,
|
|
469
464
|
]
|
|
470
465
|
},
|
|
@@ -476,7 +471,7 @@ class MongoDBStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
|
|
|
476
471
|
"$cond": {
|
|
477
472
|
"if": {
|
|
478
473
|
"$lte": [
|
|
479
|
-
{"$subtract": ["$
|
|
474
|
+
{"$subtract": ["$expireAt", "$$NOW"]},
|
|
480
475
|
expiry_ms,
|
|
481
476
|
]
|
|
482
477
|
},
|
|
@@ -484,27 +479,27 @@ class MongoDBStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
|
|
|
484
479
|
"else": {"$ifNull": ["$currentCount", 0]},
|
|
485
480
|
}
|
|
486
481
|
},
|
|
487
|
-
"
|
|
482
|
+
"expireAt": {
|
|
488
483
|
"$cond": {
|
|
489
484
|
"if": {
|
|
490
485
|
"$lte": [
|
|
491
|
-
{"$subtract": ["$
|
|
486
|
+
{"$subtract": ["$expireAt", "$$NOW"]},
|
|
492
487
|
expiry_ms,
|
|
493
488
|
]
|
|
494
489
|
},
|
|
495
|
-
"then": {"$add": ["$
|
|
496
|
-
"else": "$
|
|
490
|
+
"then": {"$add": ["$expireAt", expiry_ms]},
|
|
491
|
+
"else": "$expireAt",
|
|
497
492
|
}
|
|
498
493
|
},
|
|
499
494
|
}
|
|
500
495
|
}
|
|
501
496
|
],
|
|
502
497
|
return_document=self.proxy_dependency.module.ReturnDocument.AFTER,
|
|
503
|
-
projection=["currentCount", "previousCount", "
|
|
498
|
+
projection=["currentCount", "previousCount", "expireAt"],
|
|
504
499
|
):
|
|
505
500
|
expires_at = (
|
|
506
|
-
(result["
|
|
507
|
-
if result.get("
|
|
501
|
+
(result["expireAt"].replace(tzinfo=datetime.timezone.utc).timestamp())
|
|
502
|
+
if result.get("expireAt")
|
|
508
503
|
else time.time()
|
|
509
504
|
)
|
|
510
505
|
current_ttl = max(0, expires_at - time.time())
|
|
@@ -139,9 +139,7 @@ class RedisStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
|
|
|
139
139
|
) -> type[Exception] | tuple[type[Exception], ...]: # pragma: no cover
|
|
140
140
|
return self.bridge.base_exceptions
|
|
141
141
|
|
|
142
|
-
async def incr(
|
|
143
|
-
self, key: str, expiry: int, elastic_expiry: bool = False, amount: int = 1
|
|
144
|
-
) -> int:
|
|
142
|
+
async def incr(self, key: str, expiry: int, amount: int = 1) -> int:
|
|
145
143
|
"""
|
|
146
144
|
increments the counter for a given rate limit key
|
|
147
145
|
|
|
@@ -150,7 +148,7 @@ class RedisStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
|
|
|
150
148
|
:param amount: the number to increment by
|
|
151
149
|
"""
|
|
152
150
|
|
|
153
|
-
return await self.bridge.incr(key, expiry,
|
|
151
|
+
return await self.bridge.incr(key, expiry, amount)
|
|
154
152
|
|
|
155
153
|
async def get(self, key: str) -> int:
|
|
156
154
|
"""
|
|
@@ -112,14 +112,10 @@ class CoredisBridge(RedisBridge):
|
|
|
112
112
|
self.SCRIPT_ACQUIRE_SLIDING_WINDOW
|
|
113
113
|
)
|
|
114
114
|
|
|
115
|
-
async def incr(
|
|
116
|
-
self, key: str, expiry: int, elastic_expiry: bool = False, amount: int = 1
|
|
117
|
-
) -> int:
|
|
115
|
+
async def incr(self, key: str, expiry: int, amount: int = 1) -> int:
|
|
118
116
|
key = self.prefixed_key(key)
|
|
119
|
-
value
|
|
120
|
-
if elastic_expiry or value == amount:
|
|
117
|
+
if (value := await self.get_connection().incrby(key, amount)) == amount:
|
|
121
118
|
await self.get_connection().expire(key, expiry)
|
|
122
|
-
|
|
123
119
|
return value
|
|
124
120
|
|
|
125
121
|
async def get(self, key: str) -> int:
|
|
@@ -119,7 +119,6 @@ class RedispyBridge(RedisBridge):
|
|
|
119
119
|
self,
|
|
120
120
|
key: str,
|
|
121
121
|
expiry: int,
|
|
122
|
-
elastic_expiry: bool = False,
|
|
123
122
|
amount: int = 1,
|
|
124
123
|
) -> int:
|
|
125
124
|
"""
|
|
@@ -131,13 +130,7 @@ class RedispyBridge(RedisBridge):
|
|
|
131
130
|
:param amount: the number to increment by
|
|
132
131
|
"""
|
|
133
132
|
key = self.prefixed_key(key)
|
|
134
|
-
|
|
135
|
-
if elastic_expiry:
|
|
136
|
-
value = await self.get_connection().incrby(key, amount)
|
|
137
|
-
await self.get_connection().expire(key, expiry)
|
|
138
|
-
return value
|
|
139
|
-
else:
|
|
140
|
-
return cast(int, await self.lua_incr_expire([key], [expiry, amount]))
|
|
133
|
+
return cast(int, await self.lua_incr_expire([key], [expiry, amount]))
|
|
141
134
|
|
|
142
135
|
async def get(self, key: str) -> int:
|
|
143
136
|
"""
|
limits/aio/strategies.py
CHANGED
|
@@ -8,7 +8,7 @@ import time
|
|
|
8
8
|
from abc import ABC, abstractmethod
|
|
9
9
|
from math import floor, inf
|
|
10
10
|
|
|
11
|
-
from deprecated.sphinx import
|
|
11
|
+
from deprecated.sphinx import versionadded
|
|
12
12
|
|
|
13
13
|
from ..limits import RateLimitItem
|
|
14
14
|
from ..storage import StorageTypes
|
|
@@ -150,7 +150,6 @@ class FixedWindowRateLimiter(RateLimiter):
|
|
|
150
150
|
await self.storage.incr(
|
|
151
151
|
item.key_for(*identifiers),
|
|
152
152
|
item.get_expiry(),
|
|
153
|
-
elastic_expiry=False,
|
|
154
153
|
amount=cost,
|
|
155
154
|
)
|
|
156
155
|
<= item.amount
|
|
@@ -304,34 +303,8 @@ class SlidingWindowCounterRateLimiter(RateLimiter):
|
|
|
304
303
|
return WindowStats(now + min(previous_reset_in, current_reset_in), remaining)
|
|
305
304
|
|
|
306
305
|
|
|
307
|
-
@deprecated(version="4.1")
|
|
308
|
-
class FixedWindowElasticExpiryRateLimiter(FixedWindowRateLimiter):
|
|
309
|
-
"""
|
|
310
|
-
Reference: :ref:`strategies:fixed window with elastic expiry`
|
|
311
|
-
"""
|
|
312
|
-
|
|
313
|
-
async def hit(self, item: RateLimitItem, *identifiers: str, cost: int = 1) -> bool:
|
|
314
|
-
"""
|
|
315
|
-
Consume the rate limit
|
|
316
|
-
|
|
317
|
-
:param item: a :class:`limits.limits.RateLimitItem` instance
|
|
318
|
-
:param identifiers: variable list of strings to uniquely identify the
|
|
319
|
-
limit
|
|
320
|
-
:param cost: The cost of this hit, default 1
|
|
321
|
-
"""
|
|
322
|
-
amount = await self.storage.incr(
|
|
323
|
-
item.key_for(*identifiers),
|
|
324
|
-
item.get_expiry(),
|
|
325
|
-
elastic_expiry=True,
|
|
326
|
-
amount=cost,
|
|
327
|
-
)
|
|
328
|
-
|
|
329
|
-
return amount <= item.amount
|
|
330
|
-
|
|
331
|
-
|
|
332
306
|
STRATEGIES = {
|
|
333
307
|
"sliding-window-counter": SlidingWindowCounterRateLimiter,
|
|
334
308
|
"fixed-window": FixedWindowRateLimiter,
|
|
335
|
-
"fixed-window-elastic-expiry": FixedWindowElasticExpiryRateLimiter,
|
|
336
309
|
"moving-window": MovingWindowRateLimiter,
|
|
337
310
|
}
|
|
@@ -12,11 +12,14 @@ local entry = redis.call('lindex', KEYS[1], limit - amount)
|
|
|
12
12
|
if entry and tonumber(entry) >= timestamp - expiry then
|
|
13
13
|
return false
|
|
14
14
|
end
|
|
15
|
-
|
|
15
|
+
local entries = {}
|
|
16
16
|
for i = 1, amount do
|
|
17
|
-
|
|
17
|
+
entries[i] = timestamp
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
+
for i=1,#entries,5000 do
|
|
21
|
+
redis.call('lpush', KEYS[1], unpack(entries, i, math.min(i+4999, #entries)))
|
|
22
|
+
end
|
|
20
23
|
redis.call('ltrim', KEYS[1], 0, limit - 1)
|
|
21
24
|
redis.call('expire', KEYS[1], expiry)
|
|
22
25
|
|
|
@@ -1,21 +1,30 @@
|
|
|
1
|
-
local
|
|
1
|
+
local len = tonumber(ARGV[2])
|
|
2
2
|
local expiry = tonumber(ARGV[1])
|
|
3
|
-
local a = 0
|
|
4
|
-
local oldest = nil
|
|
5
3
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
-- Binary search to find the oldest valid entry in the window
|
|
5
|
+
local function oldest_entry(high, target)
|
|
6
|
+
local low = 0
|
|
7
|
+
local result = nil
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
while low <= high do
|
|
10
|
+
local mid = math.floor((low + high) / 2)
|
|
11
|
+
local val = tonumber(redis.call('lindex', KEYS[1], mid))
|
|
12
|
+
|
|
13
|
+
if val and val >= target then
|
|
14
|
+
result = mid
|
|
15
|
+
low = mid + 1
|
|
16
|
+
else
|
|
17
|
+
high = mid - 1
|
|
13
18
|
end
|
|
14
|
-
else
|
|
15
|
-
break
|
|
16
19
|
end
|
|
20
|
+
|
|
21
|
+
return result
|
|
17
22
|
end
|
|
18
23
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
local index = oldest_entry(len - 1, expiry)
|
|
25
|
+
|
|
26
|
+
if index then
|
|
27
|
+
local count = index + 1
|
|
28
|
+
local oldest = tonumber(redis.call('lindex', KEYS[1], index))
|
|
29
|
+
return {tostring(oldest), count}
|
|
30
|
+
end
|
limits/storage/__init__.py
CHANGED
|
@@ -12,7 +12,6 @@ import limits # noqa
|
|
|
12
12
|
from ..errors import ConfigurationError
|
|
13
13
|
from ..typing import TypeAlias, cast
|
|
14
14
|
from .base import MovingWindowSupport, SlidingWindowCounterSupport, Storage
|
|
15
|
-
from .etcd import EtcdStorage
|
|
16
15
|
from .memcached import MemcachedStorage
|
|
17
16
|
from .memory import MemoryStorage
|
|
18
17
|
from .mongodb import MongoDBStorage, MongoDBStorageBase
|
|
@@ -67,7 +66,6 @@ def storage_from_string(
|
|
|
67
66
|
|
|
68
67
|
|
|
69
68
|
__all__ = [
|
|
70
|
-
"EtcdStorage",
|
|
71
69
|
"MemcachedStorage",
|
|
72
70
|
"MemoryStorage",
|
|
73
71
|
"MongoDBStorage",
|
limits/storage/base.py
CHANGED
|
@@ -71,16 +71,12 @@ class Storage(LazyDependency, metaclass=StorageRegistry):
|
|
|
71
71
|
raise NotImplementedError
|
|
72
72
|
|
|
73
73
|
@abstractmethod
|
|
74
|
-
def incr(
|
|
75
|
-
self, key: str, expiry: int, elastic_expiry: bool = False, amount: int = 1
|
|
76
|
-
) -> int:
|
|
74
|
+
def incr(self, key: str, expiry: int, amount: int = 1) -> int:
|
|
77
75
|
"""
|
|
78
76
|
increments the counter for a given rate limit key
|
|
79
77
|
|
|
80
78
|
:param key: the key to increment
|
|
81
79
|
:param expiry: amount in seconds for the key to expire in
|
|
82
|
-
:param elastic_expiry: whether to keep extending the rate limit
|
|
83
|
-
window every hit.
|
|
84
80
|
:param amount: the number to increment by
|
|
85
81
|
"""
|
|
86
82
|
raise NotImplementedError
|
limits/storage/memcached.py
CHANGED
|
@@ -153,6 +153,8 @@ class MemcachedStorage(Storage, SlidingWindowCounterSupport, TimestampedSlidingW
|
|
|
153
153
|
Return multiple counters at once
|
|
154
154
|
|
|
155
155
|
:param keys: the keys to get the counter values for
|
|
156
|
+
|
|
157
|
+
:meta private:
|
|
156
158
|
"""
|
|
157
159
|
return self.storage.get_many(keys)
|
|
158
160
|
|
|
@@ -166,7 +168,6 @@ class MemcachedStorage(Storage, SlidingWindowCounterSupport, TimestampedSlidingW
|
|
|
166
168
|
self,
|
|
167
169
|
key: str,
|
|
168
170
|
expiry: float,
|
|
169
|
-
elastic_expiry: bool = False,
|
|
170
171
|
amount: int = 1,
|
|
171
172
|
set_expiration_key: bool = True,
|
|
172
173
|
) -> int:
|
|
@@ -175,43 +176,21 @@ class MemcachedStorage(Storage, SlidingWindowCounterSupport, TimestampedSlidingW
|
|
|
175
176
|
|
|
176
177
|
:param key: the key to increment
|
|
177
178
|
:param expiry: amount in seconds for the key to expire in
|
|
178
|
-
:param elastic_expiry: whether to keep extending the rate limit
|
|
179
179
|
window every hit.
|
|
180
180
|
:param amount: the number to increment by
|
|
181
181
|
:param set_expiration_key: set the expiration key with the expiration time if needed. If set to False, the key will still expire, but memcached cannot provide the expiration time.
|
|
182
182
|
"""
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
self.call_memcached_func(
|
|
189
|
-
self.storage.set,
|
|
190
|
-
self._expiration_key(key),
|
|
191
|
-
expiry + time.time(),
|
|
192
|
-
expire=ceil(expiry),
|
|
193
|
-
noreply=False,
|
|
194
|
-
)
|
|
195
|
-
|
|
183
|
+
if (
|
|
184
|
+
value := self.call_memcached_func(
|
|
185
|
+
self.storage.incr, key, amount, noreply=False
|
|
186
|
+
)
|
|
187
|
+
) is not None:
|
|
196
188
|
return value
|
|
197
189
|
else:
|
|
198
190
|
if not self.call_memcached_func(
|
|
199
191
|
self.storage.add, key, amount, ceil(expiry), noreply=False
|
|
200
192
|
):
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
if elastic_expiry:
|
|
204
|
-
self.call_memcached_func(self.storage.touch, key, ceil(expiry))
|
|
205
|
-
if set_expiration_key:
|
|
206
|
-
self.call_memcached_func(
|
|
207
|
-
self.storage.set,
|
|
208
|
-
self._expiration_key(key),
|
|
209
|
-
expiry + time.time(),
|
|
210
|
-
expire=ceil(expiry),
|
|
211
|
-
noreply=False,
|
|
212
|
-
)
|
|
213
|
-
|
|
214
|
-
return value
|
|
193
|
+
return self.storage.incr(key, amount) or amount
|
|
215
194
|
else:
|
|
216
195
|
if set_expiration_key:
|
|
217
196
|
self.call_memcached_func(
|
limits/storage/memory.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import bisect
|
|
3
4
|
import threading
|
|
4
5
|
import time
|
|
5
6
|
from collections import Counter, defaultdict
|
|
@@ -25,7 +26,7 @@ class MemoryStorage(
|
|
|
25
26
|
):
|
|
26
27
|
"""
|
|
27
28
|
rate limit storage using :class:`collections.Counter`
|
|
28
|
-
as an in memory storage for fixed and
|
|
29
|
+
as an in memory storage for fixed and sliding window strategies,
|
|
29
30
|
and a simple list to implement moving window strategy.
|
|
30
31
|
|
|
31
32
|
"""
|
|
@@ -56,9 +57,11 @@ class MemoryStorage(
|
|
|
56
57
|
def __expire_events(self) -> None:
|
|
57
58
|
for key in list(self.events.keys()):
|
|
58
59
|
with self.locks[key]:
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
if events := self.events.get(key, []):
|
|
61
|
+
oldest = bisect.bisect_left(
|
|
62
|
+
events, -time.time(), key=lambda event: -event.expiry
|
|
63
|
+
)
|
|
64
|
+
self.events[key] = self.events[key][:oldest]
|
|
62
65
|
if not self.events.get(key, None):
|
|
63
66
|
self.locks.pop(key, None)
|
|
64
67
|
for key in list(self.expirations.keys()):
|
|
@@ -78,26 +81,20 @@ class MemoryStorage(
|
|
|
78
81
|
) -> type[Exception] | tuple[type[Exception], ...]: # pragma: no cover
|
|
79
82
|
return ValueError
|
|
80
83
|
|
|
81
|
-
def incr(
|
|
82
|
-
self, key: str, expiry: float, elastic_expiry: bool = False, amount: int = 1
|
|
83
|
-
) -> int:
|
|
84
|
+
def incr(self, key: str, expiry: float, amount: int = 1) -> int:
|
|
84
85
|
"""
|
|
85
86
|
increments the counter for a given rate limit key
|
|
86
87
|
|
|
87
88
|
:param key: the key to increment
|
|
88
89
|
:param expiry: amount in seconds for the key to expire in
|
|
89
|
-
:param elastic_expiry: whether to keep extending the rate limit
|
|
90
|
-
window every hit.
|
|
91
90
|
:param amount: the number to increment by
|
|
92
91
|
"""
|
|
93
92
|
self.get(key)
|
|
94
93
|
self.__schedule_expiry()
|
|
95
94
|
with self.locks[key]:
|
|
96
95
|
self.storage[key] += amount
|
|
97
|
-
|
|
98
|
-
if elastic_expiry or self.storage[key] == amount:
|
|
96
|
+
if self.storage[key] == amount:
|
|
99
97
|
self.expirations[key] = time.time() + expiry
|
|
100
|
-
|
|
101
98
|
return self.storage.get(key, 0)
|
|
102
99
|
|
|
103
100
|
def decr(self, key: str, amount: int = 1) -> int:
|
|
@@ -157,7 +154,7 @@ class MemoryStorage(
|
|
|
157
154
|
if entry and entry.atime >= timestamp - expiry:
|
|
158
155
|
return False
|
|
159
156
|
else:
|
|
160
|
-
self.events[key][:0] = [Entry(expiry)
|
|
157
|
+
self.events[key][:0] = [Entry(expiry)] * amount
|
|
161
158
|
return True
|
|
162
159
|
|
|
163
160
|
def get_expiry(self, key: str) -> float:
|
|
@@ -167,21 +164,6 @@ class MemoryStorage(
|
|
|
167
164
|
|
|
168
165
|
return self.expirations.get(key, time.time())
|
|
169
166
|
|
|
170
|
-
def get_num_acquired(self, key: str, expiry: int) -> int:
|
|
171
|
-
"""
|
|
172
|
-
returns the number of entries already acquired
|
|
173
|
-
|
|
174
|
-
:param key: rate limit key to acquire an entry in
|
|
175
|
-
:param expiry: expiry of the entry
|
|
176
|
-
"""
|
|
177
|
-
timestamp = time.time()
|
|
178
|
-
|
|
179
|
-
return (
|
|
180
|
-
len([k for k in self.events.get(key, []) if k.atime >= timestamp - expiry])
|
|
181
|
-
if self.events.get(key)
|
|
182
|
-
else 0
|
|
183
|
-
)
|
|
184
|
-
|
|
185
167
|
def get_moving_window(self, key: str, limit: int, expiry: int) -> tuple[float, int]:
|
|
186
168
|
"""
|
|
187
169
|
returns the starting point and the number of entries in the moving
|
|
@@ -192,13 +174,12 @@ class MemoryStorage(
|
|
|
192
174
|
:return: (start of window, number of acquired entries)
|
|
193
175
|
"""
|
|
194
176
|
timestamp = time.time()
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
return timestamp, acquired
|
|
177
|
+
if events := self.events.get(key, []):
|
|
178
|
+
oldest = bisect.bisect_left(
|
|
179
|
+
events, -(timestamp - expiry), key=lambda entry: -entry.atime
|
|
180
|
+
)
|
|
181
|
+
return events[oldest - 1].atime, oldest
|
|
182
|
+
return timestamp, 0
|
|
202
183
|
|
|
203
184
|
def acquire_sliding_window_entry(
|
|
204
185
|
self,
|