Eventos e Observabilidade
O Kikwiflow expõe dois canais de eventos completamente independentes, com garantias e propósitos diferentes. Confundir os dois é o erro de design mais comum ao integrar observabilidade — escolha o canal errado e você acaba com telemetria que promete consistência que não entrega, ou com um outbox transacional pesado demais para simples métricas de performance.
Eventos leves (lightweight) | Eventos críticos (outbox) | |
|---|---|---|
| Flag de ativação | kikwiflow.stats.enabled | kikwiflow.outbox.events-enabled |
| Entrega | Fire-and-forget, assíncrona, em memória | Persistida na mesma transação do UnitOfWork (implementação MongoDB) |
| Garantia | Nenhuma — pode se perder em crash/restart | Durável — commit atômico junto com a mudança de estado |
| Uso recomendado | Métricas de performance, dashboards, APM | Auditoria/histórico de execução, e como base para um consumidor de relay (ver nota sobre escopo abaixo) |
:::info Escopo: esta lib entrega a persistência, não o relay
O Kikwiflow OSS garante que o evento crítico existe e é consultável — de forma transacional e durável. A
drenagem/entrega desses eventos para um sistema externo (Kafka, webhook, outro banco) — o "R" do padrão
outbox-relay — é deliberadamente deixada como um ponto de extensão (kikwi-critical-event-relay-api), sem uma
implementação embutida nesta versão. O schema da coleção outbox_events (campos relayStatus/lockedUntil,
descritos abaixo) já foi desenhado para suportar um consumidor de relay — seja um construído pela sua equipe,
seja uma futura oferta enterprise do Kikwiflow — sem precisar de migração de dados.
:::
Eventos leves: ExecutionEventListener
A interface pública que uma aplicação implementa para observar a execução é:
@FunctionalInterface
public interface ExecutionEventListener {
void onEvents(List<LightweightEvent> events);
}
public interface LightweightEvent {
Instant getTimestamp();
}
Qualquer bean Spring que implemente ExecutionEventListener é automaticamente registrado no
AsynchronousEventPublisher pelo KikwiflowAutoConfiguration (via ObjectProvider<List<ExecutionEventListener>>)
— não é preciso fiação manual:
@Component
public class MetricsListener implements ExecutionEventListener {
@Override
public void onEvents(List<LightweightEvent> events) {
events.forEach(event -> {
if (event instanceof SyncContinuationFailed failure) {
log.warn("Continuação síncrona falhou: instância={}, nó={}, erro={}",
failure.processInstanceId(), failure.failedNodeDefinitionId(), failure.errorMessage());
}
});
}
}
AsynchronousEventPublisher: entrega via pool de virtual threads
public class AsynchronousEventPublisher implements EventPublisher {
private final List<ExecutionEventListener> listeners = new CopyOnWriteArrayList<>();
private final ExecutorService listenerExecutor; // Executors.newVirtualThreadPerTaskExecutor()
public void publishEvents(List<LightweightEvent> events) {
for (ExecutionEventListener listener : listeners) {
listenerExecutor.submit(() -> listener.onEvents(events));
}
}
}
Cada listener é notificado em uma submissão de executor separada — um listener lento ou que lança exceção não bloqueia nem afeta outros listeners, nem a execução do processo em si, já que a publicação acontece de forma totalmente desacoplada do caminho crítico de persistência.
:::danger Sem garantia de entrega
SyncContinuationFailed, hoje o único evento leve emitido pelo motor (disparado quando uma continuação síncrona
falha após a conclusão de uma tarefa externa — ver KikwiflowEngine.completeExternalTask), é publicado via
fire-and-forget. Se a JVM cair entre a falha e a entrega do evento ao listener, o evento é perdido
silenciosamente. Nunca implemente lógica de negócio essencial (ex.: reverter um efeito colateral, notificar
um cliente) dentro de um ExecutionEventListener — use o outbox para isso.
:::
O catálogo de eventos críticos: CriticalEventType
Todo evento crítico conhecido pelo motor está registrado em um único enum, usado tanto para construir eventos (em vez de literais de string soltos pelo código) quanto para o dispatch polimórfico na camada de persistência:
public enum CriticalEventType {
FLOW_NODE_FINISHED(FlowNodeFinished.class),
GATEWAY_ANSWER_RESOLVED(GatewayAnswerResolved.class),
PROCESS_INSTANCE_FINISHED(ProcessInstanceFinished.class),
PROCESS_INSTANCE_STARTED(ProcessInstanceStarted.class),
INCIDENT_CREATED(IncidentCreated.class),
INCIDENT_RESOLVED(IncidentResolved.class),
EXTERNAL_TASK_CLAIMED(ExternalTaskClaimed.class),
EXTERNAL_TASK_UNCLAIMED(ExternalTaskUnclaimed.class),
EXTERNAL_TASK_COMPLETED(ExternalTaskCompleted.class),
RETRY_SCHEDULED(RetryScheduled.class),
PROCESS_VARIABLE_CHANGED(ProcessVariableChanged.class),
TIMER_FIRED(TimerFired.class),
ORPHANED_CHILD_COMPLETION(OrphanedChildCompletion.class);
private final Class<? extends CriticalEvent> payloadType;
}
public interface CriticalEvent {
String processInstanceId();
String processDefinitionId();
}
Toda implementação de CriticalEvent expõe processInstanceId()/processDefinitionId() — isso é o que permite
à camada de persistência indexar e consultar eventos por instância sem precisar de uma cadeia de instanceof.
Eventos críticos: o padrão Outbox
Cada mudança de estado relevante do processo é serializada como um OutboxEventEntity — com identidade própria
(id, gerado automaticamente) — e incluída no mesmo UnitOfWork transacional que persiste a mudança de
estado em si:
public class OutboxEventEntity {
private String id; // UUID gerado no construtor — vira o _id do documento persistido
private Instant timestamp;
private String event; // nome do CriticalEventType, ex. "FLOW_NODE_FINISHED"
private CriticalEvent payload;
public OutboxEventEntity(CriticalEventType type, CriticalEvent payload) { /* ... */ }
}
Quando kikwiflow.outbox.events-enabled=true, a implementação MongoDB (MongoKikwiEngineRepository.commitWork)
grava cada OutboxEventEntity do UnitOfWork na coleção outbox_events, dentro da mesma transação nativa
(ClientSession.withTransaction) que persiste a mudança de estado da instância:
if (outboxPersistenceEnabled && unitOfWork.events() != null && !unitOfWork.events().isEmpty()) {
List<InsertOneModel<Document>> eventWrites = unitOfWork.events().stream()
.map(OutboxEventMapper::toDocument)
.map(InsertOneModel::new)
.toList();
outboxEvents.bulkWrite(clientSession, eventWrites);
}
Isso significa que, ao contrário dos eventos leves, um evento crítico nunca é perdido silenciosamente: ou a transação inteira (mudança de estado + evento) é commitada, ou nenhuma das duas é — consistência garantida pelo mesmo mecanismo transacional descrito em Persistência e Consistência Transacional.
:::info A checagem da flag acontece de novo, no ponto de persistência
kikwiflow.outbox.events-enabled é verificada tanto na construção do evento (dentro de
CriticalEventRecorder, ver seção abaixo) quanto, independentemente, dentro de commitWork, imediatamente
antes da escrita física na coleção. Isso garante que "outbox desabilitado" signifique de fato "nada é escrito",
mesmo que algum caminho upstream decida montar o UnitOfWork com eventos preenchidos.
:::
Construindo os eventos: CriticalEventRecorder
Todo evento crítico é construído por um único colaborador, CriticalEventRecorder (kikwi-core,
io.kikwiflow.execution.event), injetado tanto em ProcessExecutionManager quanto em ContinuationService —
antes, cada um construía seus próprios FlowNodeFinished/GatewayAnswerResolved/ProcessInstanceFinished
inline, cada um com sua própria checagem de kikwiflowConfig.isOutboxEventsEnabled(), o que já causou uma
inconsistência real (ProcessExecutionManager também aceitava stats.enabled como gatilho, ContinuationService
não). O flag é resolvido uma única vez, no construtor do recorder:
public class CriticalEventRecorder {
private final KikwiflowConfig kikwiflowConfig;
public CriticalEventRecorder(KikwiflowConfig kikwiflowConfig) {
this.kikwiflowConfig = kikwiflowConfig;
}
public boolean isEnabled() {
return kikwiflowConfig.isOutboxEventsEnabled();
}
public void recordFlowNodeFinished(List<OutboxEventEntity> events, FlowNodeExecutionSnapshot snapshot, RuntimeException error) { /* ... */ }
public void recordGatewayAnswerResolved(List<OutboxEventEntity> events, ProcessInstanceExecution processInstance, ProcessDefinition processDefinition, ExclusiveGatewayDefinition gateway, Continuation continuation) { /* ... */ }
public void recordProcessInstanceFinished(List<OutboxEventEntity> events, ProcessInstanceExecution processInstanceExecution, ProcessDefinition processDefinition) { /* ... */ }
public void recordInterruptedFlowNode(List<OutboxEventEntity> events, ProcessInstanceExecution processInstanceExecution, ProcessDefinition processDefinition, String interruptedNodeDefinitionId, String interruptedByNodeDefinitionId) { /* ... */ }
public void recordProcessInstanceStarted(List<OutboxEventEntity> events, ProcessInstanceExecution processInstanceExecution, ProcessDefinition processDefinition) { /* ... */ }
public void recordIncidentCreated(List<OutboxEventEntity> events, Incident incident, String tenantId) { /* ... */ }
public void recordIncidentResolved(List<OutboxEventEntity> events, Incident resolvedIncident, String tenantId, String actorId) { /* ... */ }
public void recordExternalTaskClaimed(List<OutboxEventEntity> events, ExternalTask task, String assignee, String actorId) { /* ... */ }
public void recordExternalTaskUnclaimed(List<OutboxEventEntity> events, ExternalTask task, String actorId) { /* ... */ }
public void recordExternalTaskCompleted(List<OutboxEventEntity> events, ExternalTask task, String actorId) { /* ... */ }
public void recordRetryScheduled(List<OutboxEventEntity> events, ExecutableTask task, long executionsSoFar, long retriesLeft, Instant nextDueDate, String errorMessage, String tenantId) { /* ... */ }
public void recordProcessVariableChanged(List<OutboxEventEntity> events, String processInstanceId, String processDefinitionId, String tenantId, Map<String, ProcessVariable> variables, String actorId) { /* ... */ }
public void recordVariablesUnset(List<OutboxEventEntity> events, String processInstanceId, String processDefinitionId, String tenantId, Set<String> variableNames, String actorId) { /* ... */ }
public void recordTimerFired(List<OutboxEventEntity> events, ProcessInstanceExecution processInstanceExecution, String flowNodeDefinitionId, Instant nextDueDate) { /* ... */ }
}
Os parâmetros tenantId/actorId presentes em vários desses métodos (recordIncidentCreated,
recordIncidentResolved, recordExternalTaskClaimed/Unclaimed, recordRetryScheduled,
recordProcessVariableChanged, recordVariablesUnset) são threaded pelo chamador — Incident/ExecutableTask
não carregam tenantId no domínio, e actorId só é conhecido na borda que recebeu o IdentityContext (ver as
notas sobre actorId e tenantId mais abaixo).
Cada método record* recebe a lista acumuladora (List<OutboxEventEntity>) e os dados necessários, e decide
sozinho — internamente — se deve montar o payload e adicioná-lo à lista; os chamadores nunca checam o flag por
conta própria. recordInterruptedFlowNode também unifica o que antes eram duas construções quase idênticas em
ContinuationService (uma para boundary timer anexado a ExecutableTask, outra a ExternalTask). Chamadores
que fariam trabalho caro só para descartá-lo (ex.: montar um FlowNodeExecutionSnapshot completo) podem
consultar isEnabled() antes, para pular esse trabalho — é o único lugar onde o flag "vaza" para fora do
recorder, e só para evitar alocação desnecessária, não para decidir o quê construir.
Tipos de evento crítico emitidos hoje
event | Payload (CriticalEvent) | Quando é emitido |
|---|---|---|
FLOW_NODE_FINISHED | FlowNodeFinished | Ao final de cada execução de nó — sucesso, erro ou interrupção (boundary timer/error handler). Inclui nodeExecutionStatus (SUCCESS/INTERRUPTED/ERROR), flowNodeName/flowNodeDescription, startedAt/finishedAt, interruptedByNodeDefinitionId quando aplicável, e errorType/errorMessage/errorStackTrace quando nodeExecutionStatus == ERROR. |
GATEWAY_ANSWER_RESOLVED | GatewayAnswerResolved | Sempre que um EXCLUSIVE_GATEWAY resolve uma resposta — carrega providerType, providerBean/providerVariable, a resposta resolvida e o id da aresta escolhida. Ver Navegação e Gateways de Decisão. |
PROCESS_INSTANCE_FINISHED | ProcessInstanceFinished | Quando uma instância atinge ProcessInstanceStatus.COMPLETED. Carrega o snapshot final de variáveis, businessValue, tenantId, origin, parentInstanceId/callerTaskId/callerBranchId (call activity) e getDurationMs() derivado de startedAt/endedAt. |
PROCESS_INSTANCE_STARTED | ProcessInstanceStarted | Contraparte de PROCESS_INSTANCE_FINISHED — emitido uma única vez, na primeira continuação da instância (ProcessStarter.execute()). Carrega o mesmo tipo de snapshot inicial (variáveis, businessValue, tenantId, origin), sem endedAt/status. actorId vem de ProcessStarter.byActor(...) — null se não informado. |
INCIDENT_CREATED | IncidentCreated | Quando FailureHandler.handleFailure abre um incidente (retries esgotados, ou ProcessErrorException não tratado por nenhum boundary). actorId é sempre IdentityContext.system(). |
INCIDENT_RESOLVED | IncidentResolved | Quando KikwiflowEngine.retryIncident resolve manualmente um incidente aberto. actorId vem do IdentityContext do chamador. |
EXTERNAL_TASK_CLAIMED | ExternalTaskClaimed | Quando KikwiflowEngine.claim atribui uma external/human task a um assignee. actorId vem do IdentityContext do chamador — normalmente igual a assignee, mas podem divergir (ex.: um supervisor atribuindo em nome de outro). |
EXTERNAL_TASK_UNCLAIMED | ExternalTaskUnclaimed | Quando KikwiflowEngine.unclaim remove o assignee de uma external/human task — carrega previousAssignee e actorId (do IdentityContext do chamador). |
EXTERNAL_TASK_COMPLETED | ExternalTaskCompleted | Quando KikwiflowEngine.completeExternalTask completa a tarefa. Carrega assignee (quem estava atribuído no momento, null se nunca foi clamada) e actorId (quem comandou o complete) lado a lado — o motor não valida um contra o outro, o complete é soberano; esse par existe só para que quem consome o evento detecte "completado por alguém diferente do atribuído" (ver nota abaixo). |
RETRY_SCHEDULED | RetryScheduled | Quando FailureHandler.handleFailure reagenda uma tarefa que falhou mas ainda tem retries disponíveis (o ramo que não abre incidente). actorId é sempre IdentityContext.system(). |
PROCESS_VARIABLE_CHANGED | ProcessVariableChanged | Um evento por variável alterada via KikwiflowEngine.setVariables ou removida via KikwiflowEngine.unsetVariables (recordVariablesUnset). No caso de remoção, o payload traz removed=true e value=null; caso contrário, removed=false e o valor bruto — masking é responsabilidade de quem consome o outbox, não deste ponto de construção (ver nota sobre escopo/masking abaixo). actorId vem do IdentityContext do chamador. |
TIMER_FIRED | TimerFired | Quando um timer não-interruptivo dispara e se reagenda (ContinuationService, ramo NON_INTERRUPTIVE_TIMER). Timers interruptivos continuam cobertos por FLOW_NODE_FINISHED(INTERRUPTED) via recordInterruptedFlowNode. actorId é sempre IdentityContext.system(). |
ORPHANED_CHILD_COMPLETION | OrphanedChildCompletion | Quando uma instância filha de um CALL_ACTIVITY_COORDINATOR conclui, mas a coordenadora do pai já não existe mais (cancelada por timeout antes). processInstanceId/processDefinitionId/tenantId identificam a instância filha — não o pai. Construído diretamente pela camada de persistência (InMemoryKikwiEngineRepository/MongoKikwiEngineRepository), não por CriticalEventRecorder. Sem actorId — nunca é uma ação de usuário. |
:::info Quem comandou a ação: actorId e o sentinel IdentityContext.system()
9 dos 13 tipos carregam actorId. Para os que nascem de uma chamada explícita de um usuário/API
(claim, unclaim, completeExternalTask, retryIncident, setVariables, ProcessStarter.byActor), actorId
vem do IdentityContext.actorId() do chamador. Para os que o motor dispara sozinho, sem nenhum humano por trás
(IncidentCreated, RetryScheduled, TimerFired — todos originados em FailureHandler/ContinuationService
reagindo a uma falha ou um timer), actorId é sempre o sentinel IdentityContext.system().actorId()
("__KIKWIFLOW_SYSTEM__"). Os 3 tipos de evento mais antigos (FLOW_NODE_FINISHED, GATEWAY_ANSWER_RESOLVED,
PROCESS_INSTANCE_FINISHED) ainda não carregam actorId — eles nascem várias camadas abaixo de onde o
IdentityContext está disponível hoje (ProcessExecutionManager/ContinuationService), e alguns caminhos de
execução (worker em background via TaskAcquirer.executeFromTask) não têm identidade alguma para propagar.
:::
:::info EXTERNAL_TASK_COMPLETED: soberania preservada, auditoria separada
assignee/actorId divergentes em EXTERNAL_TASK_COMPLETED não é um erro — completeExternalTask sempre
completa a tarefa, atribuída ou não, para quem quer que a chame (esse é um requisito de produto: o complete não
pode depender de estar clamada). O evento só existe para que consumidores de auditoria/observabilidade possam
perguntar "isso foi completado por alguém diferente do atribuído?" sem que o motor precise saber ou se importar
com a resposta.
:::
:::info tenantId está em todos os 13 tipos
Diferente de actorId, tenantId foi retrofitado em todos os payloads (CriticalEvent.tenantId() é
obrigatório na interface, não default). A fonte varia por tipo: a maioria deriva de
ProcessInstanceExecution.getTenantId()/ProcessInstance.tenantId(), já disponível em todo caminho de
construção; EXTERNAL_TASK_CLAIMED/EXTERNAL_TASK_UNCLAIMED usam ExternalTask.tenantId() diretamente;
INCIDENT_CREATED/INCIDENT_RESOLVED/RETRY_SCHEDULED precisaram de um parâmetro extra threaded pelo chamador
(FailureHandler.handleFailure/KikwiflowEngine.retryIncident), já que Incident/ExecutableTask não carregam
esse campo no domínio.
:::
Todos os payloads carregam tanto processDefinitionId (o id interno, específico da versão implantada) quanto,
quando disponível, processDefinitionKey (o identificador estável usado em startProcess().byKey(...), o mesmo
entre versões) — para agrupar/filtrar execuções de "o mesmo processo" ao longo de reimplantações, use
processDefinitionKey.
Todos os tipos só são construídos quando kikwiflow.outbox.events-enabled está ligado — kikwiflow.stats.enabled
não afeta o outbox (ver nota acima sobre a inconsistência que isso causava antes de existir o
CriticalEventRecorder).
:::info claim/unclaim/setVariables/unsetVariables não passam por UnitOfWork
Diferente da maioria dos eventos (que viajam dentro de um UnitOfWork já existente), EXTERNAL_TASK_CLAIMED,
EXTERNAL_TASK_UNCLAIMED e PROCESS_VARIABLE_CHANGED (tanto o set quanto o unset) são construídos em
KikwiflowEngine e passados diretamente para
KikwiEngineRepository.claim/unclaim/addVariables/unsetVariables — métodos que hoje mutam estado fora do
padrão UnitOfWork/commitWork. A implementação MongoDB ainda garante atomicidade: cada um desses métodos abre
sua própria ClientSession.withTransaction para gravar a mudança de estado e o evento crítico juntos.
:::
:::warning Masking na escrita: nunca. Na leitura via API: sempre
PROCESS_VARIABLE_CHANGED grava value sem aplicar VariableSecurityPolicyManager.applyReadPoliciesAndMasking
— decisão de escopo deliberada: o motor não decide o que é sensível no momento de persistir o evento. Isso não
significa que ninguém aplica masking: a API REST de histórico (GET /process-instances/{id}/events, abaixo)
aplica o policy manager no momento da leitura, antes de devolver a resposta. Um relay/consumer que leia
outbox_events diretamente (sem passar por essa API) ainda recebe o valor bruto — é responsabilidade desse
consumidor aplicar sua própria política.
:::
API REST de histórico: GET /process-instances/{id}/events
kikwi-management-rest expõe a timeline completa de uma instância — a mesma lista de
findEventHistoryByProcessInstanceId, mapeada para HistoryEventSummary (kikwi-model,
io.kikwiflow.model.event):
public record HistoryEventSummary(
String id,
CriticalEventType eventType,
String processInstanceId,
String processDefinitionId,
String tenantId,
String actorId,
Instant timestamp,
CriticalEvent payload
) {}
HistoryEventSummaryMapper (kikwi-management-rest) promove ao topo os campos comuns (para não obrigar o
cliente a fazer instanceof só para montar uma linha de timeline) e mantém o payload completo, específico do
tipo, para o detalhamento. Quando payload é ProcessVariableChanged, o mapper aplica
VariableSecurityPolicyManager.applyReadPoliciesAndMasking antes de devolver — se a policy negar leitura da
variável, value vem como null na resposta, nunca o bruto.
:::info Sem enforcement de tenant neste endpoint — de propósito
Diferente do que seria natural supor, este endpoint não valida identityContext.tenantId() contra o
tenantId da instância. É uma decisão consciente: este endpoint é para uso backoffice/suporte/TI (o "Cockpit"
do Kikwiflow), não uma API de uso geral de aplicação cliente — quem acessa o monitor já tem permissão
independente de tenant. Aplicações que expõem esse dado para usuários finais devem implementar seu próprio
enforcement de tenant na camada de aplicação, não esperar que o Kikwiflow faça isso aqui.
:::
Registro do endpoint é condicional a kikwiflow.history.enabled (default true, KikwiflowHistoryProperties,
módulo kikwi-management-rest-spring-boot-autoconfigure) — desligar essa flag remove o endpoint inteiro, mesmo
com kikwiflow.outbox.events-enabled=true (que controla se o dado existe, não se é servido). 404 se a
instância não existir; lista vazia (não erro) se o outbox nunca foi habilitado para essa instância.
Reconstruindo por onde uma instância passou
A coleção outbox_events não é apenas uma fila de relay transiente — entradas nunca são deletadas quando
confirmadas/relayadas (apenas marcadas), então ela dobra como histórico durável e consultável de execução.
QueryRepository.findEventHistoryByProcessInstanceId(String processInstanceId) retorna todos os eventos de uma
instância, ordenados por timestamp ascendente:
List<OutboxEventEntity> history = kikwiEngineRepository.findEventHistoryByProcessInstanceId(processInstanceId);
// ex.: ENRICH_CUSTOMER_PROFILE_ST (FLOW_NODE_FINISHED) -> CALCULATE_CUSTOMER_RISK_ST (FLOW_NODE_FINISHED)
// -> GATEWAY-CLASSIFICACAO-RISCO (GATEWAY_ANSWER_RESOLVED, resposta "FRAUDE") -> ...
Isso só retorna dados quando kikwiflow.outbox.events-enabled=true — sem persistência habilitada, a lista é
sempre vazia. Note que apagar uma instância concluída (deleteProcessInstanceById) não apaga seu histórico
em outbox_events — é uma decisão consciente de design, documentada no próprio código-fonte do repositório.
Retenção: TTL nativo do MongoDB
Por padrão, outbox_events retém tudo indefinidamente. Se isso não for desejável (auditoria de curto prazo,
custo de armazenamento, ou o consumo é feito por uma ferramenta de relay que já move os dados para outro lugar),
configure kikwiflow.outbox.ttl com uma duração ISO-8601:
kikwiflow:
outbox:
events-enabled: true
ttl: P30D # eventos expiram 30 dias após o timestamp — MongoDB TTL nativo (expireAfterSeconds)
Isso cria (ou ajusta, via collMod, se o valor mudar entre deploys) um índice TTL real sobre o campo
timestamp — a expiração é feita pelo próprio MongoDB em background, sem nenhum job/scheduler do Kikwiflow
envolvido. Se ttl não for declarado, nenhum índice TTL é criado e a retenção é indefinida (comportamento
padrão). Diminuir ou remover o TTL depois de já criado não apaga o índice automaticamente — isso exige um passo
manual (db.outbox_events.dropIndex("outbox_ttl_idx")).
Ponto de extensão para relay: o schema já está pronto
OutboxEventMapper grava cada documento em outbox_events com dois campos pensados especificamente para um
futuro consumidor de relay, mesmo que nenhuma implementação venha embutida nesta versão:
{
"_id": "<uuid>",
"eventType": "FLOW_NODE_FINISHED",
"processInstanceId": "...",
"processDefinitionId": "...",
"timestamp": ISODate(...),
"relayStatus": "PENDING",
"lockedUntil": null,
"payload": { "...": "..." }
}
relayStatus(PENDING/LOCKED/RELAYED) elockedUntilexistem para suportar um padrão de claim atômico viafindOneAndUpdate— o mesmo usado porfindAndLockDueTasks(ver Workers e Tarefas Externas) — sem exigir migração de schema quando um consumidor de relay for adicionado, seja construído internamente pela sua equipe, seja uma oferta futura.- A interface
OutboxReader(kikwi-critical-event-relay-api) já define o contrato esperado (readAndLockNextBatch/confirmBatch), mas não há implementação MongoDB fornecida nesta versão do Kikwiflow — nenhum bean é registrado automaticamente, e nenhum processo de drenagem roda em background. - Índices já existem para dar suporte a essa consulta (
relay_status_timestamp_idx, sobrerelayStatus+timestamp) mesmo sem nenhum consumidor rodando hoje.
Escolhendo o canal certo
:::tip Regra prática
- Precisa de um dashboard de latência por nó, sem garantias fortes? Eventos leves.
- Precisa de auditoria durável, ou consultar por onde uma instância específica passou (depuração, suporte)?
Outbox, via
findEventHistoryByProcessInstanceId. - Precisa entregar esses eventos para um sistema externo (Kafka, webhook, data warehouse)? Outbox como base de dados de origem — mas você (ou uma lib de relay dedicada) precisa implementar o consumidor; esta versão não inclui um pronto para uso.
- Só precisa consultar o estado atual de uma instância (não seu histórico)? Nenhum dos dois — use o lado de
query do CQRS (
QueryRepository/ExternalTaskQueryService), descrito na Visão Geral. :::