Saltar al contenido

lección 5

Errores, reintentos y bifurcaciones en Step Functions

Gestiona fallos con Retry, Catch y Choice. Haz que tu pipeline se recupere solo de errores transitorios.

55 min

### La realidad: los pipelines fallan. Siempre.

Hasta ahora nuestros pipelines funcionan en el "camino feliz": todo sale bien, cada Lambda devuelve datos válidos, el flujo avanza sin problemas. Pero la realidad de producción es otra: APIs que devuelven timeouts, archivos que llegan corruptos, bases de datos que están siendo reiniciadas, permisos que alguien cambió sin avisar, disco que se llenó, red que se cayó...

Un pipeline de producción no es aquel que nunca falla — eso no existe. Un pipeline de producción es aquel que SABE fallar. Que distingue entre un error transitorio (reintenta y se resuelve solo) y un error permanente (notifica al equipo). Que no se queda en un limbo cuando algo sale mal. Que tiene un plan B.

Lo que le diría a mi yo de hace 10 años: gasta más tiempo diseñando cómo falla tu pipeline que cómo funciona. El camino feliz lo escribes en 1 hora. Los modos de fallo te llevan 3 días. Pero esos 3 días te ahorrarán semanas de 3AM troubleshooting en el futuro.

### Retry: reintentos automáticos para errores transitorios

Step Functions tiene un mecanismo de reintentos built-in. No tienes que programar la lógica de retry tú mismo — la declaras en el JSON y el servicio se encarga. Cada estado Task puede tener un campo Retry que define qué errores reintentar, cuántas veces y con qué backoff.

1{
2 "Ingesta": {
3 "Type": "Task",
4 "Resource": "arn:aws:lambda:eu-west-1:000000000000:function:ingesta",
5 "TimeoutSeconds": 300,
6 "Retry": [
7 {
8 "ErrorEquals": ["Lambda.ServiceException", "Lambda.TooManyRequestsException"],
9 "IntervalSeconds": 5,
10 "MaxAttempts": 3,
11 "BackoffRate": 2.0
12 },
13 {
14 "ErrorEquals": ["States.TaskFailed"],
15 "IntervalSeconds": 10,
16 "MaxAttempts": 2,
17 "BackoffRate": 1.5
18 }
19 ],
20 "ResultPath": "$.ingesta_resultado",
21 "Next": "Transformacion"
22 }
23}

Retry con backoff exponencial: espera 5s, luego 10s, luego 20s entre reintentos

Desglosemos los campos de Retry:

  • ErrorEquals: lista de errores que activan este retry. Puede ser errores de AWS, errores custom de tu Lambda, o el comodín "States.ALL" para cualquier error.
  • IntervalSeconds: cuantos segundos esperar antes del primer reintento.
  • MaxAttempts: numero maximo de reintentos (despues de esto, el error se propaga).
  • BackoffRate: multiplicador de la espera entre reintentos. Con BackoffRate=2, las esperas son: 5s, 10s, 20s (exponencial).
  • MaxDelaySeconds (opcional): techo de la espera. Sin el, el backoff crece sin limite — con IntervalSeconds=60, MaxAttempts=5 y BackoffRate=2 se llega a 960s (16 min). Con MaxDelaySeconds=300 el maximo es 5 min.
  • JitterStrategy (opcional): "FULL" aleatoriza las esperas para que cien ejecuciones que fallaron juntas no vuelvan a golpear la API todas en el mismo segundo.

La analogía del backoff exponencial: imagina que llamas a un amigo y no contesta. No le llamas inmediatamente otra vez — esperas 1 minuto. Si no contesta, esperas 5 minutos. Si sigue sin contestar, esperas 30 minutos. Cada vez esperas más porque probablemente está ocupado y bombardearle con llamadas no ayuda. Eso es backoff exponencial.

### Catch: el plan B cuando los reintentos no bastan

