Add internal/autoscaling/autoscaler.go
This commit is contained in:
204
internal/autoscaling/autoscaler.go
Normal file
204
internal/autoscaling/autoscaler.go
Normal file
@@ -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()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user