Spaces:
Sleeping
Sleeping
| """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}") | |