File size: 1,267 Bytes
732b14f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
"""Redis ZSET sliding-window rate limiter (shared across API replicas)."""

from __future__ import annotations

import time
import uuid

from redis.asyncio import Redis


class RedisSlidingWindowLimiter:
    """Per-tenant sliding window using a Redis sorted set."""

    def __init__(
        self,
        redis: Redis,
        *,
        key_prefix: str,
        max_requests: int,
        window_secs: float,
    ) -> None:
        self._redis = redis
        self._prefix = key_prefix
        self._max = max_requests
        self._window = window_secs

    async def is_allowed(self, tenant_key: str) -> bool:
        now = time.time()
        cutoff = now - self._window
        rk = f"{self._prefix}:{tenant_key}"
        member = f"{now}:{uuid.uuid4().hex}"
        pipe = self._redis.pipeline()
        pipe.zremrangebyscore(rk, 0, cutoff)
        pipe.zadd(rk, {member: now})
        pipe.zcard(rk)
        pipe.expire(rk, int(self._window) + 1)
        results = await pipe.execute()
        count = int(results[2])
        if count > self._max:
            await self._redis.zrem(rk, member)
            return False
        return True

    async def reset(self, tenant_key: str) -> None:
        await self._redis.delete(f"{self._prefix}:{tenant_key}")