Pular para o conteúdo principal

Processamento Paralelo

Por padrão, um processo anda em linha: cada passo espera o anterior terminar. Quando dois ou mais passos não dependem um do outro, um PARALLEL_GATEWAY roda os dois ao mesmo tempo.

Pense em fazer o jantar

Você não frita o bife, depois espera esfriar, depois começa a salada, depois espera, depois põe água para o arroz. Você põe a água no fogo, e enquanto ela esquenta pica a salada e tempera a carne. Três tarefas independentes correndo ao mesmo tempo, e o jantar só está pronto quando a última termina. A analogia para de funcionar no ponto em que uma tarefa depende da outra — não adianta paralelizar "escorrer o macarrão" com "cozinhar o macarrão".

"Consulta ao bureau e coleta de documentos são independentes — posso rodar as duas ao mesmo tempo?"​

Num processo de abertura de conta, antes de calcular o score, duas coisas precisam acontecer: uma consulta ao bureau de crédito (uma chamada de API, automática) e a coleta dos documentos do solicitante (uma tarefa externa, o solicitante anexa RG e comprovante). Uma não depende da outra. Modeladas em sequência, o tempo de uma soma no tempo da outra — e a consulta ao bureau fica parada esperando o solicitante achar o comprovante de residência.

Um PARALLEL_GATEWAY abre as duas como ramos independentes; um JOIN_GATEWAY espera as duas terminarem antes de seguir para Calcular Score.

Declarar no .kikwi​

No Craft, arraste um Parallel (split) antes dos ramos e um Join depois deles; o Craft associa o Join ao split automaticamente. O .kikwi resultante tem quatro nós novos. O PARALLEL_GATEWAY declara para qual join seus ramos convergem (targetJoinId); o JOIN_GATEWAY declara de qual split ele fecha (sourceSplitId):

{
"PARALLEL_SPLIT": {
"id": "PARALLEL_SPLIT",
"name": "Divisão de Esteiras",
"type": "PARALLEL_GATEWAY",
"targetJoinId": "JOIN_VERIFICACOES",
"outgoing": [
{ "id": "flow-split-bureau", "targetNodeId": "CONSULTA_BUREAU" },
{ "id": "flow-split-docs", "targetNodeId": "COLETAR_DOCUMENTOS" }
]
},
"CONSULTA_BUREAU": {
"id": "CONSULTA_BUREAU",
"name": "Consulta Bureau",
"type": "EXECUTABLE_TASK",
"executor": "consultaBureauTaskHandler",
"commitBefore": true,
"outgoing": [ { "id": "flow-bureau-join", "targetNodeId": "JOIN_VERIFICACOES" } ]
},
"COLETAR_DOCUMENTOS": {
"id": "COLETAR_DOCUMENTOS",
"name": "Coletar Documentos",
"type": "EXTERNAL_TASK",
"extensionProperties": { "actor": "solicitante" },
"outgoing": [ { "id": "flow-docs-join", "targetNodeId": "JOIN_VERIFICACOES" } ]
},
"JOIN_VERIFICACOES": {
"id": "JOIN_VERIFICACOES",
"name": "Sincronização",
"type": "JOIN_GATEWAY",
"sourceSplitId": "PARALLEL_SPLIT",
"outgoing": [ { "id": "flow-join-score", "targetNodeId": "CALCULAR_SCORE" } ]
}
}

O JOIN_GATEWAY não decide nada — ele apenas segue sua única saída assim que todos os ramos abertos por aquele split tiverem chegado nele. Um split com dois ramos exige que os dois cheguem; se um ramo terminar em um DEFAULT_END_EVENT no meio do caminho (em vez de chegar no join), o join espera para sempre — modele os ramos para sempre convergirem.

Implementar​

O ramo de tarefa externa (Coletar Documentos) não tem bean — é completado de fora, como qualquer tarefa externa. O ramo automático precisa do handler:

package com.empresa.processo.executors;

import io.kikwiflow.execution.api.context.ExecutionContext;
import io.kikwiflow.execution.api.handler.TaskHandler;
import io.kikwiflow.model.execution.ProcessVariable;
import org.springframework.stereotype.Component;

@Component("consultaBureauTaskHandler")
public class ConsultaBureauTaskHandler implements TaskHandler {

private final BureauClient bureauClient;

public ConsultaBureauTaskHandler(BureauClient bureauClient) {
this.bureauClient = bureauClient;
}

@Override
public void handle(ExecutionContext execution) {
String cpf = execution.getVariable("cpf").value().toString();
int scoreBureau = bureauClient.consultarScore(cpf);
execution.setVariable("scoreBureau", new ProcessVariable("scoreBureau", scoreBureau));
}
}

commitBefore nos ramos: por que ele aparece aqui​

Um PARALLEL_GATEWAY sempre cruza uma fronteira assíncrona internamente — cada ramo é retomado por um worker de segundo plano, não pela thread que iniciou o processo. Isso é o motor sendo correto: os ramos precisam poder rodar de fato em paralelo, e um deles (a tarefa externa) vai ficar parado esperando um humano por tempo indeterminado.

Na prática, para você que modela: declarar commitBefore: true no nó executável de um ramo (CONSULTA_BUREAU, acima) garante que o estado é persistido antes dele rodar, então se a aplicação reiniciar no meio da consulta ao bureau, o ramo é retomado do começo em vez de se perder. O que "assíncrono" significa para você ao modelar está na seção commitBefore/commitAfter de Anatomia de um Processo; o mecanismo interno (filas de trabalho, UnitOfWork, o poller) está em Execução Síncrona e Assíncrona.

Quando usar / quando não usar​

Use quando dois ou mais passos são genuinamente independentes e podem (ou devem) rodar ao mesmo tempo — duas verificações que não leem o resultado uma da outra, uma chamada de API lenta ao lado de uma espera humana. O ganho é de tempo de ciclo: o processo anda no tempo do ramo mais lento, não na soma.

Não use quando:

  • Um passo depende do resultado do outro — aí é sequência, não paralelo.
  • Você quer escolher um de vários caminhos com base numa condição — isso é um EXCLUSIVE_GATEWAY, não um paralelo.
  • Você precisa disparar N cópias do mesmo subprocesso (uma por item de uma lista) — isso é CALL_ACTIVITY_COORDINATOR, não um split com ramos fixos.

Próximo passo​

Para abrir N ramos a partir de uma lista (um subprocesso por produto, por documento), em vez de ramos fixos no diagrama, veja Subprocessos (Call Activity).