Bpmn Execution Model
Zeebe ejecuta procesos BPMN mediante una máquina de estados por element instance, sin objetos token explícitos. Los "tokens" son element instances activados y sequence flows tomados. El procesamiento central lo realiza BpmnStreamProcessor, que extrae el intent de cada record, busca el procesador correspondiente, valida la transición de estado, y delega la ejecución. El modelo BPMN pasa por un pipeline de 5 transformaciones desde XML hasta ExecutableProcess.
Máquina de estados del ciclo de vida¶
Cada element instance (cada activación de un nodo BPMN) sigue esta máquina de estados:
stateDiagram-v2
[*] --> ACTIVATING
ACTIVATING --> ACTIVATED
ACTIVATED --> COMPLETING
COMPLETING --> COMPLETED
COMPLETED --> SEQUENCE_FLOW_TAKEN: sequence flow taken
SEQUENCE_FLOW_TAKEN --> ACTIVATING: del target
ACTIVATING --> TERMINATING
ACTIVATED --> TERMINATING
TERMINATING --> TERMINATED
TERMINATED --> [*]
Transiciones válidas:
- ACTIVATING → ACTIVATED: el procesador completó la inicialización del elemento.
- ACTIVATED → COMPLETING: el elemento terminó su trabajo (job completado, timer disparado, mensaje recibido).
- COMPLETING → COMPLETED: output mappings aplicados, cleanup finalizado.
- COMPLETED → (fin): sequence flows salientes se toman, activando los targets.
- Desde cualquier estado activo → TERMINATING: cancelación, error boundary, terminate end event, interrupción del padre.
- TERMINATING → TERMINATED: cleanup completado (jobs cancelados, suscripciones eliminadas, hijos terminados).
Estados especiales:
- SEQUENCE_FLOW_TAKEN: pseudo-estado transitorio. Cuando un sequence flow se toma, se escribe un record SEQUENCE_FLOW_TAKEN que causa la activación del elemento target. No es un estado persistente del element instance.
ProcessInstanceStateTransitionGuard¶
Clase que valida cada transición de estado antes de ejecutarla. Mantiene un mapa de transiciones permitidas y rechaza cualquier transición inválida (por ejemplo, COMPLETED → ACTIVATING directamente). Si una transición se rechaza, se escribe un record de rechazo y el procesamiento se detiene.
La guard también verifica el estado del process instance padre: si el proceso está en TERMINATING, no permite nuevas activaciones de elementos hijos.
Modelo de tokens implícito¶
Zeebe no tiene objetos token explícitos. En su lugar:
- Token = element instance activado: cada vez que un elemento entra en estado ACTIVATING/ACTIVATED, eso equivale a un token en el nodo BPMN correspondiente.
- Token en tránsito = sequence flow tomado: un record
SEQUENCE_FLOW_TAKENrepresenta un token moviéndose entre nodos. - Fork (divergencia): se crean múltiples sequence flows tomados, uno por cada outgoing flow. Cada uno activará su target independientemente.
- Join (convergencia): los incoming sequence flows se registran y cuentan. El gateway espera a que todos (parallel) o al menos uno (exclusive) hayan llegado.
Esta representación es más eficiente que tokens explícitos porque no necesita sincronizar objetos token — solo necesita contar flows incoming y gestionar element instances individuales.
Parallel fork¶
Cuando un parallel gateway (u otro elemento con múltiples outgoing flows) se completa:
BpmnStateTransitionBehavior.takeOutgoingSequenceFlows()itera sobre todos los outgoing sequence flows.- Para cada flow, escribe un record
ProcessInstanceIntent.SEQUENCE_FLOW_TAKEN. - Cada record SEQUENCE_FLOW_TAKEN es procesado por
SequenceFlowProcessor, que activa el target del flow. - Las activaciones son independientes — cada target se procesa en su propia iteración del stream processor, sin coordinación entre ellas.
Resultado: N outgoing flows producen N element instances activados simultáneamente, ejecutándose en paralelo conceptual (en realidad secuencial dentro de la partición, pero sin esperar uno al otro).
Parallel join¶
El mecanismo de join para parallel gateways:
- Cada sequence flow que llega a un parallel gateway como target es procesado por
SequenceFlowProcessor. - El processor registra el flow en el event scope del gateway, incrementando un contador de flows recibidos.
- Se compara el contador con el número total de incoming sequence flows definidos en el modelo BPMN.
- Si
flows_recibidos < flows_esperados: no hace nada más. El gateway permanece esperando. - Si
flows_recibidos == flows_esperados: activa el gateway, que procede a tomar sus outgoing flows.
Detalle clave: el join NO se implementa en ParallelGatewayProcessor sino en SequenceFlowProcessor. El ParallelGatewayProcessor solo se ocupa del fork. Esta separación es necesaria porque el conteo de incoming flows debe ocurrir antes de que el gateway se active.
Variable scoping¶
Las variables siguen un modelo de scoping jerárquico por element instance:
Estructura de scopes¶
- Cada process instance tiene un scope raíz.
- Cada sub-proceso, multi-instance body, y call activity crean un scope hijo.
- Las variables definidas en un scope hijo shadow (ocultan) variables del mismo nombre en scopes ancestros.
- La lectura de variables busca primero en el scope local, luego sube por la jerarquía hasta encontrar la variable o llegar al scope raíz.
Propagación de variables¶
- Input mappings: copian variables del scope padre al scope local del elemento (con posible transformación FEEL).
- Output mappings: copian variables del scope local al scope padre al completar el elemento.
- Sin output mapping explícito: cuando un job se completa con variables y el elemento no tiene output mappings definidos, las variables se propagan upward al primer scope existente que ya contiene una variable con ese nombre. Si la variable no existe en ningún scope, se crea en el scope del flow scope (el proceso o sub-proceso más cercano).
Almacenamiento¶
Las variables se almacenan como pares key-value en el state store (RocksDB), indexadas por (scopeKey, variableName). Los valores se serializan en formato MessagePack para eficiencia.
BpmnStreamProcessor: dispatcher central¶
BpmnStreamProcessor es el stream processor que maneja todos los records de tipo ProcessInstanceIntent. Es el punto de entrada central para toda la ejecución BPMN.
Flujo de procesamiento de un record¶
- Extraer intent: del record se obtiene el
ProcessInstanceIntent(ACTIVATE_ELEMENT, COMPLETE_ELEMENT, TERMINATE_ELEMENT, SEQUENCE_FLOW_TAKEN, etc.). - Buscar element instance: usando el
elementInstanceKeydel record, se obtiene el estado actual del element instance del state store. - Validar transición:
ProcessInstanceStateTransitionGuardverifica que la transición de estado implícita en el intent es válida desde el estado actual. - Buscar procesador: del
BpmnElementTypedel element instance, se obtiene elBpmnElementProcessorcorrespondiente del registry (EnumMap). - Delegar: se invoca el método apropiado del procesador (
onActivate,onComplete,onTerminate, etc.) según el intent.
Intents procesados¶
| Intent | Acción |
|---|---|
ACTIVATE_ELEMENT |
Invoca processor.onActivate() |
ELEMENT_ACTIVATING |
Transiciona a ACTIVATING, invoca processor.finalizeActivation() |
ELEMENT_ACTIVATED |
Registra estado ACTIVATED |
COMPLETE_ELEMENT |
Invoca processor.onComplete() |
ELEMENT_COMPLETING |
Transiciona a COMPLETING, invoca processor.finalizeCompletion() |
ELEMENT_COMPLETED |
Registra estado COMPLETED, toma outgoing flows |
TERMINATE_ELEMENT |
Invoca processor.onTerminate() |
ELEMENT_TERMINATING |
Transiciona a TERMINATING, invoca processor.finalizeTermination() |
ELEMENT_TERMINATED |
Registra estado TERMINATED, notifica contenedor padre |
SEQUENCE_FLOW_TAKEN |
SequenceFlowProcessor evalúa el flow y activa el target |
Pipeline de transformación BPMN (5 pasos)¶
El modelo BPMN pasa por 5 transformaciones desde XML hasta una representación ejecutable:
Paso 1: XML parsing (JAXB)¶
El XML BPMN 2.0 se parsea usando JAXB a objetos del modelo io.camunda.zeebe.model.bpmn. Produce un árbol de objetos Java que representa fielmente la estructura XML.
Paso 2: Validación del modelo¶
Se ejecutan validadores que verifican: - Consistencia estructural (todo start event tiene un proceso padre, todo sequence flow tiene source y target). - Restricciones semánticas (exclusive gateway tiene max un default flow, parallel gateway no tiene condiciones). - Expresiones FEEL válidas sintácticamente. - Elementos soportados (Zeebe no soporta todos los elementos BPMN 2.0).
Paso 3: Transformación a modelo ejecutable¶
Los objetos JAXB se transforman a ExecutableProcess y sus sub-elementos (ExecutableServiceTask, ExecutableExclusiveGateway, etc.). Cada Executable* contiene la información pre-procesada necesaria para ejecución:
- Expresiones FEEL pre-compiladas.
- Input/output mappings resueltos.
- Task definitions con type y retries evaluables.
- Event definitions con correlation keys.
Paso 4: Linking de referencias¶
Se resuelven referencias internas: - Sequence flow sources y targets se linkean a sus elementos ejecutables. - Message catch events se asocian con sus message definitions. - Call activities se asocian con el processId referenciado (resuelto en runtime). - Boundary events se asocian con su actividad host.
Paso 5: Registro en el DeploymentState¶
El ExecutableProcess resultante se serializa y almacena en el state store bajo la clave del deployment. Queda disponible para instanciación:
- Se indexa por (bpmnProcessId, version).
- El latest version pointer se actualiza.
- Los message start event subscriptions se crean automáticamente.
- Los timer start events programan sus timers.
- Los signal start events registran sus suscripciones.
El resultado de este pipeline es un ExecutableProcess listo para instanciar: todas las expresiones pre-compiladas, todas las referencias resueltas, toda la metadata de ejecución pre-calculada. El engine nunca re-parsea XML en runtime.