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

Navegação e Gateways de Decisão

A classe io.kikwiflow.navigation.Navigator é o componente responsável por responder uma única pergunta depois que um nó termina de executar: qual é o próximo nó (ou nós)? Toda a lógica de roteamento do motor — gateways exclusivos, fan-out/fan-in paralelo, casamento de error handlers — está concentrada nesta classe, através do método determineNextContinuation.

AnswerProvider: decisões como código Java testável

Um gateway exclusivo (EXCLUSIVE_GATEWAY) precisa resolver uma resposta (uma String) para escolher qual aresta de saída seguir. O Kikwiflow oferece três estratégias, configuradas pelo campo providerType do gateway:

public enum AnswerProviderType {
BEAN,
VARIABLE
}

providerType: BEAN

A resposta vem da execução de um bean Spring que implementa AnswerProvider:

@FunctionalInterface
public interface AnswerProvider {
String resolve(EvaluationContext context); // null é permitido
}
{
"type": "EXCLUSIVE_GATEWAY",
"providerType": "BEAN",
"providerBean": "customerRiskStrategy",
"outgoing": [
{ "targetNodeId": "emitir-parecer-external-task", "isDefault": true },
{ "targetNodeId": "solicitar-dados-task", "handlesNull": true },
{ "targetNodeId": "avaliar-fraude", "expectedAnswer": "FRAUDE" }
]
}
@Component("customerRiskStrategy")
public class CustomerRiskStrategy implements AnswerProvider {
@Override
public String resolve(EvaluationContext context) {
return context.getVariableValue("riskScore")
.filter(Objects::nonNull)
.map(riskScore -> (Double) riskScore < 50 ? "FRAUDE" : "APROVADO")
.orElse(null);
}
}

EvaluationContext é deliberadamente mais enxuto que ExecutionContext (usado em TaskHandler) — é somente-leitura, expondo apenas getProcessInstanceId(), getVariableValue(String) e getVariables(). Essa assimetria é intencional: uma decisão não deve ter efeitos colaterais sobre o estado do processo. Isso é imposto na assinatura da interface, não apenas por convenção — o que a torna trivialmente testável com um mock de mapa de variáveis, sem qualquer dependência do motor.

providerType: VARIABLE

Quando a resposta já está calculada (por exemplo, definida por um TaskHandler anterior ou por uma tarefa externa que injeta o resultado de uma decisão humana), não é preciso escrever nenhum bean — o gateway lê diretamente uma variável de processo:

{
"type": "EXCLUSIVE_GATEWAY",
"providerType": "VARIABLE",
"providerVariable": "acaoResultadoAnaliseFraude",
"outgoing": [
{ "targetNodeId": "finished", "expectedAnswer": "FINALIZAR" },
{ "targetNodeId": "CALCULATE_CUSTOMER_RISK_ST", "expectedAnswer": "RECALCULAR" }
]
}

Internamente, Navigator.resolveAnswer chama variables.get(gateway.providerVariable()) e converte o valor para String via toString(). Se a variável não existir ou for null, a resposta resolvida é null (mesma semântica de handlesNull do modo BEAN).

:::warning Erros de configuração são detectados no deploy O DeployValidator (kikwi-core/validation) valida, no momento do deploy, que todo gateway BEAN aponte para um bean AnswerProvider de fato registrado no Spring, e que todo gateway VARIABLE tenha providerVariable preenchido. Um processo mal configurado nunca chega a ser implantado — ele falha com InvalidProcessDefinitionException antes de qualquer instância poder ser iniciada, evitando que o erro só seja descoberto em runtime, no meio de uma execução de produção. :::

Regras de casamento de arestas (findMatchingFlow)

Depois que uma resposta é resolvida, o Navigator escolhe a aresta de saída seguindo esta ordem de prioridade:

  1. Resposta nula → procura a (única) aresta marcada handlesNull: true. Se nenhuma existir, lança IllegalStateException — uma resposta null sem rota de tratamento é um erro de execução, não é silenciosamente ignorada.
  2. Resposta não-nula → procura a primeira aresta cujo expectedAnswer seja igual (via .equals()) à resposta.
  3. Nenhum match → recorre à aresta marcada isDefault: true. Se nenhuma existir, lança IllegalStateException.
private SequenceFlowDefinition findMatchingFlow(ExclusiveGatewayDefinition gateway, String answer) {
if (answer == null) {
return outgoingFlows.stream().filter(SequenceFlowDefinition::handlesNull).findFirst()
.orElseThrow(() -> new IllegalStateException(/* ... */));
}
return outgoingFlows.stream().filter(sf -> answer.equals(sf.expectedAnswer())).findFirst()
.orElseGet(() -> outgoingFlows.stream().filter(SequenceFlowDefinition::isDefault).findFirst()
.orElseThrow(() -> new IllegalStateException(/* ... */)));
}

:::tip Boa prática de modelagem Sempre declare uma aresta isDefault: true em gateways cuja resposta possa assumir valores não previstos explicitamente (ex.: um AnswerProvider que evolui com o tempo), e uma aresta handlesNull: true sempre que o provider puder retornar null legitimamente. O DeployValidator também rejeita definições com mais de uma aresta isDefault no mesmo gateway. :::

Fan-out: PARALLEL_GATEWAY

Um gateway paralelo não avalia nenhuma condição — ele instrui o motor a seguir todas as arestas de saída simultaneamente, cada uma como um ramo (branch) independente identificado por um branchId gerado (UUID):

if ("PARALLEL_GATEWAY".equals(completedNode.type())) {
List<FlowNodeDefinition> nextNodes = /* todos os targets de outgoing */;
String targetJoinId = ((ParallelGatewayDefinition) completedNode).targetJoinId();
FlowNodeDefinition targetJoinNode = processDefinition.flowNodes().get(targetJoinId);
return new Continuation(nextNodes, true, null, null, targetJoinNode);
}

Note que a continuação retornada é sempre isAsynchronous = true — um split paralelo sempre cruza uma fronteira transacional, mesmo que nenhum dos nós filhos tenha commitBefore: true explicitamente. Os detalhes de como os ramos são materializados como ExecutableTasks com pendingBranchIds estão em Execução Síncrona e Assíncrona.

Fan-in: JOIN_GATEWAY

Um join não decide nada — ele apenas segue sua única aresta de saída assim que todos os ramos pendentes tiverem sido concluídos (mecanismo de contagem coberto na próxima página). Se o join não tiver saída (outgoing vazio), o Navigator retorna null, sinalizando fim daquele ramo de execução.

Casamento de error handlers de borda

Navigator.findMatchingErrorHandler é usado pelo ProcessExecutionManager quando um TaskHandler lança uma ProcessErrorException — ele inspeciona os boundaryEventIds do nó que falhou, filtra os que são ErrorHandlerDefinition, e escolhe o primeiro cujo errorCode seja null (curinga, casa qualquer erro) ou igual ao código lançado. Detalhes completos em Tratamento de Erros de Negócio.

Observabilidade de decisões

Quando kikwiflow.stats.enabled=true ou kikwiflow.outbox.events-enabled=true, toda resolução de um gateway exclusivo emite um evento GATEWAY_ANSWER_RESOLVED (GatewayAnswerResolved) contendo providerType, providerBean/providerVariable, a resposta resolvida e o id da aresta escolhida — útil para auditoria de decisões de negócio sem precisar instrumentar manualmente cada AnswerProvider. Ver Eventos e Observabilidade.