limits 4.0.0__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 +233 -19
- 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 +7 -6
- limits/storage/base.py +92 -24
- limits/storage/etcd.py +6 -2
- limits/storage/memcached.py +142 -36
- limits/storage/memory.py +97 -12
- limits/storage/mongodb.py +220 -20
- 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.0.dist-info/METADATA +0 -192
- limits-4.0.0.dist-info/RECORD +0 -37
- {limits-4.0.0.dist-info → limits-4.1.dist-info}/LICENSE.txt +0 -0
- {limits-4.0.0.dist-info → limits-4.1.dist-info}/WHEEL +0 -0
- {limits-4.0.0.dist-info → limits-4.1.dist-info}/top_level.txt +0 -0
limits/aio/storage/memory.py
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import time
|
|
3
|
-
from collections import Counter
|
|
3
|
+
from collections import Counter, defaultdict
|
|
4
|
+
from math import floor
|
|
4
5
|
|
|
5
6
|
from deprecated.sphinx import versionadded
|
|
6
7
|
|
|
7
8
|
import limits.typing
|
|
8
|
-
from limits.aio.storage.base import
|
|
9
|
-
|
|
9
|
+
from limits.aio.storage.base import (
|
|
10
|
+
MovingWindowSupport,
|
|
11
|
+
SlidingWindowCounterSupport,
|
|
12
|
+
Storage,
|
|
13
|
+
)
|
|
14
|
+
from limits.storage.base import TimestampedSlidingWindow
|
|
15
|
+
from limits.typing import Optional, Type, Union
|
|
10
16
|
|
|
11
17
|
|
|
12
18
|
class LockableEntry(asyncio.Lock):
|
|
@@ -17,7 +23,9 @@ class LockableEntry(asyncio.Lock):
|
|
|
17
23
|
|
|
18
24
|
|
|
19
25
|
@versionadded(version="2.1")
|
|
20
|
-
class MemoryStorage(
|
|
26
|
+
class MemoryStorage(
|
|
27
|
+
Storage, MovingWindowSupport, SlidingWindowCounterSupport, TimestampedSlidingWindow
|
|
28
|
+
):
|
|
21
29
|
"""
|
|
22
30
|
rate limit storage using :class:`collections.Counter`
|
|
23
31
|
as an in memory storage for fixed and elastic window strategies,
|
|
@@ -34,15 +42,16 @@ class MemoryStorage(Storage, MovingWindowSupport):
|
|
|
34
42
|
self, uri: Optional[str] = None, wrap_exceptions: bool = False, **_: str
|
|
35
43
|
) -> None:
|
|
36
44
|
self.storage: limits.typing.Counter[str] = Counter()
|
|
37
|
-
self.
|
|
38
|
-
self.
|
|
45
|
+
self.locks: defaultdict[str, asyncio.Lock] = defaultdict(asyncio.Lock)
|
|
46
|
+
self.expirations: dict[str, float] = {}
|
|
47
|
+
self.events: dict[str, list[LockableEntry]] = {}
|
|
39
48
|
self.timer: Optional[asyncio.Task[None]] = None
|
|
40
49
|
super().__init__(uri, wrap_exceptions=wrap_exceptions, **_)
|
|
41
50
|
|
|
42
51
|
@property
|
|
43
52
|
def base_exceptions(
|
|
44
53
|
self,
|
|
45
|
-
) -> Union[Type[Exception],
|
|
54
|
+
) -> Union[Type[Exception], tuple[Type[Exception], ...]]: # pragma: no cover
|
|
46
55
|
return ValueError
|
|
47
56
|
|
|
48
57
|
async def __expire_events(self) -> None:
|
|
@@ -56,13 +65,14 @@ class MemoryStorage(Storage, MovingWindowSupport):
|
|
|
56
65
|
if self.expirations[key] <= time.time():
|
|
57
66
|
self.storage.pop(key, None)
|
|
58
67
|
self.expirations.pop(key, None)
|
|
68
|
+
self.locks.pop(key, None)
|
|
59
69
|
|
|
60
70
|
async def __schedule_expiry(self) -> None:
|
|
61
71
|
if not self.timer or self.timer.done():
|
|
62
72
|
self.timer = asyncio.create_task(self.__expire_events())
|
|
63
73
|
|
|
64
74
|
async def incr(
|
|
65
|
-
self, key: str, expiry:
|
|
75
|
+
self, key: str, expiry: float, elastic_expiry: bool = False, amount: int = 1
|
|
66
76
|
) -> int:
|
|
67
77
|
"""
|
|
68
78
|
increments the counter for a given rate limit key
|
|
@@ -75,10 +85,24 @@ class MemoryStorage(Storage, MovingWindowSupport):
|
|
|
75
85
|
"""
|
|
76
86
|
await self.get(key)
|
|
77
87
|
await self.__schedule_expiry()
|
|
78
|
-
self.
|
|
88
|
+
async with self.locks[key]:
|
|
89
|
+
self.storage[key] += amount
|
|
79
90
|
|
|
80
|
-
|
|
81
|
-
|
|
91
|
+
if elastic_expiry or self.storage[key] == amount:
|
|
92
|
+
self.expirations[key] = time.time() + expiry
|
|
93
|
+
|
|
94
|
+
return self.storage.get(key, amount)
|
|
95
|
+
|
|
96
|
+
async def decr(self, key: str, amount: int = 1) -> int:
|
|
97
|
+
"""
|
|
98
|
+
decrements the counter for a given rate limit key. 0 is the minimum allowed value.
|
|
99
|
+
|
|
100
|
+
:param amount: the number to increment by
|
|
101
|
+
"""
|
|
102
|
+
await self.get(key)
|
|
103
|
+
await self.__schedule_expiry()
|
|
104
|
+
async with self.locks[key]:
|
|
105
|
+
self.storage[key] = max(self.storage[key] - amount, 0)
|
|
82
106
|
|
|
83
107
|
return self.storage.get(key, amount)
|
|
84
108
|
|
|
@@ -86,10 +110,10 @@ class MemoryStorage(Storage, MovingWindowSupport):
|
|
|
86
110
|
"""
|
|
87
111
|
:param key: the key to get the counter value for
|
|
88
112
|
"""
|
|
89
|
-
|
|
90
113
|
if self.expirations.get(key, 0) <= time.time():
|
|
91
114
|
self.storage.pop(key, None)
|
|
92
115
|
self.expirations.pop(key, None)
|
|
116
|
+
self.locks.pop(key, None)
|
|
93
117
|
|
|
94
118
|
return self.storage.get(key, 0)
|
|
95
119
|
|
|
@@ -100,6 +124,7 @@ class MemoryStorage(Storage, MovingWindowSupport):
|
|
|
100
124
|
self.storage.pop(key, None)
|
|
101
125
|
self.expirations.pop(key, None)
|
|
102
126
|
self.events.pop(key, None)
|
|
127
|
+
self.locks.pop(key, None)
|
|
103
128
|
|
|
104
129
|
async def acquire_entry(
|
|
105
130
|
self, key: str, limit: int, expiry: int, amount: int = 1
|
|
@@ -153,7 +178,7 @@ class MemoryStorage(Storage, MovingWindowSupport):
|
|
|
153
178
|
# FIXME: arg limit is not used
|
|
154
179
|
async def get_moving_window(
|
|
155
180
|
self, key: str, limit: int, expiry: int
|
|
156
|
-
) ->
|
|
181
|
+
) -> tuple[float, int]:
|
|
157
182
|
"""
|
|
158
183
|
returns the starting point and the number of entries in the moving
|
|
159
184
|
window
|
|
@@ -171,6 +196,65 @@ class MemoryStorage(Storage, MovingWindowSupport):
|
|
|
171
196
|
|
|
172
197
|
return timestamp, acquired
|
|
173
198
|
|
|
199
|
+
async def acquire_sliding_window_entry(
|
|
200
|
+
self,
|
|
201
|
+
key: str,
|
|
202
|
+
limit: int,
|
|
203
|
+
expiry: int,
|
|
204
|
+
amount: int = 1,
|
|
205
|
+
) -> bool:
|
|
206
|
+
if amount > limit:
|
|
207
|
+
return False
|
|
208
|
+
now = time.time()
|
|
209
|
+
previous_key, current_key = self.sliding_window_keys(key, expiry, now)
|
|
210
|
+
(
|
|
211
|
+
previous_count,
|
|
212
|
+
previous_ttl,
|
|
213
|
+
current_count,
|
|
214
|
+
_,
|
|
215
|
+
) = await self._get_sliding_window_info(previous_key, current_key, expiry, now)
|
|
216
|
+
weighted_count = previous_count * previous_ttl / expiry + current_count
|
|
217
|
+
if floor(weighted_count) + amount > limit:
|
|
218
|
+
return False
|
|
219
|
+
else:
|
|
220
|
+
# Hit, increase the current counter.
|
|
221
|
+
# If the counter doesn't exist yet, set twice the theorical expiry.
|
|
222
|
+
current_count = await self.incr(current_key, 2 * expiry, amount=amount)
|
|
223
|
+
weighted_count = previous_count * previous_ttl / expiry + current_count
|
|
224
|
+
if floor(weighted_count) > limit:
|
|
225
|
+
# Another hit won the race condition: revert the incrementation and refuse this hit
|
|
226
|
+
# Limitation: during high concurrency at the end of the window,
|
|
227
|
+
# the counter is shifted and cannot be decremented, so less requests than expected are allowed.
|
|
228
|
+
await self.decr(current_key, amount)
|
|
229
|
+
# print("Concurrent call, reverting the counter increase")
|
|
230
|
+
return False
|
|
231
|
+
return True
|
|
232
|
+
|
|
233
|
+
async def get_sliding_window(
|
|
234
|
+
self, key: str, expiry: int
|
|
235
|
+
) -> tuple[int, float, int, float]:
|
|
236
|
+
now = time.time()
|
|
237
|
+
previous_key, current_key = self.sliding_window_keys(key, expiry, now)
|
|
238
|
+
return await self._get_sliding_window_info(
|
|
239
|
+
previous_key, current_key, expiry, now
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
async def _get_sliding_window_info(
|
|
243
|
+
self,
|
|
244
|
+
previous_key: str,
|
|
245
|
+
current_key: str,
|
|
246
|
+
expiry: int,
|
|
247
|
+
now: float,
|
|
248
|
+
) -> tuple[int, float, int, float]:
|
|
249
|
+
previous_count = await self.get(previous_key)
|
|
250
|
+
current_count = await self.get(current_key)
|
|
251
|
+
if previous_count == 0:
|
|
252
|
+
previous_ttl = float(0)
|
|
253
|
+
else:
|
|
254
|
+
previous_ttl = (1 - (((now - expiry) / expiry) % 1)) * expiry
|
|
255
|
+
current_ttl = (1 - ((now / expiry) % 1)) * expiry + expiry
|
|
256
|
+
return previous_count, previous_ttl, current_count, current_ttl
|
|
257
|
+
|
|
174
258
|
async def check(self) -> bool:
|
|
175
259
|
"""
|
|
176
260
|
check if storage is healthy
|
|
@@ -183,5 +267,6 @@ class MemoryStorage(Storage, MovingWindowSupport):
|
|
|
183
267
|
self.storage.clear()
|
|
184
268
|
self.expirations.clear()
|
|
185
269
|
self.events.clear()
|
|
270
|
+
self.locks.clear()
|
|
186
271
|
|
|
187
272
|
return num_items
|
limits/aio/storage/mongodb.py
CHANGED
|
@@ -3,12 +3,22 @@ from __future__ import annotations
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import datetime
|
|
5
5
|
import time
|
|
6
|
-
from typing import Any, cast
|
|
7
6
|
|
|
8
7
|
from deprecated.sphinx import versionadded, versionchanged
|
|
9
8
|
|
|
10
|
-
from limits.aio.storage.base import
|
|
11
|
-
|
|
9
|
+
from limits.aio.storage.base import (
|
|
10
|
+
MovingWindowSupport,
|
|
11
|
+
SlidingWindowCounterSupport,
|
|
12
|
+
Storage,
|
|
13
|
+
)
|
|
14
|
+
from limits.typing import (
|
|
15
|
+
Optional,
|
|
16
|
+
ParamSpec,
|
|
17
|
+
Type,
|
|
18
|
+
TypeVar,
|
|
19
|
+
Union,
|
|
20
|
+
cast,
|
|
21
|
+
)
|
|
12
22
|
from limits.util import get_dependency
|
|
13
23
|
|
|
14
24
|
P = ParamSpec("P")
|
|
@@ -20,7 +30,7 @@ R = TypeVar("R")
|
|
|
20
30
|
version="3.14.0",
|
|
21
31
|
reason="Added option to select custom collection names for windows & counters",
|
|
22
32
|
)
|
|
23
|
-
class MongoDBStorage(Storage, MovingWindowSupport):
|
|
33
|
+
class MongoDBStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
|
|
24
34
|
"""
|
|
25
35
|
Rate limit storage with MongoDB as backend.
|
|
26
36
|
|
|
@@ -50,7 +60,8 @@ class MongoDBStorage(Storage, MovingWindowSupport):
|
|
|
50
60
|
collections.
|
|
51
61
|
:param counter_collection_name: The collection name to use for individual counters
|
|
52
62
|
used in fixed window strategies
|
|
53
|
-
:param window_collection_name: The collection name to use for moving window
|
|
63
|
+
:param window_collection_name: The collection name to use for sliding & moving window
|
|
64
|
+
storage
|
|
54
65
|
:param wrap_exceptions: Whether to wrap storage exceptions in
|
|
55
66
|
:exc:`limits.errors.StorageError` before raising it.
|
|
56
67
|
:param options: all remaining keyword arguments are passed
|
|
@@ -83,7 +94,7 @@ class MongoDBStorage(Storage, MovingWindowSupport):
|
|
|
83
94
|
@property
|
|
84
95
|
def base_exceptions(
|
|
85
96
|
self,
|
|
86
|
-
) -> Union[Type[Exception],
|
|
97
|
+
) -> Union[Type[Exception], tuple[Type[Exception], ...]]: # pragma: no cover
|
|
87
98
|
return self.lib_errors.PyMongoError # type: ignore
|
|
88
99
|
|
|
89
100
|
@property
|
|
@@ -224,7 +235,7 @@ class MongoDBStorage(Storage, MovingWindowSupport):
|
|
|
224
235
|
|
|
225
236
|
async def get_moving_window(
|
|
226
237
|
self, key: str, limit: int, expiry: int
|
|
227
|
-
) ->
|
|
238
|
+
) -> tuple[float, int]:
|
|
228
239
|
"""
|
|
229
240
|
returns the starting point and the number of entries in the moving
|
|
230
241
|
window
|
|
@@ -281,23 +292,29 @@ class MongoDBStorage(Storage, MovingWindowSupport):
|
|
|
281
292
|
|
|
282
293
|
timestamp = time.time()
|
|
283
294
|
try:
|
|
284
|
-
updates:
|
|
285
|
-
|
|
295
|
+
updates: dict[
|
|
296
|
+
str,
|
|
297
|
+
dict[str, Union[datetime.datetime, dict[str, Union[list[float], int]]]],
|
|
298
|
+
] = {
|
|
299
|
+
"$push": {
|
|
300
|
+
"entries": {
|
|
301
|
+
"$each": [timestamp] * amount,
|
|
302
|
+
"$position": 0,
|
|
303
|
+
"$slice": limit,
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
"$set": {
|
|
307
|
+
"expireAt": (
|
|
308
|
+
datetime.datetime.now(datetime.timezone.utc)
|
|
309
|
+
+ datetime.timedelta(seconds=expiry)
|
|
310
|
+
)
|
|
311
|
+
},
|
|
286
312
|
}
|
|
287
313
|
|
|
288
|
-
updates["$set"] = {
|
|
289
|
-
"expireAt": (
|
|
290
|
-
datetime.datetime.now(datetime.timezone.utc)
|
|
291
|
-
+ datetime.timedelta(seconds=expiry)
|
|
292
|
-
)
|
|
293
|
-
}
|
|
294
|
-
updates["$push"]["entries"]["$each"] = [timestamp] * amount
|
|
295
314
|
await self.database[self.__collection_mapping["windows"]].update_one(
|
|
296
315
|
{
|
|
297
316
|
"_id": key,
|
|
298
|
-
"entries
|
|
299
|
-
"$not": {"$gte": timestamp - expiry}
|
|
300
|
-
},
|
|
317
|
+
f"entries.{limit - amount}": {"$not": {"$gte": timestamp - expiry}},
|
|
301
318
|
},
|
|
302
319
|
updates,
|
|
303
320
|
upsert=True,
|
|
@@ -306,3 +323,200 @@ class MongoDBStorage(Storage, MovingWindowSupport):
|
|
|
306
323
|
return True
|
|
307
324
|
except self.proxy_dependency.module.errors.DuplicateKeyError:
|
|
308
325
|
return False
|
|
326
|
+
|
|
327
|
+
async def acquire_sliding_window_entry(
|
|
328
|
+
self, key: str, limit: int, expiry: int, amount: int = 1
|
|
329
|
+
) -> bool:
|
|
330
|
+
await self.create_indices()
|
|
331
|
+
expiry_ms = expiry * 1000
|
|
332
|
+
result = await self.database[
|
|
333
|
+
self.__collection_mapping["windows"]
|
|
334
|
+
].find_one_and_update(
|
|
335
|
+
{"_id": key},
|
|
336
|
+
[
|
|
337
|
+
{
|
|
338
|
+
"$set": {
|
|
339
|
+
"previousCount": {
|
|
340
|
+
"$cond": {
|
|
341
|
+
"if": {
|
|
342
|
+
"$lte": [
|
|
343
|
+
{"$subtract": ["$expiresAt", "$$NOW"]},
|
|
344
|
+
expiry_ms,
|
|
345
|
+
]
|
|
346
|
+
},
|
|
347
|
+
"then": {"$ifNull": ["$currentCount", 0]},
|
|
348
|
+
"else": {"$ifNull": ["$previousCount", 0]},
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
"$set": {
|
|
355
|
+
"currentCount": {
|
|
356
|
+
"$cond": {
|
|
357
|
+
"if": {
|
|
358
|
+
"$lte": [
|
|
359
|
+
{"$subtract": ["$expiresAt", "$$NOW"]},
|
|
360
|
+
expiry_ms,
|
|
361
|
+
]
|
|
362
|
+
},
|
|
363
|
+
"then": 0,
|
|
364
|
+
"else": {"$ifNull": ["$currentCount", 0]},
|
|
365
|
+
}
|
|
366
|
+
},
|
|
367
|
+
"expiresAt": {
|
|
368
|
+
"$cond": {
|
|
369
|
+
"if": {
|
|
370
|
+
"$lte": [
|
|
371
|
+
{"$subtract": ["$expiresAt", "$$NOW"]},
|
|
372
|
+
expiry_ms,
|
|
373
|
+
]
|
|
374
|
+
},
|
|
375
|
+
"then": {
|
|
376
|
+
"$cond": {
|
|
377
|
+
"if": {"$gt": ["$expiresAt", 0]},
|
|
378
|
+
"then": {"$add": ["$expiresAt", expiry_ms]},
|
|
379
|
+
"else": {"$add": ["$$NOW", 2 * expiry_ms]},
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
"else": "$expiresAt",
|
|
383
|
+
}
|
|
384
|
+
},
|
|
385
|
+
}
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
"$set": {
|
|
389
|
+
"curWeightedCount": {
|
|
390
|
+
"$floor": {
|
|
391
|
+
"$add": [
|
|
392
|
+
{
|
|
393
|
+
"$multiply": [
|
|
394
|
+
"$previousCount",
|
|
395
|
+
{
|
|
396
|
+
"$divide": [
|
|
397
|
+
{
|
|
398
|
+
"$max": [
|
|
399
|
+
0,
|
|
400
|
+
{
|
|
401
|
+
"$subtract": [
|
|
402
|
+
"$expiresAt",
|
|
403
|
+
{
|
|
404
|
+
"$add": [
|
|
405
|
+
"$$NOW",
|
|
406
|
+
expiry_ms,
|
|
407
|
+
]
|
|
408
|
+
},
|
|
409
|
+
]
|
|
410
|
+
},
|
|
411
|
+
]
|
|
412
|
+
},
|
|
413
|
+
expiry_ms,
|
|
414
|
+
]
|
|
415
|
+
},
|
|
416
|
+
]
|
|
417
|
+
},
|
|
418
|
+
"$currentCount",
|
|
419
|
+
]
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
"$set": {
|
|
426
|
+
"currentCount": {
|
|
427
|
+
"$cond": {
|
|
428
|
+
"if": {
|
|
429
|
+
"$lte": [
|
|
430
|
+
{"$add": ["$curWeightedCount", amount]},
|
|
431
|
+
limit,
|
|
432
|
+
]
|
|
433
|
+
},
|
|
434
|
+
"then": {"$add": ["$currentCount", amount]},
|
|
435
|
+
"else": "$currentCount",
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
"$set": {
|
|
442
|
+
"_acquired": {
|
|
443
|
+
"$lte": [{"$add": ["$curWeightedCount", amount]}, limit]
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
},
|
|
447
|
+
{"$unset": ["curWeightedCount"]},
|
|
448
|
+
],
|
|
449
|
+
return_document=self.proxy_dependency.module.ReturnDocument.AFTER,
|
|
450
|
+
upsert=True,
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
return cast(bool, result["_acquired"])
|
|
454
|
+
|
|
455
|
+
async def get_sliding_window(
|
|
456
|
+
self, key: str, expiry: int
|
|
457
|
+
) -> tuple[int, float, int, float]:
|
|
458
|
+
expiry_ms = expiry * 1000
|
|
459
|
+
if result := await self.database[
|
|
460
|
+
self.__collection_mapping["windows"]
|
|
461
|
+
].find_one_and_update(
|
|
462
|
+
{"_id": key},
|
|
463
|
+
[
|
|
464
|
+
{
|
|
465
|
+
"$set": {
|
|
466
|
+
"previousCount": {
|
|
467
|
+
"$cond": {
|
|
468
|
+
"if": {
|
|
469
|
+
"$lte": [
|
|
470
|
+
{"$subtract": ["$expiresAt", "$$NOW"]},
|
|
471
|
+
expiry_ms,
|
|
472
|
+
]
|
|
473
|
+
},
|
|
474
|
+
"then": {"$ifNull": ["$currentCount", 0]},
|
|
475
|
+
"else": {"$ifNull": ["$previousCount", 0]},
|
|
476
|
+
}
|
|
477
|
+
},
|
|
478
|
+
"currentCount": {
|
|
479
|
+
"$cond": {
|
|
480
|
+
"if": {
|
|
481
|
+
"$lte": [
|
|
482
|
+
{"$subtract": ["$expiresAt", "$$NOW"]},
|
|
483
|
+
expiry_ms,
|
|
484
|
+
]
|
|
485
|
+
},
|
|
486
|
+
"then": 0,
|
|
487
|
+
"else": {"$ifNull": ["$currentCount", 0]},
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
"expiresAt": {
|
|
491
|
+
"$cond": {
|
|
492
|
+
"if": {
|
|
493
|
+
"$lte": [
|
|
494
|
+
{"$subtract": ["$expiresAt", "$$NOW"]},
|
|
495
|
+
expiry_ms,
|
|
496
|
+
]
|
|
497
|
+
},
|
|
498
|
+
"then": {"$add": ["$expiresAt", expiry_ms]},
|
|
499
|
+
"else": "$expiresAt",
|
|
500
|
+
}
|
|
501
|
+
},
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
],
|
|
505
|
+
return_document=self.proxy_dependency.module.ReturnDocument.AFTER,
|
|
506
|
+
projection=["currentCount", "previousCount", "expiresAt"],
|
|
507
|
+
):
|
|
508
|
+
expires_at = (
|
|
509
|
+
(result["expiresAt"].replace(tzinfo=datetime.timezone.utc).timestamp())
|
|
510
|
+
if result.get("expiresAt")
|
|
511
|
+
else time.time()
|
|
512
|
+
)
|
|
513
|
+
current_ttl = max(0, expires_at - time.time())
|
|
514
|
+
prev_ttl = max(0, current_ttl - expiry if result["previousCount"] else 0)
|
|
515
|
+
|
|
516
|
+
return (
|
|
517
|
+
result["previousCount"],
|
|
518
|
+
prev_ttl,
|
|
519
|
+
result["currentCount"],
|
|
520
|
+
current_ttl,
|
|
521
|
+
)
|
|
522
|
+
return 0, 0.0, 0, 0.0
|