Si después de todos los reintentos el paso sigue fallando, necesitas un plan B. Catch define a qué estado ir cuando un error persiste. Es tu try/catch pero declarativo.

1{
2 "Ingesta": {
3 "Type": "Task",
4 "Resource": "arn:aws:lambda:eu-west-1:000000000000:function:ingesta",
5 "Retry": [
6 {
7 "ErrorEquals": ["Lambda.ServiceException"],
8 "IntervalSeconds": 5,
9 "MaxAttempts": 3,
10 "BackoffRate": 2.0
11 }
12 ],
13 "Catch": [
14 {
15 "ErrorEquals": ["States.ALL"],
16 "ResultPath": "$.error_info",
17 "Next": "NotificarFallo"
18 }
19 ],
20 "ResultPath": "$.ingesta_resultado",
21 "Next": "Transformacion"
22 },
23 "NotificarFallo": {
24 "Type": "Task",
25 "Resource": "arn:aws:lambda:eu-west-1:000000000000:function:notificar_error",
26 "Parameters": {
27 "error.$": "$.error_info",
28 "pipeline": "ventas_diarias",
29 "canal": "slack-data-alerts"
30 },
31 "End": true
32 }
33}

Catch: si la ingesta falla después de 3 reintentos, notifica al equipo y termina

States.ALL no captura TODO. Hay dos errores que se le escapan: States.DataLimitExceeded (el input o output supera los 256 KiB — el tope que vimos en la leccion anterior) y States.Runtime (un fallo del propio motor). El primero lo puedes cazar poniendolo ANTES del States.ALL en la lista de Catch. El segundo no tiene remedio: si el motor falla, nada te salva. En produccion: pon States.DataLimitExceeded explicito si tu pipeline puede crecer.

Cuidado con Catch + ResultPath: si usas "ResultPath": "$.error_info", el error se guarda en ese campo PERO el input original se preserva. Si no usas ResultPath en el Catch, el error REEMPLAZA todo el state input y pierdes contexto. Siempre usa ResultPath en Catch.

### Choice: bifurcaciones condicionales

A veces no es un error lo que determina el camino — es una condición de negocio. "Si hay más de 10.000 registros, usa el procesamiento pesado. Si hay menos, usa el ligero". Para esto existe Choice.

1{
2 "VerificarVolumen": {
3 "Type": "Choice",
4 "Choices": [
5 {
6 "Variable": "$.ingesta_resultado.registros",
7 "NumericGreaterThan": 10000,
8 "Next": "ProcesamientoPesado"
9 },
10 {
11 "Variable": "$.ingesta_resultado.registros",
12 "NumericEquals": 0,
13 "Next": "SinDatos"
14 }
15 ],
16 "Default": "ProcesamientoLigero"
17 },
18 "ProcesamientoPesado": {
19 "Type": "Task",
20 "Resource": "arn:aws:lambda:eu-west-1:000000000000:function:spark_transform",
21 "Next": "Carga"
22 },
23 "ProcesamientoLigero": {
24 "Type": "Task",
25 "Resource": "arn:aws:lambda:eu-west-1:000000000000:function:pandas_transform",
26 "Next": "Carga"
27 },
28 "SinDatos": {
29 "Type": "Succeed",
30 "Comment": "No hay datos hoy, terminamos OK sin hacer nada"
31 }
32}

Choice como un switch/case: según el volumen de datos, elige la ruta de procesamiento

Choice soporta comparaciones numéricas (Greater, Less, Equal), de string (StringEquals, StringMatches), booleanas, timestamps, y combinaciones con And/Or/Not. Es sorprendentemente potente para lógica declarativa.

### Patrón completo: pipeline robusto con Retry + Catch + Choice

Vamos a juntar todo en un pipeline de producción real. Este es el tipo de máquina de estado que encuentras en empresas serias — no el Hello World de la documentación.

