package gate import ( "sync" "time" ) const ( limitBurst = 10 limitPeriod = time.Minute limitPrune = 10_000 ) // Limiter is a per-key token bucket: limitBurst attempts, refilling at // limitBurst per limitPeriod. bcrypt at cost 10 already costs tens of // milliseconds a comparison; this exists so the CPU is spent on people and // not on a script. type Limiter struct { mu sync.Mutex now func() time.Time buckets map[string]*bucket } type bucket struct { tokens float64 last time.Time } func NewLimiter(now func() time.Time) *Limiter { return &Limiter{now: now, buckets: map[string]*bucket{}} } // Allow spends one token for key, reporting whether one was available. func (l *Limiter) Allow(key string) bool { l.mu.Lock() defer l.mu.Unlock() now := l.now() b, ok := l.buckets[key] if !ok { if len(l.buckets) >= limitPrune { l.prune(now) } b = &bucket{tokens: limitBurst, last: now} l.buckets[key] = b } refill := now.Sub(b.last).Seconds() * (limitBurst / limitPeriod.Seconds()) b.tokens = min(limitBurst, b.tokens+refill) b.last = now if b.tokens < 1 { return false } b.tokens-- return true } // prune drops every bucket that has been idle long enough to be full again; // keeping it would change no answer. func (l *Limiter) prune(now time.Time) { for k, b := range l.buckets { if now.Sub(b.last) >= limitPeriod { delete(l.buckets, k) } } }