Pular para o conteúdo principal
Página não listada
Esta página não está listada. Mecanismos de busca não irão indexá-la, e somente usuários que possuam o link direto poderão acessá-la

Execução Síncrona e Assíncrona

Este é o mecanismo mais importante para entender o comportamento em produção do Kikwiflow: quando o motor processa vários nós em memória, na mesma chamada, sem tocar o banco entre eles, e quando ele para, persiste e devolve o controle? A resposta vive inteiramente em io.kikwiflow.execution.ProcessExecutionManager.

:::info Isto não é asyncBefore do BPMN Se você já trabalhou com Camunda/Activiti, é tentador mapear mentalmente este mecanismo para o atributo camunda:asyncBefore do BPMN XML. A analogia é útil como ponto de partida, mas a implementação é propositalmente diferente e mais simples: não existe um barramento de mensagens, não existe um job executor genérico consumindo uma fila — existe uma fila em memória (ExecutionFrame) processada dentro de um único método, e a fronteira de parada é decidida por dois booleanos simples no próprio nó: commitBefore e commitAfter. :::

A "Agenda": fila de ExecutionFrame

public record ExecutionFrame(FlowNodeDefinition node, String branchId, String joinTaskId) {}

ProcessExecutionManager.executeFlow roda um while (!agenda.isEmpty()) sobre uma Queue<ExecutionFrame>. Cada iteração processa um nó, decide sua continuação via Navigator, e ou:

  1. Empilha o(s) próximo(s) nó(s) na mesma fila (execução síncrona, tudo na mesma chamada de método, mesma transação lógica em memória), ou
  2. Retorna imediatamente um ExecutionResult com uma Continuation marcada isAsynchronous = true, devolvendo o controle para o chamador (KikwiflowEngine), que por sua vez delega a persistência ao ContinuationService.

Esse desenho — uma fila local em vez de um loop recursivo linear — existe especificamente para suportar ramificação paralela: quando um PARALLEL_GATEWAY produz múltiplos próximos nós, cada um vira um ExecutionFrame independente carregando seu próprio branchId, sem que os ramos precisem compartilhar pilha de chamada ou se preocupar com condições de corrida em memória — a fila é sempre processada por uma única thread, por uma única instância de processo, por vez.

commitBefore e commitAfter: as duas fronteiras

Todo FlowNodeDefinition carrega Boolean commitBefore() e Boolean commitAfter(). Seu efeito:

  • commitBefore: true — o motor para antes de executar aquele nó e persiste o estado atual. Use isso para pontos onde você quer garantir durabilidade antes de uma operação potencialmente cara/lenta/instável (ex.: uma tarefa que chama uma API externa). A tarefa fica agendada (ExecutableTask com status: PENDING e dueDate: now) para ser pega pelo TaskAcquirer em um ciclo de polling separado, possivelmente em outra instância/nó do cluster.
  • commitAfter: true — o motor executa o nó normalmente, mas força a continuação a ser assíncrona, mesmo que o próximo nó não tenha commitBefore. Use isso quando o próprio nó atual é a operação sensível e você quer garantir que o efeito dele já esteja durável antes do motor seguir adiante.
final boolean shouldStopForCommitBefore = isCommitBefore(currentNode)
&& !(isFirstNodeInLoop && isResumingFromAsyncBefore);

if (isWaitState(currentNode) || shouldStopForCommitBefore) {
return new ExecutionResult(
new ExecutionOutcome(processInstance, criticalEvents),
new Continuation(List.of(currentNode), true));
}

Note a condição !(isFirstNodeInLoop && isResumingFromAsyncBefore): quando o TaskAcquirer retoma a execução de um nó que parou por commitBefore anteriormente, o motor não deve parar de novo no mesmo nó — ele executa a lógica de fato dessa vez. Esse flag distingue "estou chegando neste nó pela primeira vez" de "estou retomando este nó que já foi persistido como pendente".

Todo nó que implementa a marca WaitState (hoje, apenas ExternalTaskDefinition) sempre interrompe a execução, independentemente de commitBefore — não faz sentido continuar em memória além de um nó que por definição aguarda um evento externo.

