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:
2026-04-18 17:11:22 +02:00
parent 3bc2d2573d
commit 036c0a8d63
12 changed files with 456 additions and 0 deletions
+33
View File
@@ -0,0 +1,33 @@
---
name: RateLimitConfig
lang: go
domain: infra
version: "1.0.0"
algebraic: product
definition: |
type RateLimitConfig struct {
RequestsPerSecond float64
BurstSize int
KeyFunc func(r *http.Request) string
CleanupInterval time.Duration
}
description: "Configuracion declarativa para construir un rate limiter HTTP. Agrupa tasa sostenida, rafaga maxima, extractor de clave y frecuencia de GC."
tags: [rate_limit, config, http, middleware, infra]
uses_types: []
file_path: "functions/infra/rate_limiter.go"
---
## Ejemplo
```go
cfg := RateLimitConfig{
RequestsPerSecond: 10,
BurstSize: 20,
KeyFunc: func(r *http.Request) string { return r.Header.Get("X-API-Key") },
CleanupInterval: 5 * time.Minute,
}
```
## Notas
Estructura inmutable que parametriza un limiter. `KeyFunc` nil indica usar la IP del cliente. `CleanupInterval` 0 desactiva el GC automatico. El consumidor decide como inicializar el limiter a partir de esta config.
+33
View File
@@ -0,0 +1,33 @@
---
name: RateLimitResult
lang: go
domain: infra
version: "1.0.0"
algebraic: product
definition: |
type RateLimitResult struct {
Allowed bool
Remaining int
ResetAt time.Time
RetryAfter float64
}
description: "Resultado de evaluar un request contra un RateLimiter. Indica si pasa, cuantos tokens quedan, cuando se rellena el bucket y cuanto esperar antes de reintentar."
tags: [rate_limit, result, http, middleware, infra]
uses_types: []
file_path: "functions/infra/rate_limiter.go"
---
## Ejemplo
```go
result := RateLimiterCheck(rl, "192.168.1.1")
if result.Allowed {
// procesar request
} else {
w.Header().Set("Retry-After", strconv.FormatFloat(result.RetryAfter, 'f', 1, 64))
}
```
## Notas
Snapshot inmutable del estado del bucket en el momento del check. `Remaining` es el numero entero de tokens disponibles tras el check. `ResetAt` es absoluto (UTC). `RetryAfter` es 0 cuando el request es permitido. Se serializa indirectamente a headers HTTP via `RateLimitHeaders`.
+33
View File
@@ -0,0 +1,33 @@
---
name: RateLimiter
lang: go
domain: infra
version: "1.0.0"
algebraic: product
definition: |
type RateLimiter struct {
rate float64
burst int
mu sync.Mutex
clients map[string]*rateLimiterClient
}
description: "Token-bucket rate limiter in-memory. Mantiene un bucket por clave (IP o custom) con tokens recargados a tasa constante. Protegido por mutex para uso concurrente."
tags: [rate_limit, http, middleware, token_bucket, infra]
uses_types: []
file_path: "functions/infra/rate_limiter.go"
---
## Ejemplo
```go
// 10 req/s con rafagas hasta 20
rl := RateLimiterNew(10, 20)
result := RateLimiterCheck(rl, "192.168.1.1")
if !result.Allowed {
// rechazar request
}
```
## Notas
Tipo producto con estado mutable (mapa de clientes). Los campos son privados — se accede via `RateLimiterNew`, `RateLimiterCheck`, `RateLimiterCleanup`. El bucket se llena a `rate` tokens/segundo hasta un maximo de `burst`. Cada request consume 1 token. Sin GC explicito (`RateLimiterCleanup`) los buckets crecen indefinidamente con cada IP unica vista.