img2threejs / app /ratelimit.py
Mike0021's picture
Deploy verified img2threejs Docker Space
39ff632 verified
Raw
History Blame Contribute Delete
2.03 kB
"""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]