172 lines
4.9 KiB
Go
172 lines
4.9 KiB
Go
// Package ratelimit implements per-tenant and per-user token bucket rate limiting.
|
|
// Buckets are kept in memory for fast Allow() checks; configuration is persisted
|
|
// in PostgreSQL and refreshed on admin mutations without restart.
|
|
package ratelimit
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
|
|
"go.uber.org/zap"
|
|
"golang.org/x/time/rate"
|
|
)
|
|
|
|
// RateLimitConfig holds rate limiting parameters for one tenant.
|
|
// When IsEnabled is false, all requests are allowed regardless of token counts.
|
|
type RateLimitConfig struct {
|
|
TenantID string
|
|
RequestsPerMin int // tenant-wide requests per minute
|
|
BurstSize int // tenant-wide burst capacity
|
|
UserRPM int // per-user requests per minute within the tenant
|
|
UserBurst int // per-user burst capacity
|
|
IsEnabled bool // false → bypass all limits for this tenant
|
|
}
|
|
|
|
// Limiter is a thread-safe rate limiter backed by per-tenant and per-user
|
|
// token buckets. It is the zero value of a sync.RWMutex-protected map.
|
|
type Limiter struct {
|
|
mu sync.RWMutex
|
|
tenantBkts map[string]*rate.Limiter // key: tenantID
|
|
userBkts map[string]*rate.Limiter // key: tenantID:userID
|
|
configs map[string]RateLimitConfig
|
|
defaultCfg RateLimitConfig
|
|
logger *zap.Logger
|
|
}
|
|
|
|
// New creates a Limiter with the given default configuration applied to every
|
|
// tenant that has no explicit per-tenant override.
|
|
func New(defaultCfg RateLimitConfig, logger *zap.Logger) *Limiter {
|
|
return &Limiter{
|
|
tenantBkts: make(map[string]*rate.Limiter),
|
|
userBkts: make(map[string]*rate.Limiter),
|
|
configs: make(map[string]RateLimitConfig),
|
|
defaultCfg: defaultCfg,
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// Allow returns true if both the tenant-wide and per-user budgets allow the
|
|
// request. Returns false (→ HTTP 429) if either limit is exceeded or if the
|
|
// tenant config has IsEnabled=false (always allowed in that case).
|
|
func (l *Limiter) Allow(tenantID, userID string) bool {
|
|
cfg := l.configFor(tenantID)
|
|
if !cfg.IsEnabled {
|
|
return true
|
|
}
|
|
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
|
|
// Tenant bucket
|
|
tb, ok := l.tenantBkts[tenantID]
|
|
if !ok {
|
|
tb = newBucket(cfg.RequestsPerMin, cfg.BurstSize)
|
|
l.tenantBkts[tenantID] = tb
|
|
}
|
|
if !tb.Allow() {
|
|
l.logger.Debug("rate limit: tenant bucket exceeded",
|
|
zap.String("tenant_id", tenantID))
|
|
return false
|
|
}
|
|
|
|
// User bucket (key: tenantID:userID)
|
|
userKey := tenantID + ":" + userID
|
|
ub, ok := l.userBkts[userKey]
|
|
if !ok {
|
|
ub = newBucket(cfg.UserRPM, cfg.UserBurst)
|
|
l.userBkts[userKey] = ub
|
|
}
|
|
if !ub.Allow() {
|
|
l.logger.Debug("rate limit: user bucket exceeded",
|
|
zap.String("tenant_id", tenantID),
|
|
zap.String("user_id", userID))
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// SetConfig stores a per-tenant config and resets the tenant's buckets so the
|
|
// new limits take effect immediately (next request builds fresh buckets).
|
|
func (l *Limiter) SetConfig(cfg RateLimitConfig) {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
|
|
l.configs[cfg.TenantID] = cfg
|
|
|
|
// Invalidate old buckets so they are rebuilt with the new limits.
|
|
delete(l.tenantBkts, cfg.TenantID)
|
|
for k := range l.userBkts {
|
|
if len(k) > len(cfg.TenantID)+1 && k[:len(cfg.TenantID)+1] == cfg.TenantID+":" {
|
|
delete(l.userBkts, k)
|
|
}
|
|
}
|
|
}
|
|
|
|
// DeleteConfig removes a per-tenant override, reverting to default limits.
|
|
// Existing buckets are reset immediately.
|
|
func (l *Limiter) DeleteConfig(tenantID string) {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
|
|
delete(l.configs, tenantID)
|
|
delete(l.tenantBkts, tenantID)
|
|
for k := range l.userBkts {
|
|
if len(k) > len(tenantID)+1 && k[:len(tenantID)+1] == tenantID+":" {
|
|
delete(l.userBkts, k)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ListConfigs returns all explicit per-tenant configurations (not the default).
|
|
func (l *Limiter) ListConfigs() []RateLimitConfig {
|
|
l.mu.RLock()
|
|
defer l.mu.RUnlock()
|
|
|
|
out := make([]RateLimitConfig, 0, len(l.configs))
|
|
for _, c := range l.configs {
|
|
out = append(out, c)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// GetConfig returns the effective config for a tenant (explicit or default).
|
|
func (l *Limiter) GetConfig(tenantID string) RateLimitConfig {
|
|
return l.configFor(tenantID)
|
|
}
|
|
|
|
// configFor returns the tenant-specific config if one exists, otherwise default.
|
|
func (l *Limiter) configFor(tenantID string) RateLimitConfig {
|
|
l.mu.RLock()
|
|
cfg, ok := l.configs[tenantID]
|
|
l.mu.RUnlock()
|
|
if ok {
|
|
return cfg
|
|
}
|
|
c := l.defaultCfg
|
|
c.TenantID = tenantID
|
|
return c
|
|
}
|
|
|
|
// newBucket creates a token bucket refilling at rpm tokens per minute with the
|
|
// given burst capacity. Uses golang.org/x/time/rate (token bucket algorithm).
|
|
func newBucket(rpm, burst int) *rate.Limiter {
|
|
if rpm <= 0 {
|
|
rpm = 1
|
|
}
|
|
if burst <= 0 {
|
|
burst = 1
|
|
}
|
|
// rate.Limit is tokens per second; convert RPM → RPS.
|
|
rps := rate.Limit(float64(rpm) / 60.0)
|
|
return rate.NewLimiter(rps, burst)
|
|
}
|
|
|
|
// userKey builds the map key for per-user buckets.
|
|
func userKey(tenantID, userID string) string {
|
|
return fmt.Sprintf("%s:%s", tenantID, userID)
|
|
}
|
|
|
|
// Ensure userKey is used (suppress unused warning from compiler).
|
|
var _ = userKey
|