package main import ( "sync" "time" ) // RateLimiter is a per-IP fixed-window counter used to throttle the public // /book endpoint. It is intentionally simple — in-memory, single-process — // matching the single-tenant deployment model. A multi-replica deployment // would need a shared store (Redis or DB) before this limiter is meaningful. type RateLimiter struct { mu sync.Mutex buckets map[string]*rlBucket perHour int } type rlBucket struct { count int resetsAt time.Time } // NewRateLimiter returns a limiter that allows perHour requests per IP in any // rolling 1-hour window (resetting after the first request in a fresh window). func NewRateLimiter(perHour int) *RateLimiter { return &RateLimiter{ buckets: make(map[string]*rlBucket), perHour: perHour, } } // Allow consumes one allowance for ip; returns false when the window's quota // is exhausted. func (r *RateLimiter) Allow(ip string) bool { r.mu.Lock() defer r.mu.Unlock() now := time.Now() b := r.buckets[ip] if b == nil || now.After(b.resetsAt) { r.buckets[ip] = &rlBucket{count: 1, resetsAt: now.Add(time.Hour)} return true } if b.count >= r.perHour { return false } b.count++ return true } // SweepStale drops buckets whose window has elapsed. Driven by // helpers.StartCleanupLoop on a periodic schedule. func (r *RateLimiter) SweepStale() { r.mu.Lock() defer r.mu.Unlock() now := time.Now() for ip, b := range r.buckets { if now.After(b.resetsAt) { delete(r.buckets, ip) } } }