RES1 - Resiliencia y parámetros de reintento (HTTP)
|
Comenzando
Este paso del tutorial utiliza como punto de partida la solución Si en cualquier momento quieres ver la solución de este paso, la encontrarás en la solución |
En CON3 - Escribir tu propio conector (HTTP), conectamos nuestra aplicación con un sistema externo de fraude de pruebas. Esto nos dio una conexión síncrona con un sistema externo, que es inherentemente menos estable que usar Kafka o JMS. Y nuestro paisaje en este punto del tutorial se ve así:
En este tutorial veremos cómo controlar la resiliencia y los parámetros de reintento para maximizar las posibilidades de que la llamada HTTP tenga éxito. Lo haremos simulando fallos del fraude-sim, de modo que las llamadas HTTP a ese servicio fallen.
Arrancar la aplicación
Si el entorno no está en ejecución, debemos iniciar el entorno docker. Arranca la aplicación como antes (las instrucciones están disponibles en Revisión de la aplicación inicial si necesitas recordatorio).
Esto debería arrancar todas las aplicaciones y simuladores. Podemos comprobar si los contenedores están en marcha y sanos con el comando:
docker ps -a
Validar procesamiento BAU
Comprobemos primero que todo funciona en BAU con todos los endpoints de los simuladores activos. Envía un pago:
curl -X POST localhost:8080/submit -H 'Content-Type: application/json' -d '{"value": "25"}' | jq
Comprobando el pago en la Developer App podemos ver los mensajes enviados e identificar los mensajes OlafRequest y OlafResponse al fraude-sim (buscar por unit of work id, clic en view, clic en ipf tutorial flow, clic en messages):
Prueba de escenario de fallo
Suponiendo que todo está bien en BAU, probemos el escenario en el que fraude-sim está caído y no llegan OlafResponses. La manera más sencilla es parar el contenedor del fraude-sim:
docker stop fraud-sim
Con el contenedor parado, enviamos otra petición de pago:
curl -X POST localhost:8080/submit -H 'Content-Type: application/json' -d '{"value": "24"}' | jq
Comprobando el pago de nuevo en Developer App deberías ver que se envía el OlafRequest pero no llega el OlafResponse, y el estado de la transacción aparece como REJECTED (esto es porque la petición ha hecho timeout y el flujo ha pasado a rechazado):
Finalmente, desde Developer App podemos ver el system event que se ha generado para este fallo:
También vale la pena revisar los logs del contenedor para ver la excepción y los errores específicos (esto será importante al configurar el servicio para reintentar la llamada HTTP). Verás que no hay más errores; el procesamiento está efectivamente detenido con la configuración actual:
07-05-2025 17:33:51.180 [ipf-flow-akka.actor.default-dispatcher-58] ERROR c.i.ipf.core.connector.SendConnector.lambda$send$12 - Sending via Fraud completed exceptionally for ProcessingContext(associationId=AssociationId(value=IpftutorialflowV2|b1a09a4d-5bb8-4d32-b262-c5a8c100f03b), checkpoint=Checkpoint(value=PROCESS_FLOW_EVENT|IpftutorialflowV2|b1a09a4d-5bb8-4d32-b262-c5a8c100f03b|6), unitOfWorkId=UnitOfWorkId(value=b863295e-fa2f-44d0-9588-2fa62f1301d3), clientRequestId=ClientRequestId(value=90838f2e-d79c-4edc-b122-e5d3e6e1fadc), processingEntity=ProcessingEntity(value=UNKNOWN))
java.util.concurrent.CompletionException: java.lang.IllegalStateException: No closed routees for connector: Fraud. Calls are failing fast
...
..
.
Caused by: java.lang.IllegalStateException: No closed routees for connector: Fraud. Calls are failing fast
at com.iconsolutions.ipf.core.connector.resiliency.ResiliencyPassthrough.sendResiliently(ResiliencyPassthrough.java:125)
... 40 common frames omitted
Caused by: akka.stream.StreamTcpException: Tcp command [Connect(localhost/<unresolved>:8089,None,List(),Some(10 seconds),true)] failed because of java.net.ConnectException: Connection refused
Caused by: java.net.ConnectException: Connection refused
Configurar Timeout y Resiliencia
En el estado actual, la aplicación de tutorial no tiene configurado el reintento de forma proactiva, ni ha establecido ajustes de resiliencia para protegerse de errores intermitentes en la conexión síncrona HTTP.
Consideraciones sobre Action Timeout
Como habrás observado, la petición de Fraude hizo timeout y el flujo avanzó a un estado terminal de Rejected. En DSL 7 - Gestión de timeouts configuramos el Action Timeout en 2 segundos.
Para los fines de este tutorial, queremos dar un poco más de tiempo a esa acción para completar normalmente (lo suficiente para simular un fallo intermitente y permitir que los ajustes de resiliencia reintenten las peticiones). Para ello, debemos incrementar el ajuste en nuestro archivo resources/application.conf:
flow.IpftutorialflowV2.CheckingFraud.CheckFraud.timeout-duration=60s
Configurar los ajustes de resiliencia para reintento
Es posible definir ajustes de resiliencia para reintentar la llamada HTTP dentro de un período definido y a intervalos configurables. La configuración por defecto se muestra a continuación, incluyendo tanto los ajustes del conector como los de resiliencia.
Ahora actualizaremos el parámetro max-attempts de resiliency del conector a 6, lo cual pretende dar suficientes reintentos de la llamada HTTP para que el servicio fraude-sim se recupere (6 intentos, junto con un backoff-multiplier de 2, deberían dar 5 reintentos antes de que expire el call-timeout de 30 segundos).
Añade nuestra configuración al archivo de configuración de la aplicación (resources/application.conf):
fraud {
transport = http
http {
client {
host = "fraud-sim"
port = "8080"
endpoint-url = "/v1"
}
}
connector {
resiliency-settings {
max-attempts = 6
}
}
}
Prueba de escenario de fallo 2
Ya podemos aplicar esta configuración reconstruyendo el contenedor ipf-tutorial-app (mvn clean install -rf :ipf-tutorial-app) y arrancándolo, y luego ejecutar los siguientes pasos de prueba:
GIVEN que fraude-sim está parado y ipf-tutorial-app tiene ajustes de resiliencia para reintentar llamadas HTTP
WHEN se inicia un pago y fraude-sim se recupera dentro del timeout de 30 segundos del conector
THEN el pago completará el procesamiento con retraso y con reintentos evidentes en los logs
docker stop fraud-sim
curl -X POST localhost:8080/submit -H 'Content-Type: application/json' -d '{"value": "23"}' | jq
Espera 5 segundos (esto permitirá al conector reintentar).
docker start fraud-sim
Si observas los logs de ipf-tutorial-app (cambia resources/logback.xml de ipf-tutorial-app para tener <logger name="com.iconsolutions.ipf" level="DEBUG"/>) verás entradas de reintento como estas (nota: esto es la decisión de reintentar; el reintento real ocurre cuando expira el backoff):
07-05-2025 17:57:51.784 [ipf-flow-akka.actor.default-dispatcher-35] WARN c.i.i.c.c.t.HttpConnectorTransport.lambda$processReceivedResponse$da95b82c$1 - Failure reply for association ID [UnitOfWorkId(value=07650576-8664-422b-a7d1-98635c767865)] with exception [OutgoingConnectionBlueprint.UnexpectedConnectionClosureException: The http server closed the connection unexpectedly before delivering responses for 1 outstanding requests] and message [TransportMessage(, httpStatusCode -> 500 Internal Server Error)]
07-05-2025 17:57:51.790 [ipf-flow-akka.actor.default-dispatcher-35] DEBUG c.i.i.c.c.r.ResiliencySettings.lambda$resolveRetryOnSendResultsWhen$6 - retryOnResult decided to retry this attempt since it was a failure: DeliveryReport(outcome=FAILURE, deliveryException=akka.http.impl.engine.client.OutgoingConnectionBlueprint$UnexpectedConnectionClosureException: The http server closed the connection unexpectedly before delivering responses for 1 outstanding requests)
Una vez pase el período de backoff, se producirá el reintento real:
07-05-2025 17:57:54.803 [pool-5-thread-1] DEBUG c.i.i.c.c.r.ResiliencyPassthrough.sendViaTransport - Calling 07650576-8664-422b-a7d1-98635c767865 : using OlafRequestReplyHttpConnectorTransport
Comprobando de nuevo el pago en Developer App, deberías ver que se envía el OlafRequest, pero la respuesta de éxito en la pestaña Messages aparece tras el retraso (aprox. 15 segundos).
-
Puedes configurar de forma flexible los reintentos pensando en el backoff-multiplier y el initial-retry-wait-duration. Por ejemplo
| initialRetryWaitDuration | backoffMultiplier | Primeros 5 intervalos de intento |
|---|---|---|
1 |
2 |
1, 2, 4, 8, 16 |
5 |
2 |
5, 10, 20, 40, 80 |
1 |
5 |
1, 5, 25, 125, 625 |
-
Este reintento ocurrió dentro del timeout de 30 segundos del conector. Por lo tanto, también deberías considerar el call-timeout junto con los ajustes de resiliencia.
-
Tal como está escrito el tutorial actualmente, si el reintento no tiene éxito dentro de esos 60 segundos, esto volverá al flujo y la comprobación de fraude no se habrá completado.
-
Es un buen ejemplo de algo que es transitorio y se resuelve rápidamente. Cuando no sea el caso, tenemos varias opciones: configurar endpoints de transporte adicionales, o "reintentar" desde el flujo definiendo la lógica de negocio apropiada en el DSL de IPF.
-
También tenemos opciones para reaccionar de forma distinta ante respuestas de negocio concretas (usando retryOnResultWhen), para reintentar ante ciertos códigos de error de negocio devueltos por la aplicación llamada. Pero esto debe equilibrarse con cuánto quieres tener en el nivel del conector frente a la lógica dentro del flujo.
-
El componente de resiliencia está implementado con resilience4j. Consulta la documentación de Resilience4j para más información sobre estos parámetros y comportamientos.