File size: 2,029 Bytes
39ff632
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
"""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]