Sistema de Timers en Zeebe¶
Resumen: Zeebe implementa un sistema de timers demand-driven (no basado en polling) con resolución de 100ms, usando un
DueDateCheckSchedulercompartido con el subsistema de job backoff. El diseño prioriza eficiencia O(1) en scheduling y prevención de starvation mediante unYieldingDecoratorque cede el control después de 50ms de procesamiento continuo.
Arquitectura general¶
El sistema de timers en Zeebe está diseñado para manejar potencialmente millones de timers activos sin degradar el rendimiento del engine. A diferencia de sistemas basados en polling periódico (que desperdician ciclos revisando timers no vencidos), Zeebe usa un modelo demand-driven: el scheduler sabe exactamente cuándo despertar porque mantiene una referencia atómica al próximo due date.
Componentes principales¶
flowchart TD
DTS[DueDateTimerCheckScheduler]
DDS[DueDateCheckScheduler<br/>compartido]
DTS --> DDS
subgraph DDSInternals[Componentes internos]
AR["AtomicReference<Long> nextDueDate"]
SES[ScheduledExecutorService]
CAS[CAS - Compare-And-Swap<br/>para updates]
end
DDS --> DDSInternals
DDSInternals --> TTP[TimerTriggerProcessor]
DDSInternals --> YD[YieldingDecorator]
DueDateTimerCheckScheduler¶
Este es el entry point del sistema de timers. Su responsabilidad principal es decidir cuándo ejecutar el chequeo de timers vencidos, sin recurrir a polling.
Modelo demand-driven¶
En lugar de un loop que cada N milisegundos revisa todos los timers, el scheduler opera así:
- Cuando se crea o modifica un timer, se registra su
dueDateen el scheduler. - El scheduler compara el nuevo
dueDatecon elnextDueDateactual usando CAS (Compare-And-Swap) sobre unAtomicReference<Long>. - Si el nuevo
dueDatees anterior al actual, el scheduler reprograma su despertar al nuevo tiempo. - Cuando el tiempo llega, el scheduler dispara el procesamiento de timers vencidos.
Resolución de 100ms¶
La resolución mínima del sistema es de 100 milisegundos. Esto significa que:
- Timers con due dates separados por menos de 100ms pueden agruparse en el mismo ciclo de procesamiento.
- La granularidad es suficiente para la mayoría de casos de uso de workflow automation (donde los timers típicos son de segundos, minutos u horas).
- Esta resolución es un trade-off deliberado entre precisión y overhead de scheduling.
DueDateCheckScheduler compartido¶
El DueDateCheckScheduler es una abstracción reutilizada por dos subsistemas:
- Timers (via
DueDateTimerCheckScheduler): para timer events en BPMN. - Job backoff (via
JobBackoffCheckScheduler): para reintentos de jobs con delay — ver concepts/retry-backoff.
Diseño O(1)¶
El scheduling opera en tiempo constante O(1) gracias a:
AtomicReference<Long> nextDueDate: almacena únicamente el próximo due date global, no una cola de todos los timers.- CAS (Compare-And-Swap): actualización lock-free del próximo due date. Múltiples threads pueden intentar actualizar concurrentemente sin locks.
ScheduledExecutorService: un solo thread programado para despertar en elnextDueDate.
El algoritmo de actualización:
función scheduleIfDueDateIsSooner(newDueDate):
loop:
currentDueDate = nextDueDate.get()
si newDueDate >= currentDueDate:
return // el actual ya es más pronto
si nextDueDate.compareAndSet(currentDueDate, newDueDate):
cancelar schedule actual
programar nuevo schedule para newDueDate
return
// CAS falló, otro thread actualizó primero → reintentar
Este diseño evita mantener una priority queue en memoria con todos los timers activos, lo cual sería O(log n) en inserción y extracción.
YieldingDecorator¶
Problema: starvation del engine¶
Cuando hay muchos timers vencidos simultáneamente (por ejemplo, después de un recovery o cuando un batch de process instances tiene timers con el mismo due date), procesarlos todos de una vez bloquearía el stream processor de Zeebe, impidiendo que otros comandos (como completar jobs o crear nuevas instancias) se procesen.
Solución: ceder después de 50ms¶
El YieldingDecorator envuelve al procesador de timers con una política de yield cooperativo:
- Al iniciar el procesamiento de timers vencidos, se marca un timestamp de inicio.
- Después de cada timer procesado, se verifica si han pasado más de 50 milisegundos desde el inicio.
- Si se superan los 50ms, el procesador cede el control al stream processor principal, permitiendo que otros comandos se procesen.
- El scheduler se reprograma para continuar procesando los timers restantes en el siguiente ciclo.
Implicaciones¶
- Fairness: otros comandos no quedan bloqueados incluso si hay miles de timers vencidos.
- Throughput de timers reducido temporalmente: en escenarios de alta carga de timers, el procesamiento se distribuye en múltiples ciclos.
- Latencia predecible: el worst-case de bloqueo por timers es de ~50ms por ciclo.
TimerTriggerProcessor¶
Este processor es el encargado de activar el elemento BPMN target cuando un timer vence, y de manejar la lógica de repeating timers.
Activación del elemento target¶
Cuando un timer vence:
- El
TimerTriggerProcessoridentifica el elemento BPMN asociado al timer (boundary event, intermediate catch event, start event, etc.). - Genera un comando para activar ese elemento, insertándolo en el stream de comandos de Zeebe.
- El elemento se procesa como cualquier otro evento BPMN.
Reschedule de repeating timers¶
Para timers con repetición (cycle timers, por ejemplo R3/PT1H — repetir 3 veces cada hora):
- Después de disparar el timer, el processor calcula el próximo due date.
- Crucialmente, el próximo due date se calcula anclado a la fecha anterior, no al momento actual. Esto significa que si un timer debía dispararse a las 10:00 y se procesó a las 10:00.050 (50ms de delay), el siguiente se programa para las 11:00, no para las 11:00.050.
- Esta estrategia previene timer drift: la acumulación gradual de pequeños delays que eventualmente desincronizarían el timer de su schedule original.
- Si el próximo due date calculado ya está en el pasado (por ejemplo, después de un recovery largo), se dispara inmediatamente y se recalcula el siguiente.
Tipos de timer events soportados¶
| Tipo BPMN | Comportamiento del trigger |
|---|---|
| Timer Start Event | Crea nueva process instance |
| Timer Intermediate Catch Event | Continúa el flujo del proceso |
| Timer Boundary Event (interrupting) | Cancela la actividad y toma el path alternativo |
| Timer Boundary Event (non-interrupting) | Dispara el path alternativo sin cancelar la actividad |
| Timer Event Sub-Process | Inicia el sub-proceso |
Interacción con el stream processor¶
El sistema de timers se integra con el stream processor de Zeebe como un source de comandos interno:
- Los timer checks generan comandos
TIMER:TRIGGERque se insertan en el log de comandos. - Estos comandos pasan por el mismo pipeline de procesamiento que cualquier comando externo (gRPC).
- El state del timer se mantiene en RocksDB como parte del state store de la partición.
- En un cluster, cada partición maneja sus propios timers de forma independiente — no hay coordinación cross-partition para timers.
Consideraciones para un MVP¶
Para una implementación simplificada de workflow engine:
- Esencial: el modelo demand-driven con
AtomicReference+ CAS es elegante y eficiente; vale la pena replicarlo. - Esencial: el yield después de 50ms previene problemas reales de starvation; cualquier implementación de timers en un single-threaded processor lo necesita.
- Simplificable: la resolución de 100ms es un buen default pero podría ser configurable.
- Simplificable: el anclaje a fecha anterior para repeating timers es correcto pero un MVP podría empezar con anclaje al momento actual y refinar después.