package ratelimit import ( "context" "errors" "sync" "time" ) var ( ErrInvalidRate = errors.New("rate must be positive") ErrInvalidBurst = errors.New("burst must be positive") ErrExceedsBurst = errors.New("requested tokens exceed burst capacity") ) // Limiter is a concurrency-safe token-bucket rate limiter. type Limiter struct { mu sync.Mutex rate float64 burst int tokens float64 last time.Time } // New creates a limiter that replenishes rate tokens per second and can store // at most burst tokens. A new limiter starts with a full bucket. func New(rate float64, burst int) (*Limiter, error) { if rate <= 0 { return nil, ErrInvalidRate } if burst <= 0 { return nil, ErrInvalidBurst } return &Limiter{ rate: rate, burst: burst, tokens: float64(burst), last: time.Now(), }, nil } // Allow reports whether one token is immediately available. func (l *Limiter) Allow() bool { return l.AllowN(1) } // AllowN reports whether n tokens are immediately available. Tokens are // deducted only when the request succeeds. func (l *Limiter) AllowN(n int) bool { if n <= 0 { return true } l.mu.Lock() defer l.mu.Unlock() l.refill(time.Now()) if n > l.burst || l.tokens < float64(n) { return false } l.tokens -= float64(n) return true } // Wait blocks until one token is available or ctx is canceled. func (l *Limiter) Wait(ctx context.Context) error { return l.WaitN(ctx, 1) } // WaitN blocks until n tokens are available or ctx is canceled. func (l *Limiter) WaitN(ctx context.Context, n int) error { if n <= 0 { return nil } if n > l.burst { return ErrExceedsBurst } for { l.mu.Lock() now := time.Now() l.refill(now) if l.tokens >= float64(n) { l.tokens -= float64(n) l.mu.Unlock() return nil } delay := time.Duration((float64(n)-l.tokens)/l.rate*float64(time.Second)) if delay < time.Nanosecond { delay = time.Nanosecond } l.mu.Unlock() timer := time.NewTimer(delay) select { case <-timer.C: case <-ctx.Done(): if !timer.Stop() { <-timer.C } return ctx.Err() } } } // Tokens returns the number of tokens currently available. func (l *Limiter) Tokens() float64 { l.mu.Lock() defer l.mu.Unlock() l.refill(time.Now()) return l.tokens } func (l *Limiter) refill(now time.Time) { if !now.After(l.last) { return } l.tokens += now.Sub(l.last).Seconds() * l.rate if l.tokens > float64(l.burst) { l.tokens = float64(l.burst) } l.last = now }