// Package ratelimit provides a cooperative token-bucket rate limiter. // No mutexes - designed for single-threaded cooperative scheduling. package ratelimit import "time" type bucket struct { tokens float64 last int64 // unix nano } // Limiter is a per-key token bucket rate limiter. // // buckets holds bucket state BY VALUE. A *bucket would be allocated by Allow, // and Allow mutates only through a map update - no store through the receiver - // so the compiler does not treat it as a mutating method of a sovereign // receiver and its allocations land in whatever frame arena is current. That // arena is released at the end of the turn, so the bucket did not persist and // every write was allowed: the limiter only ever worked by leaking (the event // loop's arena used to be permanent). Keeping the bucket in the map puts it in // the map's own storage, which lives as long as the limiter. type Limiter struct { buckets map[string]bucket rate float64 // tokens per second burst int32 // max tokens } // New creates a rate limiter. Rate is tokens/second, burst is the // maximum tokens that can accumulate. func New(rate float64, burst int32) (l *Limiter) { return &Limiter{ buckets: map[string]bucket{}, rate: rate, burst: burst, } } // Allow checks whether key has a token available. Consumes one token // if allowed. func (l *Limiter) Allow(key []byte) (ok bool) { k := string(key) now := time.Now().UnixNano() b, found := l.buckets[k] if !found { b = bucket{tokens: float64(l.burst), last: now} } elapsed := float64(now-b.last) / 1e9 b.tokens += elapsed * l.rate if b.tokens > float64(l.burst) { b.tokens = float64(l.burst) } b.last = now if b.tokens < 1.0 { l.buckets[k] = b return false } // Explicit write-back of the whole bucket: the value came out of the map by // copy, so the decrement has to be stored back for the next call to see it. b.tokens = b.tokens - 1.0 l.buckets[k] = b return true } // Cleanup removes entries older than maxAge to prevent unbounded growth. func (l *Limiter) Cleanup(maxAge time.Duration) { cutoff := time.Now().Add(-maxAge).UnixNano() for k, b := range l.buckets { if b.last < cutoff { delete(l.buckets, k) } } }