Saltar a contenido

Retry y Backoff en Jobs

Resumen: El sistema de reintentos de Zeebe se implementa en JobFailProcessor con tres escenarios: reintento inmediato (sin backoff), reintento con delay (usando el DueDateCheckScheduler compartido 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 ActivateJobs puede 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 backoffDuration es un valor en milisegundos proporcionado por el cliente en el comando FailJob.
  • Se calcula un retryBackoffTimestamp = now() + backoffDuration.
  • Este timestamp se registra en el JobBackoffCheckScheduler, que usa el mismo DueDateCheckScheduler del 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)

Condición: job.retries == 0
Acción: se crea un Incident de tipo JOB_NO_RETRIES
  • 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

  1. 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.
  2. Conocimiento del dominio: el worker conoce mejor que el engine la naturaleza del fallo y la estrategia de recovery apropiada.
  3. 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:

  • ThrowError vs Fail: si un worker lanza un ThrowError, esto activa un error boundary event en BPMN (si existe) y no usa el sistema de retries. Fail es para fallos transitorios; ThrowError es para errores de negocio.
  • Error codes: cuando un job falla con retries=0, el incident creado incluye el errorCode si 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 JobFailProcessor cubren 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.