Sem commitBefore/commitAfter: tudo em uma única passada

Se um processo inteiro é composto apenas por EXECUTABLE_TASKs e gateways sem nenhum commitBefore/ commitAfter: true, o motor processa todo o grafo, do início ao fim, em uma única chamada síncronaProcessStarter.execute() retorna já com o ProcessInstance no status final (COMPLETED), sem nenhuma volta ao TaskAcquirer. Isso é intencional e é o caminho de menor latência possível: para processos puramente computacionais/rápidos, forçar uma ida ao banco por nó seria desperdício.

know-your-customer-sync.json (padrão recomendado para processos rápidos)
{
"type": "EXECUTABLE_TASK",
"executor": "calculateCustomerRiskTaskHandler",
"commitBefore": false,
"commitAfter": false
}

:::tip Quando marcar commitBefore: true Regra prática: marque commitBefore: true em qualquer EXECUTABLE_TASK que faça I/O externo (chamada HTTP, mensageria, banco de terceiros) ou que deva sobreviver a um restart do processo Java entre o passo anterior e este. Deixe false (o padrão idiomático) para transformações de dados puramente locais que você quer que aconteçam atomicamente com o resto do fluxo síncrono. :::

Fan-out e fan-in: PARALLEL_GATEWAY e JOIN_GATEWAY

Quando a continuação de um PARALLEL_GATEWAY é processada pelo ContinuationService, cada ramo ganha um branchId (UUID) próprio, e um ExecutableTask especial do tipo ExecutableTaskType.JOIN_GATEWAY é criado antecipadamente, já sabendo quais branchIds precisa aguardar:

ExecutableTask joinTask = ExecutableTask.builder()
.taskDefinitionId(continuation.targetJoinNode().id())
.type(ExecutableTaskType.JOIN_GATEWAY)
.status(ExecutableTaskStatus.AWAITING_BRANCHES)
.pendingBranchIds(branchIds)
.build();

Cada ramo então executa independentemente (potencialmente em threads/momentos diferentes, se cruzar uma fronteira commitBefore). Quando um ramo termina — seja atingindo um DEFAULT_END_EVENT de ramo ou o próprio JOIN_GATEWAY de destino — a execução chama processInstance.registerBranchConclusion(joinTaskId, branchId), que acumula uma BranchPullIntention em memória. Essa intenção é enviada ao UnitOfWork e, na camada de persistência, remove aquele branchId da lista pendingBranchIds do JOIN_GATEWAY de forma atômica — quando a lista fica vazia, o repositório libera o JOIN_GATEWAY para execução (fora do escopo deste documento; ver Persistência e Consistência Transacional).

if (("DEFAULT_END_EVENT".equals(currentNode.type()) || "JOIN_GATEWAY".equals(currentNode.type()))
&& !isLegitimateJoinResumption && currentBranchId != null) {
if (currentJoinTaskId != null) {
processInstance.registerBranchConclusion(currentJoinTaskId, currentBranchId);
}
branchConcluded = true;
continue;
}

Esse design evita a necessidade de locks pessimistas ou contadores centralizados em memória compartilhada — a contagem de ramos pendentes vive no próprio documento persistido do JOIN_GATEWAY, e cada ramo, ao concluir, faz uma operação de remoção idempotente sobre essa lista.

Timers não-interruptivos se reagendam sozinhos

Um caso especial de continuação assíncrona: quando um ExecutableTask do tipo NON_INTERRUPTIVE_TIMER termina, o ContinuationService não apenas processa a continuação normal — ele também calcula a próxima ocorrência via TimerDueDateEvaluator.calculateNextSchedule e injeta um novo ExecutableTask já agendado para o próximo ciclo, com um branchId novo (UUID.randomUUID()), efetivamente criando um laço de recorrência que independe de qualquer scheduler externo (cron do SO, Quartz, etc.) — o próprio motor é o scheduler. Ver Timers e Agendamento.

Resumo do fluxo ponta a ponta