Step Functions · máquina de estados
1{
2 "Comment": "Pipeline ventas con gestión completa de errores",
3 "StartAt": "Ingesta",
4 "States": {
5 "Ingesta": {
6 "Type": "Task",
7 "Resource": "arn:aws:lambda:eu-west-1:000000000000:function:ingesta",
8 "Retry": [
9 {"ErrorEquals": ["Lambda.ServiceException"], "IntervalSeconds": 5, "MaxAttempts": 3, "BackoffRate": 2.0}
10 ],
11 "Catch": [
12 {"ErrorEquals": ["States.ALL"], "ResultPath": "$.error", "Next": "ManejarError"}
13 ],
14 "ResultPath": "$.ingesta",
15 "Next": "VerificarVolumen"
16 },
17 "VerificarVolumen": {
18 "Type": "Choice",
19 "Choices": [
20 {"Variable": "$.ingesta.registros", "NumericEquals": 0, "Next": "SinDatos"},
21 {"Variable": "$.ingesta.registros", "NumericGreaterThan": 50000, "Next": "TransformacionPesada"}
22 ],
23 "Default": "TransformacionNormal"
24 },
25 "TransformacionNormal": {
26 "Type": "Task",
27 "Resource": "arn:aws:lambda:eu-west-1:000000000000:function:transformar",
28 "Retry": [
29 {"ErrorEquals": ["States.ALL"], "IntervalSeconds": 10, "MaxAttempts": 2, "BackoffRate": 1.5}
30 ],
31 "Catch": [
32 {"ErrorEquals": ["States.ALL"], "ResultPath": "$.error", "Next": "ManejarError"}
33 ],
34 "ResultPath": "$.transformacion",
35 "Next": "Carga"
36 },
37 "TransformacionPesada": {
38 "Type": "Task",
39 "Resource": "arn:aws:lambda:eu-west-1:000000000000:function:transformar_spark",
40 "Retry": [
41 {"ErrorEquals": ["States.ALL"], "IntervalSeconds": 30, "MaxAttempts": 2, "BackoffRate": 2.0}
42 ],
43 "Catch": [
44 {"ErrorEquals": ["States.ALL"], "ResultPath": "$.error", "Next": "ManejarError"}
45 ],
46 "ResultPath": "$.transformacion",
47 "Next": "Carga"
48 },
49 "Carga": {
50 "Type": "Task",
51 "Resource": "arn:aws:lambda:eu-west-1:000000000000:function:cargar",
52 "Retry": [
53 {"ErrorEquals": ["States.ALL"], "IntervalSeconds": 5, "MaxAttempts": 3, "BackoffRate": 2.0}
54 ],
55 "Catch": [
56 {"ErrorEquals": ["States.ALL"], "ResultPath": "$.error", "Next": "ManejarError"}
57 ],
58 "ResultPath": "$.carga",
59 "Next": "NotificarExito"
60 },
61 "NotificarExito": {
62 "Type": "Task",
63 "Resource": "arn:aws:lambda:eu-west-1:000000000000:function:notificar",
64 "Parameters": {"mensaje": "Pipeline completado", "estado": "ok"},
65 "End": true
66 },
67 "SinDatos": {
68 "Type": "Succeed",
69 "Comment": "No hay datos nuevos hoy"
70 },
71 "ManejarError": {
72 "Type": "Task",
73 "Resource": "arn:aws:lambda:eu-west-1:000000000000:function:notificar",
74 "Parameters": {
75 "mensaje": "Pipeline FALLIDO",
76 "error.$": "$.error",
77 "estado": "error"
78 },
79 "Next": "PipelineFallido"
80 },
81 "PipelineFallido": {
82 "Type": "Fail",
83 "Cause": "Error no recuperable en el pipeline",
84 "Error": "PipelineError"
85 }
86 }
87}

Un pipeline de producción real: cada paso tiene Retry + Catch, hay bifurcación por volumen, y un flujo de error unificado

