43 lines
1.2 KiB
Python
43 lines
1.2 KiB
Python
"""Redis-backed fixed-window rate limiter helpers for sensitive API operations."""
|
|
|
|
import time
|
|
|
|
from redis.exceptions import RedisError
|
|
|
|
from app.worker.queue import get_redis
|
|
|
|
|
|
def _rate_limit_key(*, scope: str, subject: str, window_id: int) -> str:
|
|
"""Builds a stable Redis key for one scope, subject, and fixed time window."""
|
|
|
|
return f"dcm:rate-limit:{scope}:{subject}:{window_id}"
|
|
|
|
|
|
def increment_rate_limit(
|
|
*,
|
|
scope: str,
|
|
subject: str,
|
|
limit: int,
|
|
window_seconds: int = 60,
|
|
) -> tuple[int, int]:
|
|
"""Increments one rate bucket and returns current count with configured limit."""
|
|
|
|
bounded_limit = max(0, int(limit))
|
|
if bounded_limit == 0:
|
|
return (0, 0)
|
|
|
|
bounded_window = max(1, int(window_seconds))
|
|
current_window = int(time.time() // bounded_window)
|
|
key = _rate_limit_key(scope=scope, subject=subject, window_id=current_window)
|
|
|
|
redis_client = get_redis()
|
|
try:
|
|
pipeline = redis_client.pipeline(transaction=True)
|
|
pipeline.incr(key, 1)
|
|
pipeline.expire(key, bounded_window + 5)
|
|
count_value, _ = pipeline.execute()
|
|
except RedisError as error:
|
|
raise RuntimeError("Rate limiter backend unavailable") from error
|
|
|
|
return (int(count_value), bounded_limit)
|