Ejercicio 1 — Los timeouts
- El colapso documentado: 20 requests sin timeout contra el lento → 20 hilos/worker bloqueados 60 s (el gunicorn con 4 workers: el servicio ENTERO responde solo 4 requests más en el minuto), el pool de PG sin conexiones libres (los hilos no devuelven), el browse (que no toca la pasarela) con p95 4 s por falta de workers. El fallo de UNA dependencia derrumba TODO: la definición del colapso en cadena. Con timeout 8 s: el daño queda en el checkout (§3: el breaker lo limita más).
- La tabla:
| Llamada | connect | read | Criterio |
|---|---|---|---|
| Pasarela: cobro | 2 s | 8 s | p99 400 ms ×20; < edge 60 s |
| Pasarela: consulta intent | 2 s | 3 s | operación ligera |
| SMTP: envío | 2 s | 5 s | async (29): tolera más |
| Redis: caché | 0.2 s | 0.5 s | si tarda >0.5 s el caché pierde su propósito (38) |
| Postgres | — | statement_timeout 10 s | el linter de la 09 mata la query infinita |
El Redis con timeout CORTO es la regla del caché: cachear debe ser más rápido que no cachear — el caché que tarda 2 s es peor que el miss.
- El test con el timeout real: el fake del gateway lanza
ReadTimeout(noGatewayTimeoutsimulado) → la saga entra UNKNOWN → la reconciliación del 32 (GET con intent) → CONFIRMED. La cadena completa: timeout → no-sé → protocolo: la resiliencia del dinero es el flujo entero, no el parámetro.
Ejercicio 2 — Los reintentos
- La evidencia del flaky: intento 1 falla, 2 falla, 3 pasa:
attempt 1: fail (ConnectError) → sleep 1.0 + 0.31 (jitter) = 1.31 s
attempt 2: fail (TimeoutError) → sleep 2.0 + 0.07 = 2.07 s
attempt 3: OK- La distinción: el 402 (declinación) → 1 llamada (sin retry: el rechazo de negocio no mejora repitiendo: es la compensación del 32); el ConnectError → 3 con backoff. La regla en el cliente:
retryable=(ConnectError, ReadTimeout)— el 4xx jamás.
- El herd: 50 clientes sin jitter al volver el servicio: pico de 50 requests en <100 ms (el servicio que vuelve recibe el doble de golpe: la segunda caída); con jitter (±0.5 s): el pico se reparte en ~500 ms (el histograma de delays muestra la dispersión). El jitter es gratis y es la diferencia entre "el servicio vuelve" y "el servicio vuelve y cae otra vez".
Ejercicio 3 — El breaker
- Los tests de los 3 estados:
def test_abre_tras_umbral():
for _ in range(5): # 5 fallos consecutivos
with pytest.raises(RetryableError): breaker.call(fallar)
with pytest.raises(BreakerOpen): # OPEN: fail-fast, sin tocar la red
breaker.call(fallar)
assert gw.llamadas == 5 # la 6ª JAMÁS salió
def test_half_open_sana_o_reabre():
... abiertos los 30 s ...
breaker.call(exito) # sonda half-open: CLOSED de nuevo, fallos a 0
# la sonda fallida: abierto_hasta += ventana (re-OPEN)- Los números: SIN breaker (pasarela caída, 50 VUs): p95 checkout 8.1 s (todos esperan el timeout), pool del 39 agotado, browse p95 2.4 s (colateral), errores del browse +12%. CON breaker: los requests del pago fallan en ~2 ms con 503+Retry-After, browse p95 280 ms (intacto), pool respirando. El breaker LOCALIZA: el pago cae, el resto no lo nota.
- El breaker observable:
breaker_state{service="gateway"}→ gauge 0/1/2 (closed/open/half) → la alertabreaker_open > 2 min(page: la pasarela lleva 2 min en cuarentena — o el breaker está mal ajustado y martilla en falso). El breaker sin métrica es un fusible en el sótano: funde y nadie lo sabe.
Ejercicio 4 — La degradación
- El flujo completo del modo degradado:
Pasarela caída → breaker OPEN → POST /reservations/{ref}/pay → 503
{"type": ".../payment-unavailable", "Retry-After": 30,
"detail": "Estamos teniendo problemas con los pagos. Tu asiento queda retenido;
reintenta en unos minutos o te avisaremos."}
→ la saga PENDING retiene el asiento (el TTL de la reserva se extiende: la política del negocio)
→ la pasarela vuelve → el retry del 29 re-encola el cobro → reconciliación → CONFIRMED → emailEl usuario NO perdió el asiento: la retención con extensión es la decisión de producto (51: la conversión del pico vale la retención) — el fallback diseñado, no improvisado.
- El degradado de las sugerencias:
FEATURE_SUGGESTIONS=false(el flag del 27 al revés) → el front muestra el selector manual (el mismo seatmap del 00b) → el E2E del 35 en modo degradado: la compra completa SIN sugerencias pasa (el test del modo alternativo: la feature "bonita" muere, el negocio vive).
- El doc (extracto): "Se degrada: caché→origen (lento-correcto), sugerencias→manual, email→cola acumulada (converge), pasarela→503+retención+reintento. JAMÁS se degrada: la unicidad del asiento (10: duplicar el inventario no es degradar, es morir), la validez del pago (el dinero), la autenticación (18: jamás 'auth degradada')". 15 líneas que el on-call lee antes de improvisar.
Ejercicio 5 — El game day
- La cronología del juego 1:
T+0 sandbox caído (docker stop)
T+2s 3 requests del pago esperan timeout
T+8s 3 timeouts → 3 fallos del breaker
T+15s 5º fallo → breaker OPEN (métrica breaker_state=1)
T+16s checkout 503+Retry-After en ~2 ms (el resto del sistema: p95 intacto)
T+2m alerta page: breaker_open > 2 min ✓
T+5m sandbox vuelve → sonda half-open OK → CLOSED → el retry del 29 drena el backlog
T+7m las sagas retenidas se confirman; cero asientos perdidos- El juego 2 (latencia 5 s): el timeout del gateway (8 s) NO corta (5 s < 8 s: la operación "sana pero lenta" pasa con p95 5.1 s) — PERO el edge del 42 (60 s) tampoco, y el usuario del spinner (~10 s) sí ve el dolor: el finding: el timeout de la OPERACIÓN no protege del lento-funcional (el SLO del 46 sí: la alerta del burn dispara). La lección: el timeout protege de la caída; el SLO del lento; el breaker del martilleo — tres herramientas, tres dolores distintos.
- El informe (extracto): gaps: (1) la retención extendida no avisaba al usuario del límite (el detalle del 503 ahora lo dice); (2) la alerta del breaker no existía antes del juego (creada); (3) el runbook de degradación (el doc del ejercicio 4) se escribió DESPUÉS de necesitarlo (la acción P1). Acciones con issue/dueño/fecha. La resiliencia final: lo que sobrevivió el caos: el browse, el pool, el inventario; lo que no: la UX del aviso (corregida).
Resumen del profesor
- Todo remoto lleva timeout por operación (p99 ×20, menor que el edge): el connect y el read son dolores distintos con reintentos distintos.
- Reintento: solo lo idempotente, backoff+jitter, 3-5 intentos, el 4xx jamás — el breaker deja de martillar y LOCALIZA la caída (el resto respira).
- La degradación se diseña (¿qué cae primero? ¿qué jamás cae?) y se prueba con caos controlado: la resiliencia es lo que sobrevive el juego, no lo que dice el código.