fix(infra): gradle_run detecta android-sdk — issue 0076 #2
@@ -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