Patrón senior: ten un solo estado "ManejarError" centralizado en lugar de un handler por cada paso. Así la lógica de notificación está en un solo sitio. Si mañana cambias de Slack a PagerDuty, solo tocas un estado.

### Pruebalo: provocar un error y ver que pasa

Una leccion de errores que no provoca ninguno es una leccion de teoria. Vamos a provocar uno a proposito para ver como funciona el Catch. El truco mas facil: apuntar un Task a una Lambda que no existe.

1# 1. Crear una maquina con Catch, apuntando a una Lambda que NO existe
2cat > prueba-catch.json << 'EOF'
3{
4 "StartAt": "PasoQueVaAFallar",
5 "States": {
6 "PasoQueVaAFallar": {
7 "Type": "Task",
8 "Resource": "arn:aws:lambda:eu-west-1:000000000000:function:no-existe",
9 "TimeoutSeconds": 30,
10 "Retry": [{"ErrorEquals": ["Lambda.ServiceException"], "MaxAttempts": 1, "IntervalSeconds": 2}],
11 "Catch": [{"ErrorEquals": ["States.ALL"], "ResultPath": "$.error", "Next": "VerError"}],
12 "Next": "Fin"
13 },
14 "VerError": {"Type": "Pass", "End": true},
15 "Fin": {"Type": "Pass", "End": true}
16 }
17}
18EOF
19
20# 2. Crear, ejecutar y mirar el resultado
21SM=$(aws stepfunctions create-state-machine --name PruebaCatch \
22 --definition file://prueba-catch.json \
23 --role-arn "arn:aws:iam::000000000000:role/DummyRole" \
24 --endpoint-url http://localhost:4566 \
25 --query 'stateMachineArn' --output text)
26
27EX=$(aws stepfunctions start-execution --state-machine-arn "$SM" \
28 --input '{"fecha": "2024-01-15", "pipeline": "ventas"}' \
29 --endpoint-url http://localhost:4566 \
30 --query 'executionArn' --output text)
31
32# 3. Ver el resultado (esperamos SUCCEEDED gracias al Catch)
33aws stepfunctions describe-execution --execution-arn "$EX" \
34 --endpoint-url http://localhost:4566 \
35 --query '[status, output]' --output text
36
37# Que deberias ver:
38# SUCCEEDED (si, succeeded: el Catch convirtio un fallo en un final controlado)
39# Output: {"fecha":"2024-01-15","pipeline":"ventas","error":{"Error":"...","Cause":"..."}}
40# Tu input intacto + una clave "error" con Error y Cause.
41# Ahora quitale el ResultPath al Catch y repite: veras que del input no queda nada.

Provocar un error a proposito es la forma de comprobar que tu Catch funciona

## ejercicios

[01]

Añadir reintentos a un pipeline existente

Tienes un pipeline de 3 pasos sin gestión de errores. Añade Retry (3 intentos, backoff x2, intervalo 10s) a cada Task y un Catch global que vaya a un estado "AlertarEquipo".

Cargando editor...
[02]

Bifurcación según tipo de archivo

Crea un estado Choice que examine el campo "$.archivo.formato" y route: si es "csv" va a "ProcesarCSV", si es "json" va a "ProcesarJSON", si es "parquet" va a "ProcesarParquet". Default: "FormatoDesconocido" (Fail).

Cargando editor...
[03]

Diseñar un pipeline completo de producción

El equipo de marketing necesita un pipeline diario que: 1) Descargue datos de campañas (puede fallar por rate limit), 2) Si hay >1000 registros use Spark, si no use Pandas, 3) Cargue en warehouse, 4) Notifique por Slack (éxito o error). Diseña la máquina de estado completa.

Cargando editor...

Regístrate para guardar tu progreso.

## comentarios

Reporta erratas, ayuda a otros o comparte tu opinión. Sé constructivo.

Inicia sesión para comentar y responder.

cargando comentarios...