dana-medical-site/internal/api/ratelimit.go
2026-10-14 13:33:13 +00:00

72 lines
2.0 KiB
Go

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
}