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,27 @@
|
|||||||
|
package infra
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RateLimitHeaders construye los headers IETF estandar de rate limiting a partir de un resultado.
|
||||||
|
// Setea X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset y, si no esta permitido, Retry-After.
|
||||||
|
// limit es la capacidad total (burst) que se anuncia al cliente.
|
||||||
|
// Funcion pura — solo formatea, no hace I/O.
|
||||||
|
func RateLimitHeaders(result RateLimitResult, limit int) http.Header {
|
||||||
|
h := http.Header{}
|
||||||
|
h.Set("X-RateLimit-Limit", strconv.Itoa(limit))
|
||||||
|
h.Set("X-RateLimit-Remaining", strconv.Itoa(result.Remaining))
|
||||||
|
h.Set("X-RateLimit-Reset", strconv.FormatInt(result.ResetAt.Unix(), 10))
|
||||||
|
if !result.Allowed {
|
||||||
|
// Retry-After segun RFC 7231: numero entero de segundos.
|
||||||
|
retryAfter := int(math.Ceil(result.RetryAfter))
|
||||||
|
if retryAfter < 1 {
|
||||||
|
retryAfter = 1
|
||||||
|
}
|
||||||
|
h.Set("Retry-After", strconv.Itoa(retryAfter))
|
||||||
|
}
|
||||||
|
return h
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
---
|
||||||
|
name: rate_limit_headers
|
||||||
|
kind: function
|
||||||
|
lang: go
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: pure
|
||||||
|
signature: "func RateLimitHeaders(result RateLimitResult, limit int) http.Header"
|
||||||
|
description: "Construye los headers IETF estandar de rate limiting (X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset y Retry-After cuando se rechaza). Funcion pura, solo formatea."
|
||||||
|
tags: [rate_limit, http, headers, middleware, infra]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: [RateLimitResult_go_infra]
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: ""
|
||||||
|
imports: [math, net/http, strconv]
|
||||||
|
params:
|
||||||
|
- name: result
|
||||||
|
desc: "RateLimitResult devuelto por RateLimiterCheck"
|
||||||
|
- name: limit
|
||||||
|
desc: "capacidad total (burst) anunciada al cliente en X-RateLimit-Limit"
|
||||||
|
output: "http.Header con los 3 headers X-RateLimit-* y, cuando result.Allowed es false, Retry-After (segundos enteros)"
|
||||||
|
tested: true
|
||||||
|
tests: ["setea X-RateLimit-Limit con el burst", "setea X-RateLimit-Remaining con result.Remaining", "setea X-RateLimit-Reset como timestamp unix", "incluye Retry-After cuando no esta permitido", "no incluye Retry-After cuando esta permitido"]
|
||||||
|
test_file_path: "functions/infra/rate_limit_headers_test.go"
|
||||||
|
file_path: "functions/infra/rate_limit_headers.go"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```go
|
||||||
|
result := RateLimiterCheck(rl, "192.168.1.1")
|
||||||
|
headers := RateLimitHeaders(result, 20)
|
||||||
|
for k, v := range headers {
|
||||||
|
w.Header()[k] = v
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
Funcion pura — sin I/O, sin estado, deterministica para una misma entrada. Sigue `draft-ietf-httpapi-ratelimit-headers`. `Retry-After` se redondea hacia arriba en segundos enteros (RFC 7231) y nunca se setea por debajo de 1 (sino el cliente reintenta inmediatamente). El timestamp en `X-RateLimit-Reset` es Unix epoch en segundos.
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package infra
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// rateLimiterClient mantiene el estado de un bucket para una clave concreta.
|
||||||
|
type rateLimiterClient struct {
|
||||||
|
tokens float64
|
||||||
|
lastSeen time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// RateLimiter mantiene el estado token-bucket de todos los clientes en memoria.
|
||||||
|
// rate son los tokens recargados por segundo, burst es la capacidad maxima del bucket.
|
||||||
|
// El campo clients es un mapa key -> bucket protegido por mu.
|
||||||
|
type RateLimiter struct {
|
||||||
|
rate float64
|
||||||
|
burst int
|
||||||
|
mu sync.Mutex
|
||||||
|
clients map[string]*rateLimiterClient
|
||||||
|
}
|
||||||
|
|
||||||
|
// RateLimitConfig parametriza el middleware de rate limiting.
|
||||||
|
// KeyFunc extrae la clave del request (nil = IP del cliente).
|
||||||
|
// CleanupInterval controla la frecuencia del GC de entries inactivas (0 = no GC).
|
||||||
|
type RateLimitConfig struct {
|
||||||
|
RequestsPerSecond float64
|
||||||
|
BurstSize int
|
||||||
|
KeyFunc func(r *http.Request) string
|
||||||
|
CleanupInterval time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// RateLimitResult es el resultado de evaluar un request contra el limiter.
|
||||||
|
// Allowed indica si el request puede pasar.
|
||||||
|
// Remaining son los tokens restantes en el bucket despues del check.
|
||||||
|
// ResetAt es el momento en que el bucket vuelve a estar lleno.
|
||||||
|
// RetryAfter son los segundos hasta que se pueda reintentar (0 si Allowed).
|
||||||
|
type RateLimitResult struct {
|
||||||
|
Allowed bool
|
||||||
|
Remaining int
|
||||||
|
ResetAt time.Time
|
||||||
|
RetryAfter float64
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
---
|
||||||
|
name: rate_limiter_check
|
||||||
|
kind: function
|
||||||
|
lang: go
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "func RateLimiterCheck(rl *RateLimiter, key string) RateLimitResult"
|
||||||
|
description: "Evalua un request del cliente identificado por key contra el token bucket. Recarga tokens segun el tiempo transcurrido y consume uno si esta permitido. Seguro para uso concurrente (mutex interno)."
|
||||||
|
tags: [rate_limit, http, middleware, token_bucket, check, infra]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: [RateLimiter_go_infra, RateLimitResult_go_infra]
|
||||||
|
returns: [RateLimitResult_go_infra]
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: [math, time]
|
||||||
|
params:
|
||||||
|
- name: rl
|
||||||
|
desc: "puntero al RateLimiter creado con RateLimiterNew"
|
||||||
|
- name: key
|
||||||
|
desc: "identificador del cliente (IP, API key, user ID, etc.)"
|
||||||
|
output: "RateLimitResult con Allowed, Remaining, ResetAt y RetryAfter calculados a partir del estado del bucket"
|
||||||
|
tested: true
|
||||||
|
tests: ["primer request siempre allowed con burst-1 remaining", "consumir todos los tokens bloquea siguiente request", "los tokens se recargan con el paso del tiempo", "retryAfter es positivo cuando se rechaza", "keys distintas tienen buckets independientes"]
|
||||||
|
test_file_path: "functions/infra/rate_limiter_check_test.go"
|
||||||
|
file_path: "functions/infra/rate_limiter_check.go"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```go
|
||||||
|
rl := RateLimiterNew(10, 20)
|
||||||
|
|
||||||
|
result := RateLimiterCheck(rl, "192.168.1.1")
|
||||||
|
if !result.Allowed {
|
||||||
|
// sleep(result.RetryAfter * time.Second) o 429
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
Funcion impura porque depende de tiempo wall-clock (`time.Now`) y muta el estado del limiter. Algoritmo token bucket clasico: cada bucket se recarga linealmente a `rate` tokens/segundo hasta `burst`. Un request consume 1 token. La primera vez que aparece una key se crea el bucket lleno (descontando ya el primer token). El mutex asegura uso seguro desde multiples goroutines.
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package infra
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// RateLimiterCleanup arranca una goroutine que purga periodicamente las entries
|
||||||
|
// del limiter que no han tenido actividad en maxAge.
|
||||||
|
// Retorna una funcion que detiene la goroutine cuando se invoca.
|
||||||
|
// interval controla la frecuencia del barrido. maxAge es la edad maxima sin actividad.
|
||||||
|
func RateLimiterCleanup(rl *RateLimiter, maxAge time.Duration, interval time.Duration) func() {
|
||||||
|
if interval <= 0 {
|
||||||
|
interval = time.Minute
|
||||||
|
}
|
||||||
|
if maxAge <= 0 {
|
||||||
|
maxAge = 10 * time.Minute
|
||||||
|
}
|
||||||
|
|
||||||
|
stop := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(interval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-stop:
|
||||||
|
return
|
||||||
|
case now := <-ticker.C:
|
||||||
|
rl.mu.Lock()
|
||||||
|
for key, client := range rl.clients {
|
||||||
|
if now.Sub(client.lastSeen) > maxAge {
|
||||||
|
delete(rl.clients, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rl.mu.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
var stopped bool
|
||||||
|
return func() {
|
||||||
|
if stopped {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stopped = true
|
||||||
|
close(stop)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
---
|
||||||
|
name: rate_limiter_cleanup
|
||||||
|
kind: function
|
||||||
|
lang: go
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "func RateLimiterCleanup(rl *RateLimiter, maxAge time.Duration, interval time.Duration) func()"
|
||||||
|
description: "Arranca una goroutine que purga periodicamente las entries del RateLimiter sin actividad reciente. Retorna una funcion para detener el GC sin leaks de goroutine."
|
||||||
|
tags: [rate_limit, http, middleware, cleanup, gc, goroutine, infra]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: [RateLimiter_go_infra]
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: [time]
|
||||||
|
params:
|
||||||
|
- name: rl
|
||||||
|
desc: "puntero al RateLimiter cuyas entries se purgaran"
|
||||||
|
- name: maxAge
|
||||||
|
desc: "edad maxima sin actividad antes de purgar una entry (ej: 10*time.Minute)"
|
||||||
|
- name: interval
|
||||||
|
desc: "frecuencia del barrido de purga (ej: 5*time.Minute). 0 usa default 1 minuto"
|
||||||
|
output: "funcion stop sin argumentos que detiene la goroutine de cleanup. Idempotente — llamarla varias veces es seguro"
|
||||||
|
tested: true
|
||||||
|
tests: ["purga entries con lastSeen mas antiguo que maxAge", "no purga entries recientes", "stop detiene la goroutine sin panic", "stop es idempotente"]
|
||||||
|
test_file_path: "functions/infra/rate_limiter_cleanup_test.go"
|
||||||
|
file_path: "functions/infra/rate_limiter_cleanup.go"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```go
|
||||||
|
rl := RateLimiterNew(10, 20)
|
||||||
|
stop := RateLimiterCleanup(rl, 10*time.Minute, 5*time.Minute)
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
// Servir requests... el GC limpia entries inactivas en background.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
Funcion impura — arranca una goroutine, depende de tiempo wall-clock. La goroutine se cierra al invocar la funcion retornada (cierra un channel interno). Sin invocar stop la goroutine vive hasta que el proceso termina (leak). Valores <= 0 para interval o maxAge se sustituyen por defaults seguros (1 min / 10 min). El barrido toma el mutex del limiter — un interval muy corto puede causar contencion en limiters con muchas keys.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package infra
|
||||||
|
|
||||||
|
// RateLimiterNew crea un RateLimiter token-bucket vacio con la tasa y rafaga indicadas.
|
||||||
|
// rate son los tokens recargados por segundo (sostenido).
|
||||||
|
// burst es la capacidad maxima del bucket (rafaga puntual).
|
||||||
|
// Si rate o burst son <= 0 se usa 1 como minimo seguro.
|
||||||
|
func RateLimiterNew(rate float64, burst int) *RateLimiter {
|
||||||
|
if rate <= 0 {
|
||||||
|
rate = 1
|
||||||
|
}
|
||||||
|
if burst <= 0 {
|
||||||
|
burst = 1
|
||||||
|
}
|
||||||
|
return &RateLimiter{
|
||||||
|
rate: rate,
|
||||||
|
burst: burst,
|
||||||
|
clients: make(map[string]*rateLimiterClient),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
---
|
||||||
|
name: rate_limiter_new
|
||||||
|
kind: function
|
||||||
|
lang: go
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "func RateLimiterNew(rate float64, burst int) *RateLimiter"
|
||||||
|
description: "Crea un RateLimiter token-bucket in-memory vacio. rate son los tokens recargados por segundo, burst es la capacidad maxima del bucket. Valores <= 0 se sustituyen por 1 como minimo seguro."
|
||||||
|
tags: [rate_limit, http, middleware, token_bucket, infra]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: [RateLimiter_go_infra]
|
||||||
|
returns: [RateLimiter_go_infra]
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_go_core"
|
||||||
|
imports: []
|
||||||
|
params:
|
||||||
|
- name: rate
|
||||||
|
desc: "tokens recargados por segundo (tasa sostenida, ej: 10 = 10 req/s)"
|
||||||
|
- name: burst
|
||||||
|
desc: "capacidad maxima del bucket (rafaga maxima permitida en un instante, ej: 20)"
|
||||||
|
output: "puntero a RateLimiter inicializado con mapa vacio de clientes, listo para usar con RateLimiterCheck"
|
||||||
|
tested: true
|
||||||
|
tests: ["crea limiter con rate y burst configurados", "valores cero se sustituyen por 1", "el mapa de clientes empieza vacio"]
|
||||||
|
test_file_path: "functions/infra/rate_limiter_new_test.go"
|
||||||
|
file_path: "functions/infra/rate_limiter_new.go"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 10 req/s sostenidos, hasta 20 en rafaga
|
||||||
|
rl := RateLimiterNew(10, 20)
|
||||||
|
|
||||||
|
// 1 req/s, sin rafaga
|
||||||
|
strict := RateLimiterNew(1, 1)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
Funcion impura porque retorna un valor con estado mutable (mapa interno). No hace I/O ni network. La validacion de rate y burst evita panics por configuracion invalida (division por cero, buckets imposibles). Para iniciar el GC de entries inactivas usar `RateLimiterCleanup`.
|
||||||
@@ -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.
|
||||||
@@ -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`.
|
||||||
@@ -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.
|
||||||
Reference in New Issue
Block a user