Files
fn_registry/functions/infra/rate_limiter_check.go
egutierrez 036c0a8d63 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.
2026-04-18 17:11:22 +02:00

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,
}
}