Saltar a contenido

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 activoTERMINATING: 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_TAKEN representa 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:

  1. BpmnStateTransitionBehavior.takeOutgoingSequenceFlows() itera sobre todos los outgoing sequence flows.
  2. Para cada flow, escribe un record ProcessInstanceIntent.SEQUENCE_FLOW_TAKEN.
  3. Cada record SEQUENCE_FLOW_TAKEN es procesado por SequenceFlowProcessor, que activa el target del flow.
  4. 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:

  1. Cada sequence flow que llega a un parallel gateway como target es procesado por SequenceFlowProcessor.
  2. El processor registra el flow en el event scope del gateway, incrementando un contador de flows recibidos.
  3. Se compara el contador con el número total de incoming sequence flows definidos en el modelo BPMN.
  4. Si flows_recibidos < flows_esperados: no hace nada más. El gateway permanece esperando.
  5. 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

  1. Extraer intent: del record se obtiene el ProcessInstanceIntent (ACTIVATE_ELEMENT, COMPLETE_ELEMENT, TERMINATE_ELEMENT, SEQUENCE_FLOW_TAKEN, etc.).
  2. Buscar element instance: usando el elementInstanceKey del record, se obtiene el estado actual del element instance del state store.
  3. Validar transición: ProcessInstanceStateTransitionGuard verifica que la transición de estado implícita en el intent es válida desde el estado actual.
  4. Buscar procesador: del BpmnElementType del element instance, se obtiene el BpmnElementProcessor correspondiente del registry (EnumMap).
  5. 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.