Índice del curso
Lección 19 — El destino no contesta
Por Dorian Chávez · cofundador y Chief Executive Officer de Hábil ·
Lo que vas a lograr: que cuando la API no contesta, el router vuelva a intentar solo, cada vez con más pausa, hasta que la orden llegue. Y que mientras tanto el embarque de esa orden no se adelante: la API tiene que recibir primero la orden y después su embarque.
Antes de empezar necesitas: la lección 18 terminada y el ambiente de la 00 encendido.
Lo nuevo de hoy, en una frase
En la 15 el router aprendió a guardar antes de contestar, y en la 18 a reconocer un mensaje repetido. Hoy pasa lo contrario de la 18: el mensaje no llega, porque el destino no contesta. El router no puede saber si la API recibió la orden; lo que sí puede hacer es volver a intentar. Y como ya sabe reconocer repetidos (la API también: lo viste en la 02 con la llave), reintentar no duplica nada.
La imagen: marcarle a alguien que no contesta. No le marcas cada segundo durante una hora: le marcas, esperas un minuto, vuelves a marcar, esperas dos, luego cuatro. Le das tiempo a que salga de la junta. Y no le cuentas lo segundo antes que lo primero: si la llamada era para avisarle que su paquete ya salió, no le mandas antes un mensaje de «¿ya te llegó?».
1. Prepara tu carpeta
cd lecciones/19-el-destino-no-contesta
cp -R inicio mio
Hoy todo el trabajo va en un archivo nuevo del router, orden-router/reintentos.camel.yaml. Ábrelo y lee el
comentario de arriba completo. Trae un dibujo:
intento 1 ✗ ─ 1 s ─ intento 2 ✗ ─ 2 s ─ intento 3 ✗ ─ 4 s ─ intento 4 ✓ (más o menos 15 % al azar)
Y tres reglas que ya están en el código:
| Regla | Qué significa | Dónde vive |
|---|---|---|
| Solo el router reintenta | Los canales (int-rest, int-bd, int-edi) contestan lo que les pasó y ya | Los canales no tienen reintentos |
| Solo se repite lo que quedó «no se sabe» | Sin respuesta, o un error 5xx. Un 4xx de esta API (su 422) es un «no» del destino y no se repite (en la 20 verás que no todos los 4xx son un «no») | direct:intentar lanza una IOException solo en ese caso |
| Cada intento deja su renglón | Antes de llamar al canal, intentar escribe en la tabla intento; después anota cómo le fue |
reparto.camel.yaml, desde la 15 |
2. Predice
Contesta en un papel:
- La API está apagada cuando llega la orden 4001, y vuelve a los pocos segundos. Mientras tanto, la bodega deja el embarque de la 4001. ¿En qué orden deberían llegarle a la API la orden y el embarque? ¿Y si llegaran al revés?
- Si el router reintenta cada 100 milisegundos, ¿qué le pasa a una API que está arrancando?
- El ERP y el socio sí estaban arriba. ¿Cuántas veces deberían recibir la 4001?
Mira cómo está hoy:
./verifica inicio
La tienda recibe su 202, porque el router guardó. Pero el canal rest lo intenta una sola vez: la API estaba
apagada, el intento queda en no_se_sabe y nadie lo vuelve a intentar. La API regresa unos segundos después y la
4001 nunca le llega.
3. Reintenta con espera creciente
Abre mio/orden-router/reintentos.camel.yaml. Arriba está un routeConfiguration con un onException que atrapa
la IOException que lanza intentar. Hoy solo anota en el log que se rindió. Tu trabajo es decirle cuántas veces
y con qué pausa reintentar, con una redeliveryPolicy dentro del onException:
| TODO | Qué escribes | Para qué |
|---|---|---|
| 1 | maximumRedeliveries: 5 y redeliveryDelay: 1000 |
Hasta cinco reintentos; el primero, al segundo |
| 2 | useExponentialBackOff: true y backOffMultiplier: 2 |
Que cada pausa sea el doble de la anterior: 1, 2, 4, 8, 16 segundos |
| 3 | useCollisionAvoidance: true y collisionAvoidanceFactor: 0.15 |
Que cada pausa varíe un 15 % al azar |
| 4 | retryAttemptedLogLevel: WARN |
Que cada reintento se vea en el log |
La redeliveryPolicy va a la misma altura que handled y steps, dentro del onException. Así se ve el
esqueleto:
- onException:
exception:
- java.io.IOException
redeliveryPolicy:
maximumRedeliveries: 5
…
handled:
constant:
expression: "true"
steps:
- log: …
Fíjate en dos cosas:
- El azar del TODO 3 parece un capricho y no lo es. Imagina tres copias del router (la 12) que pierden la API al mismo tiempo. Sin azar, las tres reintentan al segundo exacto, luego a los dos, luego a los cuatro: la API regresa y recibe tres golpes juntos cada vez. Con un 15 % de variación, los golpes se separan.
- El reintento es «en el lugar». La cola
seda:repartirtiene un solo consumidor, y la pausa lo detiene. Lo que viene detrás en la cola, como el embarque de la 4001, espera su turno. Eso es lo que conserva el orden.
4. Rompe uno a propósito
Con tus reintentos terminados, cambia maximumRedeliveries a 1 y corre ./verifica.
Antes, predice: la verifica deja la API apagada unos seis segundos. Con un solo reintento al segundo, ¿llega la orden a la API?
No llega. El primer intento falla, el reintento al segundo también, y el router se rinde mientras la API sigue
apagada. Sale en rojo la marca de los intentos de rest y la de la API. La operación se queda en no_se_sabe, y eso es
correcto: el router no inventa que la entregó. Pero muestra el costo de quedarse corto: cinco reintentos con
espera creciente cubren unos treinta segundos de caída; uno solo, un segundo. Regresa a 5.
5. Comprueba
./verifica
verifica apaga la API, manda la 4001, deja su embarque en el buzón, espera unos segundos y vuelve a prender la
API. Está terminada cuando ves las seis marcas verdes:
✅ Arrancaron el router (con sus reintentos), los tres canales y el sondeo
✅ Con la API apagada, la tienda recibió su 202: lo suyo quedó guardado
✅ Estado: el canal rest reintentó en el lugar: (…) intentos; esperas (…); desenlaces (…)
✅ Estado: el ERP y el socio no se repitieron: aceptados al primer intento (bd=aceptada/1 edi=aceptada/1)
✅ API: recibió la orden y después su embarque, y la 4001 quedó EMBARCADO
✅ ERP: la 4001 está EMBARCADO
Lee la tercera marca con calma. Te dice cuántos intentos hubo, cuánto esperó entre uno y otro y cómo terminó cada
uno. Las esperas cambian en cada corrida por el azar, pero siempre crecen, y el último intento es el único
aceptada.
¿No te corre? ./verifica solucion prueba la nuestra, y
diff mio/orden-router/reintentos.camel.yaml solucion/orden-router/reintentos.camel.yaml te dice en qué son
distintas.
| Lo que ves | Qué haces |
|---|---|
esperaba 3 o más intentos de rest y hay 1 |
Falta la redeliveryPolicy, o quedó fuera del onException |
esperaba 3 o más intentos de rest y las esperas no crecen |
Falta useExponentialBackOff: true |
esperaba la orden antes que el embarque y la orden «no llegó» |
Pocos reintentos o pausas muy cortas: el router se rindió antes de que volviera la API |
El ERP y el socio quedaron con más de un intento |
Pusiste reintentos también en otro lado: solo van en reintentos.camel.yaml |
El puerto 8080 está ocupado |
Quedaron arriba los servicios de otra corrida: DONDE=mio docker compose -f compose.yaml down |
Si la verifica se corta a la mitad, la API se vuelve a prender sola al salir. Si aun así la ves apagada:
docker compose -f ../../infra/compose.yaml start api-pedidos.
Lo que esto enseña fuera de la práctica
Reintentar solo es seguro si el destino reconoce los repetidos. El router no sabe si el primer intento llegó. Si llegó y reintenta, la API recibe la orden dos veces, y lo que impide el duplicado es su llave de idempotencia (la 02). Reintentos sin idempotencia son duplicados con otro nombre.
La espera creciente con azar es la práctica habitual, y tiene nombre: exponential backoff with jitter. La recomiendan los proveedores de nube grandes para cualquier cliente de sus servicios. Espera fija, o le das tiempo de más al que ya volvió o golpeas al que sigue arrancando.
Uno solo reintenta. Si el router reintenta cinco veces y el canal también cinco, un solo envío puede volverse treinta y seis llamadas. Se elige un lugar, y el resto contesta lo que le pasó.
El orden tiene un precio. Reintentar en el lugar detiene la cola: mientras la API no contesta, nada de lo que viene detrás avanza. Aquí vale la pena, porque un embarque antes que su orden es un error. Cuando un destino caído detiene demasiado a los demás, hace falta otra herramienta: es la lección 22.
Ejercicio: la bitácora de los intentos
Con tu versión terminada, corre ./verifica y entra a la base:
docker compose -f ../../infra/compose.yaml exec estado psql -U router -d estado
- Consulta los intentos del canal rest para la creación de la 4001, con su hora:
SELECT i.n, i.inicio, i.desenlace FROM intento i JOIN operacion p ON p.id = i.operacion WHERE p.orden = '4001' AND p.momento = 'creada' AND p.canal = 'rest' ORDER BY i.n;Resta a mano la hora de cada intento menos la del anterior. ¿Se parecen a 1, 2, 4 y 8 segundos? ¿Por qué no son exactas? - Suma las cinco pausas de la política (1 + 2 + 4 + 8 + 16). Si la API tardara un minuto en volver, ¿qué pasaría con la orden? ¿En qué estado quedaría, y qué haría falta para que llegara? (Lo resuelve la lección 23.)
- Explica con tus palabras por qué el embarque de la 4001 llegó después que la orden, aunque la bodega lo dejó mientras la API estaba apagada.
Por qué se hizo así
Lo que decidimos en esta lección, con lo que descartamos y qué nos haría cambiar de idea:
- Se reintenta en el lugar, solo en el router, con espera creciente
Repaso
- ¿Qué respuestas del destino se reintentan y cuáles no? ¿Por qué el 422 de esta API no?
- ¿Para qué sirve que cada pausa sea el doble de la anterior?
- ¿Para qué sirve el 15 % de azar? Explícalo con tres copias del router.
- ¿Por qué reintentar no duplicó la orden en la API?
- La pregunta de cierre: reintentar «en el lugar» conserva el orden. ¿Por qué, y qué cuesta? Usa el embarque de la 4001 como ejemplo.
Siguiente: en la lección 20 la API sí contesta, pero no siempre dice «sí». A veces dice «no», y a veces tarda tanto que se corta la llamada. ¿Escribió o no escribió?
¿Prefiere correo? Escríbanos a hola@habil.mx