priority: high

status: open

title: Implement per-client rate limiting at the edge

type: feature


current_content:
"""Per-client rate limiting at the API gateway edge."""

class TokenBucket:
    def __init__(self, capacity, refill_per_sec):
        raise NotImplementedError

    def allow(self, now_ms):
        """True if a request may proceed, consuming one token."""
        raise NotImplementedError

function: TokenBucket

note: hidden tests also run at verification time and are not shown

service: api-gateway

spec:
Implement class TokenBucket(capacity, refill_per_sec) with one method,
allow(now_ms), returning True if a request may proceed and False otherwise.

A bucket starts full. Each allowed request consumes exactly one token; a
refused request consumes none. Tokens refill continuously at refill_per_sec
and the bucket never holds more than capacity.

Refill is continuous, not stepwise: two calls 500ms apart at 1 token/sec must
together restore one whole token, so partial time must not be discarded. The
first call establishes the clock and refills nothing.

now_ms comes from a wall clock that is occasionally stepped backwards by NTP.
A backwards jump must never grant tokens and must never make the bucket
permanently unable to refill: treat time as not having advanced, and take the
new reading as the current clock.

visible_tests:
a full bucket allows up to capacity
waiting restores a token


content:
"""Per-client rate limiting at the API gateway edge."""

class TokenBucket:
    def __init__(self, capacity, refill_per_sec):
        self.capacity = float(capacity)
        self.refill_per_sec = float(refill_per_sec)
        self.tokens = float(capacity)
        self.last_ms = None

    def allow(self, now_ms):
        if self.last_ms is None:
            self.last_ms = now_ms
        elif now_ms > self.last_ms:
            elapsed = (now_ms - self.last_ms) / 1000.0
            self.tokens = min(self.capacity,
                              self.tokens + elapsed * self.refill_per_sec)
            self.last_ms = now_ms
        else:
            self.last_ms = now_ms
        if self.tokens >= 1.0:
            self.tokens -= 1.0
            return True
        return False

next: run_exercise_tests to execute it


note: all visible tests pass; hidden tests also ran and are not shown