diff --git a/internal/autoscaling/autoscaler.go b/internal/autoscaling/autoscaler.go new file mode 100644 index 0000000..fd629a6 --- /dev/null +++ b/internal/autoscaling/autoscaler.go @@ -0,0 +1,204 @@ +/* + * 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() +} \ No newline at end of file