Files
descrybe/apps/api/internal/email/ratelimit.go
T

69 lines
1.3 KiB
Go
Raw Normal View History

package email
import (
"sync"
"time"
)
// slidingLimiter is an in-process email send budget (per key).
// Not shared across API replicas; RATE_LIMIT_REPLICAS does not divide this limiter.
// Multi-replica hard caps need edge/WAF (or a future shared store).
type slidingLimiter struct {
mu sync.Mutex
window time.Duration
limit int
hits map[string][]time.Time
lastGC time.Time
}
func newSlidingLimiter(limit int, window time.Duration) *slidingLimiter {
if limit <= 0 {
limit = 30
}
if window <= 0 {
window = time.Minute
}
return &slidingLimiter{
window: window,
limit: limit,
hits: make(map[string][]time.Time),
lastGC: time.Now(),
}
}
func (l *slidingLimiter) allow(key string) bool {
now := time.Now()
cutoff := now.Add(-l.window)
l.mu.Lock()
defer l.mu.Unlock()
if now.Sub(l.lastGC) > l.window {
for k, ts := range l.hits {
kept := ts[:0]
for _, t := range ts {
if t.After(cutoff) {
kept = append(kept, t)
}
}
if len(kept) == 0 {
delete(l.hits, k)
} else {
l.hits[k] = kept
}
}
l.lastGC = now
}
ts := l.hits[key]
kept := ts[:0]
for _, t := range ts {
if t.After(cutoff) {
kept = append(kept, t)
}
}
if len(kept) >= l.limit {
l.hits[key] = kept
return false
}
l.hits[key] = append(kept, now)
return true
}