"""Per-user sliding-window rate limiter.""" from __future__ import annotations import threading import time from collections import deque from collections.abc import Callable class RateLimiter: """Allows at most `limit` events per `window_s` seconds for each key.""" def __init__(self, limit: int, window_s: float = 60.0, clock: Callable[[], float] = time.monotonic) -> None: if limit < 1: raise ValueError("limit must be at least 1") self._limit = limit self._window = window_s self._clock = clock self._events: dict[int, deque[float]] = {} self._lock = threading.Lock() def check(self, key: int) -> float: """Record an event and return 0, or return the seconds to wait if the key is over the limit.""" now = self._clock() with self._lock: events = self._events.setdefault(key, deque()) while events and now - events[0] >= self._window: events.popleft() if len(events) >= self._limit: return max(0.0, self._window - (now - events[0])) events.append(now) self._prune(now) return 0.0 def _prune(self, now: float) -> None: if len(self._events) < 5_000: return for key in [k for k, ev in self._events.items() if not ev or now - ev[-1] >= self._window]: del self._events[key]