Files
futriix/internal/autoscaling/autoscaler.go

204 lines
5.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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()
}