package tokenbucket import ( "sync" "testing" "time" ) // Bucket is a thread-safe token-bucket rate limiter. type Bucket struct { mu sync.Mutex capacity float64 tokens float64 rate float64 last time.Time now func() time.Time } // New creates a full bucket with the given capacity and refill rate. func New(capacity int, tokensPerSecond float64) *Bucket { if capacity <= 0 { panic("capacity must be positive") } if tokensPerSecond <= 0 { panic("tokensPerSecond must be positive") } return &Bucket{ capacity: float64(capacity), tokens: float64(capacity), rate: tokensPerSecond, last: time.Now(), now: time.Now, } } // Allow reports whether one token can be consumed immediately. func (b *Bucket) Allow() bool { return b.AllowN(1) } // AllowN reports whether n tokens can be consumed immediately. func (b *Bucket) AllowN(n int) bool { if n <= 0 { return false } b.mu.Lock() defer b.mu.Unlock() now := b.now() elapsed := now.Sub(b.last).Seconds() if elapsed > 0 { b.tokens += elapsed * b.rate if b.tokens > b.capacity { b.tokens = b.capacity } b.last = now } if float64(n) > b.tokens { return false } b.tokens -= float64(n) return true } func TestBucketCapacity(t *testing.T) { b := New(2, 1) if !b.Allow() || !b.Allow() { t.Fatal("expected initial tokens to be available") } if b.Allow() { t.Fatal("expected empty bucket to reject request") } } func TestBucketRefill(t *testing.T) { now := time.Unix(0, 0) b := New(1, 2) b.now = func() time.Time { return now } b.last = now if !b.Allow() { t.Fatal("expected initial token") } now = now.Add(500 * time.Millisecond) if !b.Allow() { t.Fatal("expected one refilled token") } }