package api import ( "net/http" "sync" "time" ) // rateLimiter — простой лимитер в памяти по ключу (обычно IP): не больше max // событий за окно window. Хватает для одного экземпляра сервиса. Старые записи // подчищаются фоном, чтобы карта не росла. type rateLimiter struct { mu sync.Mutex hits map[string][]int64 // ключ -> времена событий (unix-наносекунды) } func newRateLimiter() *rateLimiter { rl := &rateLimiter{hits: make(map[string][]int64)} go rl.cleanup() return rl } // allow регистрирует событие и возвращает false, если за окно их уже было max. func (rl *rateLimiter) allow(key string, max int, window time.Duration) bool { now := time.Now().UnixNano() cutoff := now - int64(window) rl.mu.Lock() defer rl.mu.Unlock() kept := rl.hits[key][:0] for _, t := range rl.hits[key] { if t > cutoff { kept = append(kept, t) } } if len(kept) >= max { rl.hits[key] = kept return false } rl.hits[key] = append(kept, now) return true } func (rl *rateLimiter) cleanup() { for range time.Tick(10 * time.Minute) { now := time.Now().UnixNano() rl.mu.Lock() for k, ts := range rl.hits { alive := ts[:0] for _, t := range ts { if t > now-int64(time.Hour) { alive = append(alive, t) } } if len(alive) == 0 { delete(rl.hits, k) } else { rl.hits[k] = alive } } rl.mu.Unlock() } } // tooMany отвечает 429, если ключ превысил лимит. Возвращает true, если запрос надо прервать. func (a *API) tooMany(w http.ResponseWriter, key string, max int, window time.Duration) bool { if a.limiter == nil || a.limiter.allow(key, max, window) { return false } fail(w, http.StatusTooManyRequests, "rate_limited", "Слишком много запросов. Повторите позже.") return true }