Pular para conteúdo

Eventos, Outbox e Inbox

Core e Template Service escrevem outbox_events no contexto de seus tenants. O Dispatcher lê/atualiza essa mesma collection, acrescentando estado de transporte. Collections com o mesmo nome podem compartilhar um banco se a configuração resolver o mesmo tenant; ownership aqui é por operação, não uma alegação de exclusividade física.

Modelos visuais

Evento e registro de processamento

As duas collections se correlacionam pela identidade lógica do evento; o payload transporta a representação String.

Evento e registro de processamento; visão simplificada. Detalhes no visualizador abaixo. Evento e registro de processamento; visão simplificada em tema escuro.

Explorar diagramas detalhados com zoom

Legenda e convenções dos modelos. Os dicionários e evidências desta página permanecem a referência dos campos.

Persistência e transporte

%%{init: {"theme":"base","themeVariables":{"fontFamily":"Arial, sans-serif","fontSize":"16px","lineColor":"#64748b","primaryTextColor":"#172b4d"},"flowchart":{"htmlLabels":true,"useMaxWidth":true,"nodeSpacing":28,"rankSpacing":38,"padding":14,"curve":"linear","subGraphTitleMargin":{"top":12,"bottom":16}},"sequence":{"useMaxWidth":true,"actorMargin":28,"width":145,"height":45,"diagramMarginX":32,"diagramMarginY":12,"messageMargin":24,"wrap":true,"mirrorActors":false},"themeCSS":".edgeLabel, .edgeLabel p {background-color:var(--diagram-surface)!important;color:var(--diagram-text)!important}\n.cluster rect {fill:var(--diagram-group)!important;stroke:var(--diagram-line)!important}\n.cluster-label span, .cluster-label text {color:var(--diagram-text)!important;fill:var(--diagram-text)!important}\n.flowchart-link, .actor-line {stroke:var(--diagram-line)!important}\nmarker path {fill:var(--diagram-line)!important;stroke:var(--diagram-line)!important}\n.actor {fill:var(--diagram-domain)!important;stroke:var(--diagram-domain-border)!important}\ntext.actor, .messageText, .loopText, .labelText {fill:var(--diagram-text)!important}\n.messageLine0, .messageLine1, .loopLine {stroke:var(--diagram-line)!important}\n.labelBox {fill:var(--diagram-surface)!important;stroke:var(--diagram-line)!important}\n.note {fill:var(--diagram-async)!important;stroke:var(--diagram-async-border)!important}\n.noteText {fill:var(--diagram-text)!important}\n.nodeLabel, .nodeLabel p, .nodeLabel span {color:var(--diagram-text)!important}\n.node .nodeLabel, .node .nodeLabel p {font-size:17px;font-weight:600;line-height:1.4;text-align:center}\n.nodeLabel small {display:inline-block;margin-top:4px;font-size:15px;font-weight:400;line-height:1.4;color:var(--diagram-description)!important}\ntext.actor, text.actor tspan {font-size:17px;font-weight:600}\n.node.entry rect, .node.entry polygon, .node.entry path, .node.entry circle {fill:var(--diagram-entry)!important;stroke:var(--diagram-entry-border)!important}\n.node.domain rect, .node.domain polygon, .node.domain path, .node.domain circle {fill:var(--diagram-domain)!important;stroke:var(--diagram-domain-border)!important}\n.node.security rect, .node.security polygon, .node.security path, .node.security circle {fill:var(--diagram-security)!important;stroke:var(--diagram-security-border)!important}\n.node.store rect, .node.store polygon, .node.store path, .node.store circle {fill:var(--diagram-store)!important;stroke:var(--diagram-store-border)!important}\n.node.async rect, .node.async polygon, .node.async path, .node.async circle {fill:var(--diagram-async)!important;stroke:var(--diagram-async-border)!important}\n.node.external rect, .node.external polygon, .node.external path, .node.external circle {fill:var(--diagram-external)!important;stroke:var(--diagram-external-border)!important}\n text.actor tspan, .loopText tspan, .labelText tspan, .messageText tspan, .noteText tspan {fill:var(--diagram-text)!important}\n\n"}}%%
flowchart TB
    producer["Core / Template Service<br/><small>Alteração e evento de domínio</small>"]:::domain
    outbox["outbox_events<br/><small>Documento Mongo por tenant</small>"]:::store
    dispatcher["Outbox Dispatcher<br/><small>Claim, lease e retry</small>"]:::async
    rabbit[["RabbitMQ<br/><small>gateway-events</small>"]]:::async
    producer -->|Insert| outbox
    dispatcher -->|Atualização condicional| outbox
    dispatcher -->|Payload JSON / confirm| rabbit
    classDef domain stroke-width:1.5px;
    classDef store stroke-width:1.5px;
    classDef async stroke-width:1.5px;
