// Package ratelimit provides the API request throttle shared by the provider // clients. package ratelimit import ( "context" "sync" "time" ) // Limiter is a token bucket: it enforces a sustained rate of // requestsPerMinute while allowing up to burst requests to proceed // immediately when budget has accrued. // // It used to space every request exactly interval apart, with no burst. That // made max_connections meaningless for anything that goes through the API: // with the default 60 requests/minute, six download workers each needing a // metadata call and a stream-URL call could only start one track per two // seconds, so an album appeared to download serially even though the transfers // themselves were fast. type Limiter struct { interval time.Duration burst int mu sync.Mutex // next is the time the next token becomes available. It is allowed to // trail now by up to (burst-1)*interval, which is what accrues burst // budget while no requests are being made. next time.Time } // New returns a limiter allowing requestsPerMinute sustained, with burst // requests permitted back-to-back. A non-positive requestsPerMinute disables // throttling entirely. // // burst is clamped to at least 1 and to at most one minute's worth of // requests, so a very low rate cannot be bypassed by a large burst. func New(requestsPerMinute int, burst int) *Limiter { if requestsPerMinute <= 0 { return &Limiter{} } if burst < 1 { burst = 1 } if burst > requestsPerMinute { burst = requestsPerMinute } return &Limiter{ interval: time.Minute / time.Duration(requestsPerMinute), burst: burst, } } // Wait blocks until the caller may issue a request, or until ctx is done. // // Budget is reserved at call time, so a cancelled Wait still consumes its // slot. That keeps the sustained rate honest under cancellation, at the cost // of briefly under-using the budget. func (l *Limiter) Wait(ctx context.Context) error { if l.interval <= 0 { return nil } l.mu.Lock() now := time.Now() // Accrue budget for idle time, capped at burst tokens. earliest := now.Add(-time.Duration(l.burst-1) * l.interval) if l.next.Before(earliest) { l.next = earliest } wake := l.next l.next = l.next.Add(l.interval) l.mu.Unlock() delay := time.Until(wake) if delay <= 0 { return nil } timer := time.NewTimer(delay) defer timer.Stop() select { case <-ctx.Done(): return ctx.Err() case <-timer.C: return nil } }