🟢 Caso 04 — Node.js 22 con AbortController y circuit breaker#
Implementacion operativa del caso 04 para estudiar cadenas de timeouts y tormentas de reintentos con evidencia observable, manteniendo paridad funcional con la version Python y aprovechando primitivas nativas del runtime:
AbortController,AbortSignalysetTimeoutcooperativo.
⬅️ Caso 04 · ⚖️ Comparativa de los 7 stacks · 🟢 Perfil de Node.js · 🧬 Todos los perfiles
🎯 Que resuelve#
Modela un endpoint de quote contra un proveedor inestable. Dos politicas:
quote-legacy: 4 reintentos, sin backoff, sin circuit breaker, sin fallback. Cuando el proveedor degrada, multiplica la presion sobre el dependiente (retry storm clasico).quote-resilient: 2 intentos con backoff exponencial + jitter, timeout via AbortController, circuit breaker (umbral 2 fallas, ventana 30s) y fallback a quote cacheado.
Escenarios soportados: ok, slow_provider, flaky_provider, provider_down, burst_then_recover.
💼 Por que importa#
Un proveedor lento + politica de reintentos sin freno es la receta clasica del retry storm: la propia politica pretende mejorar la disponibilidad y termina derrumbando al proveedor (y al cliente). El caso muestra como pasar de "tu sistema es la causa de su propio incidente" a "tu sistema absorbe la falla y sigue util".
🔬 Analisis Tecnico de la Implementacion (Node.js)#
- AbortController como timeout primitivo:
callWithTimeout()crea unAbortControllery registrasetTimeout(() => ac.abort(), timeoutMs). La llamada simulada al proveedor es awaiteable y cancelable: si el timer dispara,signal.abort()rechaza la promise sin esperar la respuesta tardia. Esto es radicalmente distinto atime.sleep()en Python: el Node tira la operacion realmente, libera el slot del event loop y permite que el resto del proceso siga atendiendo. - Implementacion Falla (
legacy): politica{timeout_ms: 360, max_attempts: 4, backoff_base_ms: 0, use_circuit_breaker: false, allow_fallback: false}. Bajoslow_provider, cada intento de 640-730ms es cancelado por AbortController a los 360ms. Total: 4 cancelaciones consecutivas sin esperar entre intentos. La metricaattempts_totalytimeouts_totalpor modo deja visible la presion. Sin fallback, el cliente termina con HTTP 503. - Sanitizacion (
resilient): politica{timeout_ms: 220, max_attempts: 2, backoff_base_ms: 80, use_circuit_breaker: true, allow_fallback: true}. Backoff exponencial entre intentos80 * 2^(attempt-1) + jitter(15-45)ms. Si tras los 2 intentos el contadorconsecutive_failures >= 2, el breaker se abre por 30s. Mientras esta abierto, las requests entran en short-circuit y devuelven elfallback_quotecacheado (HTTP 200 consource: "fallback"). - Concurrencia y estado: Node es single-thread, asi que el estado del breaker (
opened_until,consecutive_failures) no necesita locks. La persistencia entmp/dependency_state.jsonpermite que multiples requests vean el mismo estado del breaker sin race conditions del runtime.
🧱 Servicio#
app→ API Node.js 22 con politicas legacy/resilient, breaker en JSON local y telemetria por modo.
🚀 Arranque#
docker compose -f compose.yml up -d --buildPuerto local: 824 (modo aislado, ver opciones abajo).
Como consumir (dos opciones)#
Hub Node.js (recomendado, 8300 en compose.nodejs.yml): este caso queda servido en http://localhost:8300/04/... junto a los otros 11 casos.
Modo aislado (824 en este compose.yml): levanta solo este caso, util cuando la medicion necesita procesar limpio (sin otros casos compartiendo runtime).
🔎 Endpoints#
curl http://localhost:8300/04/
curl http://localhost:8300/04/health
curl "http://localhost:8300/04/quote-legacy?scenario=slow_provider&customer_id=42&items=3"
curl "http://localhost:8300/04/quote-resilient?scenario=slow_provider&customer_id=42&items=3"
curl http://localhost:8300/04/dependency/state
curl "http://localhost:8300/04/incidents?limit=10"
curl http://localhost:8300/04/diagnostics/summary
curl http://localhost:8300/04/metrics
curl http://localhost:8300/04/metrics-prometheus
curl http://localhost:8300/04/reset-lab🧭 Que observar#
- bajo
slow_provider,quote-legacyacumula 4 timeouts y devuelve 503;quote-resilientactiva breaker tras 2 fallas y empieza a servir fallback; dependency_circuit_openen Prometheus pasa a1cuando el breaker se abre y vuelve a0cuando expira;app_flow_avg_attempts{mode="legacy"}se mantiene cerca de 4;mode="resilient"se mantiene en 1-2 por la combinacion CB + fallback;recent_incidentspermite reconstruir incidentes con suficiente contexto para postmortem.
⚖️ Nota de honestidad#
El proveedor es simulado y los escenarios son determinismo controlado por Math.random() con rangos definidos. El laboratorio no reemplaza un test contra dependencia real bajo carga, pero demuestra el patron y deja la primitiva AbortController como referencia explicita — la misma que se usa con fetch en codigo de produccion.