Campo BSON Produtor Java / Dispatcher Go Escrita e significado
_id ObjectId / primitive.ObjectID Identidade do documento
eventId ObjectId / string no modelo Go Identidade do evento; contratos possuem diferença de tipo a validar, registrada na análise
tenantId String no produtor; não declarado no struct Go Contexto de domínio; também incluído no payload
routingKey String / string Evento de domínio
payload Document / bson.Raw Conteúdo enviado ao Rabbit; não é documento de negócio inteiro obrigatoriamente
status Enum Java PENDING/SENT; Go PENDING/PROCESSING/SENT PROCESSING é escrito pelo Dispatcher
attempts Integer / int Zero na criação; incremento em falha
nextAttemptAt Instant / time.Time Agora na criação; agenda backoff
createdAt, updatedAt Herdados de BaseEntity Java Timestamps do produtor; Go declara createdAt
lastError String / *string Falha; reset ao marcar SENT
leaseUntil *time.Time no Go Escrito no claim, limpo em conclusão/falha/reaper
workerId string no Go Dono do claim; filtro condicional nas atualizações
sentAt *time.Time no Go Conclusão do transporte
exchange string no struct Go; ausente no modelo produtor O fluxo de publicação usa o exchange da configuração, não esse campo

Índices criados pelo Dispatcher: {status:1,nextAttemptAt:1,createdAt:1}, {status:1,leaseUntil:1} e {eventId:1} unique. Sem TTL/remoção de SENT declarado. FindOneAndUpdate reivindica PENDING vencido e define lease/worker; sucesso marca SENT; falha volta a PENDING com attempts+1 e backoff. Reaper libera PROCESSING com lease vencido.

Defaults do Dispatcher: polling 5 min, atualização de databases 10 min, batch 200, lease 30 s, backoff base 10 s/máximo 900 s. São defaults de código, não configuração corporativa. Publicação usa confirmação Rabbit; publicar e marcar SENT não são operação atômica. Repetição após falha nesse intervalo é possível.

Fontes: documento Go, claim/índices, dreno, config, publicador, produtor Core.

Contratos de payload

Routing key Conteúdo construído no código / consumidor
gateway.published, gateway.deleted eventId string hexadecimal, occurredAt, tenantId, gatewayId; OpenAPI Doc gera/remove projeção
gateway.setups_solved eventId, occurredAt, tenantId, data.{operationId,wizardId}; Template Service retoma a operação
template.published, template.deleted eventId, occurredAt, tenantId, gatewayId (ID do GatewayTemplate); OpenAPI Doc distingue a origem pela routing key
application.import.item Mensagem de trabalho publicada diretamente pelo Importer; não é outbox Mongo
gateway, route, authorization, ip_policy Invalidação publicada pelo Cache Invalidator; header tenantId e corpo vazio, distinto de publicação de domínio

Fragmento sanitizado derivado de OutboxEventService.createGatewayEvent: IDs, tenant e data foram substituídos. Este é o payload, não o documento outbox_events completo.

{
  "eventId": "000000000000000000000001",
  "occurredAt": "2026-10-13T00:00:00Z",
  "tenantId": "tenant-exemplo",
  "gatewayId": "000000000000000000000002"
}

Fontes adicionais: produtor Template, Importer messaging, invalidação.

Inbox e projeções

