Files

204 lines
5.4 KiB
Go
Raw Permalink Normal View History

2026-06-12 16:23:32 +00:00
/*
* Copyright 2026 Safronov Grigorii
*
* Licensed under the CDDL, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
*
* You may obtain a copy of the License at
* https://opensource.org/licenses/CDDL-1.0
*/
// Файл: internal/api/rate_limiter.go (НОВЫЙ ФАЙЛ)
// Назначение: Rate limiting для API
package api
import (
"fmt"
"net/http"
"sync"
"sync/atomic"
"time"
)
// RateLimiterConfig содержит конфигурацию rate limiter'а
type RateLimiterConfig struct {
RequestsPerSecond int
BurstSize int
CleanupInterval time.Duration
MaxEntries int
}
// DefaultRateLimiterConfig возвращает конфигурацию по умолчанию
func DefaultRateLimiterConfig() *RateLimiterConfig {
return &RateLimiterConfig{
RequestsPerSecond: 100,
BurstSize: 20,
CleanupInterval: 5 * time.Minute,
MaxEntries: 10000,
}
}
// TokenBucket реализует алгоритм token bucket
type TokenBucket struct {
tokens float64
maxTokens float64
refillRate float64
lastRefill time.Time
mu sync.Mutex
}
// NewTokenBucket создаёт новый token bucket
func NewTokenBucket(ratePerSecond float64, burst int) *TokenBucket {
return &TokenBucket{
tokens: float64(burst),
maxTokens: float64(burst),
refillRate: ratePerSecond,
lastRefill: time.Now(),
}
}
// Allow проверяет, разрешён ли запрос
func (tb *TokenBucket) Allow() bool {
tb.mu.Lock()
defer tb.mu.Unlock()
now := time.Now()
elapsed := now.Sub(tb.lastRefill).Seconds()
tb.tokens += elapsed * tb.refillRate
if tb.tokens > tb.maxTokens {
tb.tokens = tb.maxTokens
}
tb.lastRefill = now
if tb.tokens >= 1.0 {
tb.tokens -= 1.0
return true
}
return false
}
// RateLimiter управляет rate limiting для API
type RateLimiter struct {
config *RateLimiterConfig
limiters sync.Map
rejected atomic.Uint64
allowed atomic.Uint64
stopChan chan struct{}
wg sync.WaitGroup
}
// NewRateLimiter создаёт новый rate limiter
func NewRateLimiter(config *RateLimiterConfig) *RateLimiter {
if config == nil {
config = DefaultRateLimiterConfig()
}
rl := &RateLimiter{
config: config,
stopChan: make(chan struct{}),
}
rl.wg.Add(1)
go rl.cleanupLoop()
return rl
}
// getLimiter возвращает или создаёт лимитер для ключа
func (rl *RateLimiter) getLimiter(key string) *TokenBucket {
if val, ok := rl.limiters.Load(key); ok {
return val.(*TokenBucket)
}
limiter := NewTokenBucket(float64(rl.config.RequestsPerSecond), rl.config.BurstSize)
actual, _ := rl.limiters.LoadOrStore(key, limiter)
return actual.(*TokenBucket)
}
// Allow проверяет, разрешён ли запрос для ключа
func (rl *RateLimiter) Allow(key string) bool {
limiter := rl.getLimiter(key)
allowed := limiter.Allow()
if allowed {
rl.allowed.Add(1)
} else {
rl.rejected.Add(1)
}
return allowed
}
// Middleware возвращает middleware для HTTP
func (rl *RateLimiter) Middleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Используем IP + User-Agent как ключ
key := r.RemoteAddr
if forwarded := r.Header.Get("X-Forwarded-For"); forwarded != "" {
key = forwarded
}
if !rl.Allow(key) {
w.Header().Set("X-RateLimit-Limit", fmt.Sprintf("%d", rl.config.RequestsPerSecond))
w.Header().Set("X-RateLimit-Remaining", "0")
w.Header().Set("Retry-After", "1")
http.Error(w, "Rate limit exceeded. Please try again later.", http.StatusTooManyRequests)
return
}
next(w, r)
}
}
// cleanupLoop периодически очищает старые лимитеры
func (rl *RateLimiter) cleanupLoop() {
defer rl.wg.Done()
ticker := time.NewTicker(rl.config.CleanupInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
rl.cleanup()
case <-rl.stopChan:
return
}
}
}
// cleanup удаляет старые лимитеры
func (rl *RateLimiter) cleanup() {
count := 0
rl.limiters.Range(func(key, value interface{}) bool {
count++
if count > rl.config.MaxEntries {
rl.limiters.Delete(key)
}
return true
})
}
// GetStats возвращает статистику rate limiter'а
func (rl *RateLimiter) GetStats() map[string]interface{} {
count := 0
rl.limiters.Range(func(key, value interface{}) bool {
count++
return true
})
return map[string]interface{}{
"active_limiters": count,
"allowed_requests": rl.allowed.Load(),
"rejected_requests": rl.rejected.Load(),
"requests_per_second": rl.config.RequestsPerSecond,
"burst_size": rl.config.BurstSize,
}
}
// Stop останавливает rate limiter
func (rl *RateLimiter) Stop() {
close(rl.stopChan)
rl.wg.Wait()
}