Registro de límites de tasa
Una tormenta de reintentos que golpea un limitador de tasa puede generar más líneas de log por segundo de las que un humano jamás leerá; la solución es registrar menos, no registrar nada.
Las políticas de reintento y disyuntor de Failsafe, vistas antes en el curso, protegen al servicio de valuación de un proveedor poco confiable. Un limitador de tasa protege contra un modo de falla completamente distinto: volumen, no confiabilidad. Black Book, Carfax y S&P VIS aplican cada uno sus propias cuotas de llamadas, y tu propio endpoint `/vehicles/{vin}/valuation` necesita la misma protección en la dirección contraria, para que un solo llamador reintentando de forma agresiva no le quite capacidad a todos los demás. El algoritmo clásico y bien entendido para esto es el cubo de tokens (token bucket): un cubo mantiene un número fijo de tokens, se rellena a una tasa constante, y cada solicitud consume un token — cuando el cubo está vacío, la solicitud se limita en lugar de atenderse.
Bucket4j es la biblioteca estándar para esto en la JVM, y conectarla para proteger, digamos, las llamadas a Carfax, consiste en definir un ancho de banda — una capacidad y una tasa de relleno — por proveedor y consultar el cubo antes de cada llamada saliente. `bucket.tryConsumeAndReturnRemaining(1)` devuelve un objeto de sonda que indica si el token fue otorgado y, si no, exactamente cuándo estará disponible el siguiente, que es la información que una línea de log de limitación realmente necesita.
Lo que debe ir en esa línea de log es específico: qué limitador se activó (qué proveedor, o la clave de API de qué llamador), el estado actual del cubo (tokens restantes, tiempo hasta el próximo relleno), y suficiente identidad para rastrear la solicitud — un VIN, un id de llamador, un trace id — de modo que quien esté mirando esto durante un incidente pueda responder "a quién se le limitó, por cuál limitador, y cuándo se recupera" con esa única línea.
Lo que no debe ir ahí es una línea de WARN por cada solicitud rechazada una vez que el sistema está realmente bajo carga. Una tormenta de reintentos golpeando un cubo vacío puede producir cientos o miles de rechazos por segundo, y registrar cada uno individualmente hace dos cosas a la vez: cuesta dinero real en ingestión, y entierra el puñado de líneas de log que realmente habrían ayudado a diagnosticar el pico bajo una pared de ruido idéntico. La solución no es el silencio — es el muestreo o la agregación: registrar una línea representativa de cada N rechazos, o emitir un resumen periódico ("se limitaron 4,812 solicitudes del cliente X en los últimos 60 segundos") en lugar de una línea por solicitud.
La métrica es la que lleva la señal real, y debería existir independientemente de la tasa de muestreo que usen los logs. Incrementar un contador `valuation.ratelimit.throttled`, etiquetado por limitador y por llamador o proveedor, en cada evento de limitación — usando el mismo patrón de Micrometer de la lección anterior — significa que el dashboard ve la tasa real de rechazos en tiempo real aun mientras los logs se limitan a sí mismos deliberadamente hasta un goteo manejable.
Imagina el escenario concreto: arranca un trabajo nocturno por lotes que re-cotiza todo el inventario, se dispara muy por encima de la tasa configurada para Carfax, y el limitador empieza a rechazar. El contador de Datadog se dispara de inmediato y una alerta se activa. Los logs muestreados te dan una o dos líneas representativas que nombran al llamador y al limitador — suficiente para identificar el trabajo por lotes como el culpable en menos de un minuto — en lugar de cincuenta mil líneas de log idénticas o, peor aún, nada en absoluto porque alguien decidió que registrar cada rechazo era demasiado ruido y apagó todo.
Esta es la misma disciplina hacia la que ha apuntado todo el módulo: una barrera de protección es solo la mitad del trabajo. Failsafe protege contra un downstream poco confiable y se combina con métricas que muestran cuándo se activa; el limitador de tasa protege contra el volumen y se combina con métricas y un registro disciplinado que muestran cuándo se activa. Ninguna barrera vale mucho si nadie puede ver que está funcionando.
@Componentclass ProviderRateLimiters {private val buckets = ConcurrentHashMap<String, Bucket>()fun bucketFor(providerName: String): Bucket = buckets.computeIfAbsent(providerName) {Bucket.builder().addLimit(Bandwidth.classic(50, Refill.greedy(50, Duration.ofSeconds(60)))).build()}}
A per-provider token bucket built with Bucket4j: 50 calls per 60-second window, created lazily and cached per provider name.
class RateLimitedValuationProviderClient(private val providerName: String,private val delegate: ValuationProviderClient,private val rateLimiters: ProviderRateLimiters,private val metrics: ProviderMetrics,private val throttleLogSampler: ThrottleLogSampler,) : ValuationProviderClient {override suspend fun fetchValuation(vin: String): ProviderQuote {val bucket = rateLimiters.bucketFor(providerName)val probe = bucket.tryConsumeAndReturnRemaining(1)if (!probe.isConsumed) {metrics.recordThrottle(providerName)if (throttleLogSampler.shouldLog(providerName)) {logger.warn("Throttled call to provider={} vin={} tokensRemaining={} nextRefillMs={}",providerName, vin, probe.remainingTokens, probe.nanosToWaitForRefill / 1_000_000)}throw ProviderRateLimitExceededException(providerName)}return delegate.fetchValuation(vin)}}
Consulting the bucket before a provider call, recording a metric on every throttle, and logging only a sampled subset of rejections.
class ThrottleLogSampler(private val logEvery: Int = 100) {private val counters = ConcurrentHashMap<String, AtomicLong>()fun shouldLog(key: String): Boolean {val count = counters.computeIfAbsent(key) { AtomicLong(0) }.incrementAndGet()return count % logEvery == 1L}}
A sampler that logs only every Nth throttle event per key, so a sustained burst produces a handful of log lines instead of thousands.
🧠 Comprueba tu comprensión
0/1 · 0/1 answered1. During a sustained burst, a rate limiter starts rejecting hundreds of requests per second. What is the biggest risk of logging a WARN line for every single rejection, as opposed to sampling or aggregating?