%%{init: {"theme":"base","themeVariables":{"fontFamily":"Arial, sans-serif","fontSize":"16px","lineColor":"#64748b","primaryTextColor":"#172b4d"},"flowchart":{"htmlLabels":true,"useMaxWidth":true,"nodeSpacing":28,"rankSpacing":38,"padding":14,"curve":"linear","subGraphTitleMargin":{"top":12,"bottom":16}},"sequence":{"useMaxWidth":true,"actorMargin":28,"width":145,"height":45,"diagramMarginX":32,"diagramMarginY":12,"messageMargin":24,"wrap":true,"mirrorActors":false},"themeCSS":".edgeLabel, .edgeLabel p {background-color:var(--diagram-surface)!important;color:var(--diagram-text)!important}\n.cluster rect {fill:var(--diagram-group)!important;stroke:var(--diagram-line)!important}\n.cluster-label span, .cluster-label text {color:var(--diagram-text)!important;fill:var(--diagram-text)!important}\n.flowchart-link, .actor-line {stroke:var(--diagram-line)!important}\nmarker path {fill:var(--diagram-line)!important;stroke:var(--diagram-line)!important}\n.actor {fill:var(--diagram-domain)!important;stroke:var(--diagram-domain-border)!important}\ntext.actor, .messageText, .loopText, .labelText {fill:var(--diagram-text)!important}\n.messageLine0, .messageLine1, .loopLine {stroke:var(--diagram-line)!important}\n.labelBox {fill:var(--diagram-surface)!important;stroke:var(--diagram-line)!important}\n.note {fill:var(--diagram-async)!important;stroke:var(--diagram-async-border)!important}\n.noteText {fill:var(--diagram-text)!important}\n.nodeLabel, .nodeLabel p, .nodeLabel span {color:var(--diagram-text)!important}\n.node .nodeLabel, .node .nodeLabel p {font-size:17px;font-weight:600;line-height:1.4;text-align:center}\n.nodeLabel small {display:inline-block;margin-top:4px;font-size:15px;font-weight:400;line-height:1.4;color:var(--diagram-description)!important}\ntext.actor, text.actor tspan {font-size:17px;font-weight:600}\n.node.entry rect, .node.entry polygon, .node.entry path, .node.entry circle {fill:var(--diagram-entry)!important;stroke:var(--diagram-entry-border)!important}\n.node.domain rect, .node.domain polygon, .node.domain path, .node.domain circle {fill:var(--diagram-domain)!important;stroke:var(--diagram-domain-border)!important}\n.node.security rect, .node.security polygon, .node.security path, .node.security circle {fill:var(--diagram-security)!important;stroke:var(--diagram-security-border)!important}\n.node.store rect, .node.store polygon, .node.store path, .node.store circle {fill:var(--diagram-store)!important;stroke:var(--diagram-store-border)!important}\n.node.async rect, .node.async polygon, .node.async path, .node.async circle {fill:var(--diagram-async)!important;stroke:var(--diagram-async-border)!important}\n.node.external rect, .node.external polygon, .node.external path, .node.external circle {fill:var(--diagram-external)!important;stroke:var(--diagram-external-border)!important}\n text.actor tspan, .loopText tspan, .labelText tspan, .messageText tspan, .noteText tspan {fill:var(--diagram-text)!important}\n\n"}}%%
flowchart LR
    handler["OpenAPI Doc<br/><small>Processa evento</small>"]:::async
    doc["gateway_openapi<br/><small>Projeção atualizada</small>"]:::store
    inbox["inbox_processed<br/><small>eventId / processedAt</small>"]:::store
    handler -->|Upsert / delete| doc
    handler -->|Após processamento| inbox
    classDef store stroke-width:1.5px;
    classDef async stroke-width:1.5px;

InboxRepository faz insert; EventHandler registra depois do efeito. Não foi implementada consulta prévia de deduplicação nesse caminho. Portanto o registro não garante exactly-once. Os modelos/campos e ausência de unique/TTL estão em OpenAPI Doc.

Operações Templates usam stages, IDs clonados e lease de upgrade para reentrada/checkpoints, não uma collection Inbox equivalente. Importer mantém status/reprocessamento próprio. Filas e efeitos já documentados na matriz assíncrona não são novas collections.

Fontes: EventHandler, InboxRepository, Operation. Análises de dados concentra as divergências de tipos e garantias.