feat(infra): rate limiter token-bucket in-memory + tipos y core funcs
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.
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user