package calls import ( "sync" "time" ) // maxSignalBuckets bounds the signalling limiter's map (review Important // 1): a generous multiple of what any real deployment holds at once // (sixteen participations to a room), so legitimate traffic never // evicts anything, while an attacker opening session after session // cannot grow the map without bound. const maxSignalBuckets = 4096 // rateLimiter is a per-key token bucket (spec §4.4): rate tokens are // added to a key's bucket per second, up to burst held, and one message // spends one token. POST /m/{sqid}/signal keys it on session and room // (session+"|"+sqid), not on anything the request body claims: the // session half comes from the session cookie the request actually // presented, so nobody can spend another session's budget or dodge // their own by rotating a claimed peer id (review Important 1). // // Entries are process-local, like the hub itself, and bounded by // maxKeys: once the map holds that many keys, the least recently // touched bucket is evicted to make room, so an attacker opening // unbounded sessions cannot grow the map without bound. A bucket is // also dropped once idle for a minute; either way it is simply // recreated at burst on its next visit, which is correct — a key that // has been quiet owes nothing. The idle sweep runs inline, every // thousandth call, rather than on a timer: nothing here needs a // goroutine, and the call rate this guards is exactly what would drive // the sweep anyway. type rateLimiter struct { rate float64 // tokens added per second burst float64 // bucket capacity maxKeys int // 0 means unbounded — used only by tests; named to // avoid shadowing the builtin cap (review Task 5 item 3) mu sync.Mutex buckets map[string]*tokenBucket calls uint64 } type tokenBucket struct { tokens float64 seenAt time.Time } func newRateLimiter(rate, burst, maxKeys int) *rateLimiter { return &rateLimiter{rate: float64(rate), burst: float64(burst), maxKeys: maxKeys, buckets: map[string]*tokenBucket{}} } // newRateLimiterEvery is a limiter that adds one token per interval: for // the report endpoints, whose rates are slower than one a second. func newRateLimiterEvery(every time.Duration, burst, maxKeys int) *rateLimiter { return &rateLimiter{rate: 1 / every.Seconds(), burst: float64(burst), maxKeys: maxKeys, buckets: map[string]*tokenBucket{}} } // allow spends one token for key, refilling by the time elapsed since // its last visit. ok is false when the bucket is empty, in which case // retryAfter is how long until one token is available. func (l *rateLimiter) allow(key string, now time.Time) (ok bool, retryAfter time.Duration) { l.mu.Lock() defer l.mu.Unlock() l.calls++ if l.calls%1000 == 0 { l.sweepLocked(now) } b, found := l.buckets[key] if !found { if l.maxKeys > 0 && len(l.buckets) >= l.maxKeys { l.evictOldestLocked() } b = &tokenBucket{tokens: l.burst, seenAt: now} l.buckets[key] = b } else if elapsed := now.Sub(b.seenAt).Seconds(); elapsed > 0 { b.tokens = min(l.burst, b.tokens+elapsed*l.rate) b.seenAt = now } if b.tokens < 1 { need := (1 - b.tokens) / l.rate return false, time.Duration(need * float64(time.Second)) } b.tokens-- return true, 0 } // sweepLocked drops any bucket idle for a full minute. func (l *rateLimiter) sweepLocked(now time.Time) { for k, b := range l.buckets { if now.Sub(b.seenAt) > time.Minute { delete(l.buckets, k) } } } // evictOldestLocked drops the bucket least recently touched, so a // bounded limiter never grows past maxKeys: a burst of distinct keys // displaces the quietest one rather than growing the map without bound // (review Important 1). O(n) in the bucket count, which only matters // right at the limit — everyday traffic never reaches it. func (l *rateLimiter) evictOldestLocked() { var oldestKey string var oldestAt time.Time first := true for k, b := range l.buckets { if first || b.seenAt.Before(oldestAt) { oldestKey, oldestAt, first = k, b.seenAt, false } } if !first { delete(l.buckets, oldestKey) } } // size is how many keys the limiter currently holds — a test-only // window into whether a request minted a bucket at all (review Task 5 // item 1's rotation test). func (l *rateLimiter) size() int { l.mu.Lock() defer l.mu.Unlock() return len(l.buckets) }