Files
fn_registry/dev/issues/completed/0016-rate-limiting.md
T

7.0 KiB

0016 — Rate Limiting

Metadata

Campo Valor
ID 0016
Estado pendiente
Prioridad media
Tipo feature

Dependencias

  • 0009 (HTTP Server Foundation) — el middleware de rate limiting se integra via http_middleware_chain.

Objetivo

Proteger cualquier API del registry contra abuso con rate limiting in-memory basado en token bucket, sin dependencias externas (no Redis). Funciones componibles que se enchufan al stack de middlewares de 0009.

Contexto

  • sqlite_api y futuras apps HTTP no tienen ninguna proteccion contra abuso. Un cliente puede hacer miles de requests por segundo sin limite.
  • Con las funciones de HTTP server de 0009, integrar rate limiting es cuestion de componer un middleware mas en la chain.
  • Token bucket es el algoritmo estandar para rate limiting HTTP: permite rafagas controladas (burst) mientras mantiene una tasa sostenida (rate).
  • Go stdlib incluye golang.org/x/time/rate pero crear funciones propias permite control total sobre cleanup, headers y key extraction.

Arquitectura

functions/infra/
  rate_limiter_new.go          — NEW: crea rate limiter in-memory
  rate_limiter_new.md          — NEW
  rate_limiter_check.go        — NEW: consulta si un request esta permitido
  rate_limiter_check.md        — NEW
  rate_limit_middleware.go     — NEW: middleware HTTP por IP
  rate_limit_middleware.md     — NEW
  rate_limiter_by_key.go       — NEW: rate limit por clave custom
  rate_limiter_by_key.md       — NEW
  rate_limiter_cleanup.go      — NEW: GC de entries stale
  rate_limiter_cleanup.md      — NEW
  rate_limit_headers.go        — NEW: construye headers estandar
  rate_limit_headers.md        — NEW

types/infra/
  rate_limiter.md              — NEW
  rate_limit_config.md         — NEW
  rate_limit_result.md         — NEW

Diseno

Tipos

// RateLimiter mantiene estado de todos los clientes
type RateLimiter struct {
    rate    float64                  // tokens por segundo
    burst   int                     // capacidad maxima del bucket
    mu      sync.Mutex
    clients map[string]*clientEntry // key -> bucket state
}

type clientEntry struct {
    tokens   float64
    lastSeen time.Time
}

// RateLimitConfig configura el middleware
type RateLimitConfig struct {
    RequestsPerSecond float64                              // tasa sostenida
    BurstSize         int                                  // rafaga maxima
    KeyFunc           func(r *http.Request) string         // extractor de clave (nil = IP)
    CleanupInterval   time.Duration                        // frecuencia de GC
}

// RateLimitResult es el resultado de un check
type RateLimitResult struct {
    Allowed    bool      // request permitido
    Remaining  int       // tokens restantes
    ResetAt    time.Time // cuando se rellena el bucket
    RetryAfter float64   // segundos hasta que se pueda reintentar (0 si allowed)
}

Funciones

Funcion Purity Firma (simplificada)
rate_limiter_new impure (rate float64, burst int) *RateLimiter
rate_limiter_check impure (rl *RateLimiter, key string) RateLimitResult
rate_limit_middleware impure (rl *RateLimiter) Middleware
rate_limiter_by_key impure (rl *RateLimiter, keyFunc func(*http.Request) string) Middleware
rate_limiter_cleanup impure (rl *RateLimiter, maxAge time.Duration, interval time.Duration) func()
rate_limit_headers pure (result RateLimitResult, limit int) http.Header

Token bucket

Cada key tiene un bucket con burst tokens. Se recargan a rate tokens/segundo. Un request consume 1 token. Si no quedan tokens, se rechaza con 429.


Tareas

Fase 1: Core + tipos

  • 1.1 Crear tipos RateLimiter, RateLimitConfig, RateLimitResult en functions/infra/ con .md en types/infra/
  • 1.2 rate_limiter_new — inicializa RateLimiter con rate y burst
  • 1.3 rate_limiter_check — evalua token bucket para una key, retorna RateLimitResult
  • 1.4 rate_limit_headers — construye X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After a partir de RateLimitResult
  • 1.5 rate_limiter_cleanup — goroutine que borra entries sin actividad reciente, retorna func() para cancelar

Fase 2: Middlewares + tests

  • 2.1 rate_limit_middleware — middleware que limita por IP del cliente (extrae de RemoteAddr / X-Forwarded-For)
  • 2.2 rate_limiter_by_key — middleware configurable con keyFunc para limitar por API key, user ID, etc.
  • 2.3 Tests de cada funcion con httptest.NewRecorder
  • 2.4 fn index y verificar con fn show

Ejemplo de uso

// Crear limiter: 10 req/s con burst de 20
rl := infra.RateLimiterNew(10, 20)

// Arrancar cleanup cada 5 minutos, borra entries sin actividad en 10 min
stopCleanup := infra.RateLimiterCleanup(rl, 10*time.Minute, 5*time.Minute)
defer stopCleanup()

// Componer con otros middlewares de 0009
middleware := infra.HttpMiddlewareChain(
    infra.HttpCorsMiddleware([]string{"*"}, []string{"GET", "POST"}),
    infra.RateLimitMiddleware(rl),  // por IP
    infra.HttpLoggerMiddleware(os.Stdout),
)

mux := infra.HttpRouter(routes)
infra.HttpServe(":8484", middleware(mux), ctx)
// Rate limit por API key en vez de IP
keyMiddleware := infra.RateLimiterByKey(rl, func(r *http.Request) string {
    return r.Header.Get("X-API-Key")
})
// Respuesta 429 automatica del middleware:
// HTTP/1.1 429 Too Many Requests
// X-RateLimit-Limit: 10
// X-RateLimit-Remaining: 0
// X-RateLimit-Reset: 1713045600
// Retry-After: 1
// Content-Type: application/json
//
// {"status":429,"code":"rate_limited","message":"too many requests"}

Decisiones de diseno

  • In-memory, no Redis: para el scope del registry (single-process, pocas apps) un sync.Mutex + map es suficiente y evita una dependencia de infraestructura.
  • Token bucket sobre sliding window: permite rafagas legitimas (burst) sin penalizar al cliente por picos puntuales, y es trivial de implementar.
  • Headers IETF draft: sigue draft-ietf-httpapi-ratelimit-headers (X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset). Los clientes pueden adaptar su ritmo sin adivinar.
  • rate_limit_headers como funcion pura: construir headers no requiere I/O, solo formateo. El middleware la usa internamente pero queda disponible para otros usos.
  • Cleanup explicito: el GC goroutine se arranca con parametros configurables y se para con la funcion retornada, sin goroutine leaks.

Riesgos

  • Memoria con muchas IPs unicas: Mitigado con rate_limiter_cleanup que purga entries inactivas periodicamente. Para APIs con millones de IPs distintas habria que migrar a Redis, pero ese no es el caso del registry.
  • IP detras de proxy: X-Forwarded-For puede ser spoofed. Para uso interno es aceptable; para exposicion publica real habria que validar el header contra trusted proxies.