Retry y Backoff en Jobs¶
Resumen: El sistema de reintentos de Zeebe se implementa en
JobFailProcessorcon tres escenarios: reintento inmediato (sin backoff), reintento con delay (usando elDueDateCheckSchedulercompartido con timers), e incident creation (sin reintentos restantes). El backoff duration es especificado por el cliente worker — Zeebe no implementa exponential backoff built-in, dejando esa decisión al consumidor.
JobFailProcessor: tres escenarios de fallo¶
Cuando un job worker reporta un fallo (vía el comando FailJob en gRPC), el JobFailProcessor evalúa el estado del job y toma una de tres acciones:
Escenario 1: retries > 0, sin backoff¶
Condición: job.retries > 0 AND backoffDuration == 0
Acción: el job se marca como ACTIVATABLE inmediatamente
- El job vuelve al pool de jobs disponibles sin delay.
- El próximo worker que haga
ActivateJobspuede tomarlo inmediatamente. - Este es el comportamiento por defecto cuando el cliente no especifica un backoff duration.
- Riesgo: si el fallo es por una condición transitoria que no se resuelve instantáneamente (por ejemplo, un servicio externo caído), el job puede fallar repetidamente en rápida sucesión, consumiendo todos sus reintentos en segundos.
Escenario 2: retries > 0, con backoff¶
Condición: job.retries > 0 AND backoffDuration > 0
Acción: el job se marca como FAILED_BACK_OFF, se programa un retry via DueDateCheckScheduler
- El
backoffDurationes un valor en milisegundos proporcionado por el cliente en el comandoFailJob. - Se calcula un
retryBackoffTimestamp = now() + backoffDuration. - Este timestamp se registra en el
JobBackoffCheckScheduler, que usa el mismoDueDateCheckSchedulerdel sistema de timers — ver concepts/timer-system. - Cuando el timestamp se alcanza, el job se mueve a estado ACTIVATABLE.
- Durante el período de backoff, el job no es visible para los workers; no pueden activarlo.
Escenario 3: sin retries restantes (incident)¶
- El job queda en estado FAILED permanentemente.
- Se genera un incident de tipo
JOB_NO_RETRIES. - La resolución requiere intervención manual o automática via la API de incidents.
- Al resolver el incident, se puede actualizar el número de retries y el job vuelve a ser activable.
Backoff: decisión del cliente, no del engine¶
Una decisión de diseño importante en Zeebe es que el engine no implementa exponential backoff. El backoffDuration es un valor fijo proporcionado por el cliente en cada llamada a FailJob.
Por qué no hay exponential backoff built-in¶
- Flexibilidad: diferentes tipos de jobs pueden necesitar diferentes estrategias de backoff. Un job que llama a una API externa podría querer exponential backoff con jitter, mientras que un job de cálculo podría querer un delay fijo.
- Conocimiento del dominio: el worker conoce mejor que el engine la naturaleza del fallo y la estrategia de recovery apropiada.
- Simplicidad del engine: mantener el estado de la progresión del backoff (intento 1 → 1s, intento 2 → 2s, intento 3 → 4s, etc.) requeriría state adicional en el engine.
Implementación típica en el cliente¶
Los SDKs de Zeebe y los clientes suelen implementar su propio exponential backoff:
// Pseudocódigo en el worker
function handleFailure(job, error):
attempt = job.maxRetries - job.remainingRetries + 1
backoff = min(baseDelay * 2^attempt, maxDelay) + random(jitter)
client.failJob(job.key, retries=job.remainingRetries - 1, backoff=backoff)
JobBackoffCheckScheduler¶
El JobBackoffCheckScheduler es el componente que monitorea los jobs en estado de backoff y los reactiva cuando su delay expira.
Reutilización del DueDateCheckScheduler¶
El JobBackoffCheckScheduler no implementa su propio mecanismo de scheduling. En su lugar, reutiliza el mismo DueDateCheckScheduler que usa el sistema de timers:
flowchart LR
DDS[DueDateCheckScheduler<br/>compartido]
DDS --> DTS[DueDateTimerCheckScheduler]
DDS --> JBS[JobBackoffCheckScheduler]
DTS --> TE[timer events]
JBS --> JR[job retries con delay]
Esta reutilización tiene varias ventajas:
- Código compartido: la lógica de CAS,
AtomicReference, y scheduling eficiente se escribe una vez. - Misma resolución: los backoffs tienen la misma resolución de 100ms que los timers.
- Un solo
ScheduledExecutorService: se comparte el thread pool de scheduling.
Flujo de un job con backoff¶
1. Worker llama FailJob(key, retries=2, backoff=5000ms)
2. JobFailProcessor:
a. Calcula retryBackoffTimestamp = now() + 5000
b. Guarda el job con estado FAILED_BACK_OFF y el timestamp
c. Registra el timestamp en JobBackoffCheckScheduler
3. JobBackoffCheckScheduler:
a. Llama a DueDateCheckScheduler.scheduleIfDueDateIsSooner(timestamp)
b. DueDateCheckScheduler compara con su nextDueDate via CAS
c. Si es más pronto, reprograma el ScheduledExecutorService
4. Después de 5 segundos:
a. DueDateCheckScheduler despierta
b. JobBackoffCheckScheduler procesa jobs con timestamp <= now()
c. Jobs se mueven a estado ACTIVATABLE
d. Los workers pueden activarlos normalmente
Estado del job en RocksDB¶
Los jobs con backoff se persisten en RocksDB con su estado y timestamp de retry:
| Campo | Descripción |
|---|---|
state |
FAILED_BACK_OFF durante el período de espera |
retries |
Número de reintentos restantes (ya decrementado) |
retryBackoffTimestamp |
Timestamp absoluto (epoch millis) en que el job se reactiva |
errorMessage |
Mensaje de error del último fallo |
errorCode |
Código de error opcional para error handling en BPMN |
Interacción con error handling de BPMN¶
El sistema de retries interactúa con el error handling de BPMN de la siguiente manera:
ThrowErrorvsFail: si un worker lanza unThrowError, esto activa un error boundary event en BPMN (si existe) y no usa el sistema de retries.Failes para fallos transitorios;ThrowErrores para errores de negocio.- Error codes: cuando un job falla con
retries=0, el incident creado incluye elerrorCodesi fue proporcionado, lo cual puede ser usado por tooling externo para categorizar fallos. - Manual retry via incident: al resolver un incident de tipo
JOB_NO_RETRIES, el operador puede establecer un nuevo número de retries, efectivamente "resucitando" el job.
Consideraciones para un MVP¶
- Esencial: los tres escenarios de
JobFailProcessorcubren todos los casos de uso reales y son relativamente simples de implementar. - Esencial: reutilizar el scheduler de timers para backoff es una decisión de diseño elegante que reduce complejidad.
- Recomendado: mantener el backoff duration como decisión del cliente (no built-in exponential) simplifica el engine y da más control.
- Simplificable: para un MVP, el escenario 1 (retry inmediato) y 3 (incident) pueden implementarse primero, añadiendo backoff scheduling después.