watchdog/internal/ratelimit/limiter.go
NotAShelf f46697bd21
internal/ratelimit: prevent time drift in TokenBucket refills
The TokenBucket ratelimiter accumulated time drift over multiple refills
because I'm an idiot. We were using 'now' as base for lastFill calc. but
this could case rate limiting to become inaccurate over time. Now we
advance lastFill by *exact* periods from previous value.

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ia3990b441ab6072f51dfdfa4a2511b5f6a6a6964
2026-03-02 22:38:23 +03:00

54 lines
1.1 KiB
Go

package ratelimit
import (
"sync"
"time"
)
// Implements a simple token bucket rate limiter
type TokenBucket struct {
mu sync.Mutex
tokens int
capacity int
refill int
interval time.Duration
lastFill time.Time
}
// Creates a rate limiter with specified capacity and refill rate capacity
func NewTokenBucket(capacity, refillPerInterval int, interval time.Duration) *TokenBucket {
return &TokenBucket{
tokens: capacity,
capacity: capacity,
refill: refillPerInterval,
interval: interval,
lastFill: time.Now(),
}
}
// Allow checks if a request should be allowed
func (tb *TokenBucket) Allow() bool {
tb.mu.Lock()
defer tb.mu.Unlock()
// Refill tokens based on elapsed time
now := time.Now()
elapsed := now.Sub(tb.lastFill)
if elapsed >= tb.interval {
periods := int(elapsed / tb.interval)
tb.tokens += periods * tb.refill
if tb.tokens > tb.capacity {
tb.tokens = tb.capacity
}
// Advance lastFill by exact periods to prevent drift
tb.lastFill = tb.lastFill.Add(time.Duration(periods) * tb.interval)
}
// Check if we have tokens available
if tb.tokens > 0 {
tb.tokens--
return true
}
return false
}