Spaces:
Running
Running
| """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] | |