"""In-memory per-client rate limiting (token window) for job creation. Single-process Spaces need no external store. The limiter is deliberately simple: a fixed 1-hour window of timestamps per client key. """ from __future__ import annotations import time from collections import OrderedDict, deque class RateLimiter: """Fixed-window limiter with a strict LRU bound on client buckets.""" def __init__(self, max_per_hour: int, *, max_clients: int = 4096) -> None: self.max_per_hour = max(1, int(max_per_hour)) self.max_clients = max(1, min(int(max_clients), 4096)) self._hits: OrderedDict[str, deque[float]] = OrderedDict() def check(self, key: str) -> int | None: """Record a hit. Returns None when allowed, else seconds until the oldest hit expires (for Retry-After).""" now = time.time() key = (key or "?")[:128] window = self._hits.get(key) if window is None: # Bound memory even when every request presents a new peer. LRU # eviction is deterministic and happens *before* insertion, so # the map can never transiently exceed max_clients. if len(self._hits) >= self.max_clients: self._hits.popitem(last=False) window = deque() self._hits[key] = window else: self._hits.move_to_end(key) while window and now - window[0] > 3600: window.popleft() if len(window) >= self.max_per_hour: return int(3600 - (now - window[0])) + 1 window.append(now) return None def client_key(peer_host: str | None) -> str: """Return only the ASGI socket peer resolved by the trusted server. Raw ``X-Forwarded-For`` is intentionally not accepted here. Uvicorn may resolve trusted proxy headers into ``request.client`` according to its own proxy policy; application code must not reinterpret caller-controlled forwarding headers. """ return (peer_host or "?")[:128]