Circuit Breakers con Failsafe
Cuando un proveedor realmente está caído, lo más considerado que puede hacer un cliente es dejar de llamarlo — y servir una respuesta cacheada en lugar de una falla lenta.
Los reintentos resuelven el problema de un proveedor con un tropiezo breve e independiente, pero asumen algo equivocado cuando un proveedor está realmente caído durante un período prolongado: que volver a intentarlo, con un backoff corto, vale la pena el costo. Si Carfax lleva fallando los últimos dos minutos, cada solicitud de valuación que llega durante esa ventana igual paga el calendario completo de reintentos — tres o cuatro intentos, cada uno con backoff — antes de finalmente rendirse, lo que significa que cada llamador espera el tiempo máximo posible para recibir una falla que se podía haber predicho antes de hacer una sola llamada de red. Un circuit breaker es lo que le permite al servicio notar ese patrón y dejar de pagar ese costo.
Un `CircuitBreaker` transita por tres estados, y los nombres describen un circuito eléctrico a propósito. `CLOSED` es la operación normal: las llamadas fluyen hacia el proveedor real, y el breaker cuenta silenciosamente éxitos y fallas a medida que ocurren. Una vez que las fallas cruzan un umbral configurado, el breaker se dispara a `OPEN`: ahora cada llamada se rechaza de inmediato, sin siquiera tocar la red, durante un retardo fijo. Después de que ese retardo transcurre, el breaker pasa a `HALF_OPEN`, un estado de prueba que deja pasar un pequeño número de llamadas de ensayo para verificar si el proveedor se ha recuperado — si tienen éxito, el breaker se cierra de nuevo y se reanuda el tráfico normal; si fallan, se vuelve a abrir y reinicia el retardo.
Configurar uno se parece bastante a configurar la política de reintentos de la lección anterior, solo que ajustada a una pregunta distinta — no `debería reintentarse esta llamada`, sino `este proveedor ha dejado de ser confiable`. `CircuitBreaker.builder<ValuationResult>()` con `.withFailureThreshold(5, 10)` dispara el breaker en cuanto 5 de las últimas 10 llamadas a Carfax han fallado; `.withDelay(Duration.ofSeconds(30))` es cuánto tiempo permanece `OPEN` antes de permitir un ensayo `HALF_OPEN`; `.withSuccessThreshold(3)` es cuántas de esas llamadas de ensayo necesitan tener éxito antes de que el breaker confíe lo suficiente en el proveedor como para cerrarse de nuevo. Ajusta los números por proveedor — uno con límites de tasa más estrictos podría querer un umbral de falla más bajo y un retardo de apertura más largo que uno que solo ocasionalmente expira.
Un breaker abierto falla rápido, pero fallar rápido sigue siendo fallar, y a quien llama a `VehicleController` no le importa por qué falta la valuación. Aquí es donde un `Fallback` se gana su lugar: en lugar de dejar que una `CircuitBreakerOpenException` se propague hasta arriba, la capturas y sirves algo útil en su lugar — en este servicio, la caché de Redis que ya almacena las búsquedas de valuación por VIN. Una valuación de Carfax de hace diez minutos para este VIN suele ser una respuesta mucho mejor que ninguna respuesta, y devolverla convierte una caída del proveedor en una respuesta ligeramente desactualizada pero con la forma correcta, en lugar de una solicitud fallida.
Componer reintentos, circuit breaker y fallback juntos es una sola línea — `Failsafe.with(fallback, circuitBreaker, retryPolicy)` — pero el orden de los argumentos no es cosmético. Failsafe compone las políticas que le pasas de la más externa a la más interna, en el orden en que las listas, así que esta línea significa: el fallback envuelve al circuit breaker, que envuelve a la política de reintentos, que envuelve a la llamada real. Ese anidamiento es lo que hace que cada política vea exactamente las fallas que le corresponde ver.
Pon el circuit breaker afuera del reintento, y un breaker abierto rechaza la llamada antes de que la política de reintentos siquiera corra — sin intentos desperdiciados, sin retardos de backoff desperdiciados, el fallback se dispara casi al instante. Invierte ese orden — reintento afuera del circuit breaker — y cada intento de reintento tiene que reingresar individualmente al breaker, ser rechazado y dormir todo su retardo de backoff antes de volver a intentarlo, lo que significa que quien llama se queda esperando varios sueños de backoff seguidos aunque ninguno de esos intentos haya llegado jamás a la red. Circuit breaker afuera del reintento es el orden al que hay que recurrir siempre que un breaker abierto deba significar un fallback instantáneo en lugar de una falla lenta y acolchada.
Con esta composición en su lugar, cada llamada que hacen `BlackBookClient`, `CarfaxClient` y `VisClient` se reintenta ante tropiezos transitorios, falla rápido en el momento en que un proveedor está realmente enfermo, y recurre a una respuesta cacheada en lugar de una falla total — todo sin que ninguna de esas clases sepa que existe esta maquinaria. Con esto se cierra este módulo sobre cómo llamar a proveedores de terceros de forma segura; la próxima lección pasa a una pregunta de confiabilidad distinta: cuál de Postgres, Redis y MongoDB debería en realidad guardar cada pieza de los datos de este servicio.
import dev.failsafe.CircuitBreakerimport java.time.Durationval carfaxCircuitBreaker: CircuitBreaker<ValuationResult> = CircuitBreaker.builder<ValuationResult>().handle(RetryableProviderException::class.java).withFailureThreshold(5, 10) // 5 failures out of the last 10 calls trips it.withDelay(Duration.ofSeconds(30)) // stays OPEN for 30s before a trial call.withSuccessThreshold(3) // needs 3 successful trials to CLOSE again.onOpen { log.warn("Carfax circuit breaker OPEN -- failing fast") }.onHalfOpen { log.info("Carfax circuit breaker HALF_OPEN -- testing recovery") }.onClose { log.info("Carfax circuit breaker CLOSED -- back to normal") }.build()
Configures a CircuitBreaker for Carfax with a failure threshold, an open delay, and a success threshold to close again, plus lifecycle logging hooks; it can't run in-browser because dev.failsafe is a real JVM library with no browser equivalent.
import dev.failsafe.Fallbackfun cachedValuationFallback(cache: ValuationCache, vin: String): Fallback<ValuationResult> =Fallback.builder<ValuationResult> { event ->// The breaker is open, or every retry was exhausted -- rather than// propagate the failure up to VehicleController, serve the last// valuation cached in Redis for this VIN. A ten-minute-old price is// a far better answer than a failed request.cache.getStaleValuation(vin) ?: throw NoStaleValuationAvailableException(vin)}.build()
Builds a Fallback that serves a stale Redis-cached valuation whenever the breaker is open or the retries are exhausted, rather than letting the failure reach VehicleController; it can't run in-browser because it depends on real dev.failsafe classes and a live Redis connection.
import dev.failsafe.Failsafeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.withContextimport okhttp3.OkHttpClientimport okhttp3.Requestclass CarfaxClient(private val client: OkHttpClient,private val baseUrl: String,private val cache: ValuationCache) : ValuationProviderClient {override val providerName = "carfax"// Order matters: fallback is outermost so it catches anything that// escapes, circuitBreaker sits next so a call fails fast the instant the// breaker is open, and retryPolicy is innermost so only individual// network attempts get retried. If retryPolicy were outermost instead,// every retry attempt would have to re-enter the open breaker, sleep// through its backoff delay, and get rejected again for no benefit.private fun executorFor(vin: String) =Failsafe.with(cachedValuationFallback(cache, vin), carfaxCircuitBreaker, providerRetryPolicy)override suspend fun fetchValuation(vin: String): ValuationResult =withContext(Dispatchers.IO) {executorFor(vin).get {val request = Request.Builder().url("$baseUrl/history/valuation?vin=$vin").get().build()client.newCall(request).execute().use { response -> parseCarfax(vin, response) }}}}
Composes fallback, circuit breaker, and retry policy into one FailsafeExecutor, with the order chosen so an open breaker fails fast before the retry policy ever runs; it can't run in-browser because it wraps a real OkHttp call to a real network endpoint.
🧠 Comprueba tu comprensión
0/1 · 0/1 answered1. A FailsafeExecutor is built as Failsafe.with(fallback, circuitBreaker, retryPolicy) for Carfax calls. Once carfaxCircuitBreaker has tripped OPEN, what happens the next time fetchValuation is called?