036c0a8d63
Implementa fase 1 del issue 0016: - Tipos RateLimiter, RateLimitConfig y RateLimitResult en types/infra/ - rate_limiter_new: constructor con validacion rate/burst > 0 - rate_limiter_check: evalua token bucket por key, calcula Allowed/Remaining/ResetAt/RetryAfter - rate_limit_headers (pure): construye headers IETF X-RateLimit-* y Retry-After - rate_limiter_cleanup: goroutine GC de entries inactivas con stop idempotente Sin dependencias externas (no Redis). sync.Mutex + map. Algoritmo token bucket estandar con recarga lineal proporcional al tiempo transcurrido.
55 lines
1.6 KiB
Go
55 lines
1.6 KiB
Go
package infra
|
|
|
|
import (
|
|
"math"
|
|
"time"
|
|
)
|
|
|
|
// RateLimiterCheck evalua si un request identificado por key puede consumir un token.
|
|
// Recarga tokens segun el tiempo transcurrido desde el ultimo check (token bucket).
|
|
// Retorna RateLimitResult con Allowed, Remaining, ResetAt y RetryAfter.
|
|
// Es seguro para uso concurrente.
|
|
func RateLimiterCheck(rl *RateLimiter, key string) RateLimitResult {
|
|
rl.mu.Lock()
|
|
defer rl.mu.Unlock()
|
|
|
|
now := time.Now()
|
|
client, ok := rl.clients[key]
|
|
if !ok {
|
|
// Primera vez para esta key: bucket lleno menos el token consumido.
|
|
client = &rateLimiterClient{tokens: float64(rl.burst), lastSeen: now}
|
|
rl.clients[key] = client
|
|
} else {
|
|
// Recargar tokens segun tiempo transcurrido.
|
|
elapsed := now.Sub(client.lastSeen).Seconds()
|
|
client.tokens = math.Min(float64(rl.burst), client.tokens+elapsed*rl.rate)
|
|
client.lastSeen = now
|
|
}
|
|
|
|
if client.tokens >= 1 {
|
|
client.tokens -= 1
|
|
remaining := int(math.Floor(client.tokens))
|
|
// ResetAt: cuando volvera a estar lleno.
|
|
missing := float64(rl.burst) - client.tokens
|
|
secondsToFull := missing / rl.rate
|
|
return RateLimitResult{
|
|
Allowed: true,
|
|
Remaining: remaining,
|
|
ResetAt: now.Add(time.Duration(secondsToFull * float64(time.Second))),
|
|
RetryAfter: 0,
|
|
}
|
|
}
|
|
|
|
// Sin tokens suficientes — calcular cuanto esperar.
|
|
needed := 1 - client.tokens
|
|
retryAfter := needed / rl.rate
|
|
missing := float64(rl.burst) - client.tokens
|
|
secondsToFull := missing / rl.rate
|
|
return RateLimitResult{
|
|
Allowed: false,
|
|
Remaining: 0,
|
|
ResetAt: now.Add(time.Duration(secondsToFull * float64(time.Second))),
|
|
RetryAfter: retryAfter,
|
|
}
|
|
}
|