Pular para o conteúdo
Vamos conversar

ATLAS / INTEGRAÇÕES E EVENTOS / FABRÍCIO CASTRO

Gravar não é publicar.
Publicar não é confirmar.

Quando o negócio precisa gravar uma mudança e comunicar um evento, duas operações isoladas criam uma janela de falha. A Transactional Outbox grava a intenção de publicação junto do estado de domínio em uma transação local; um relay separado pode enviar depois. Reentregas são possíveis e precisam ser tratadas, nunca ocultadas por promessas de exactly-once.

01 / UMA MUDANÇA E DOIS SISTEMAS

O commit pode acontecer. O envio, não.

Considere uma solicitação de compras fictícia. O serviço registra o envio e depois tenta notificar uma integração externa. Se o processo morrer entre essas duas ações, o banco contém o novo estado, mas o destinatário pode nunca receber a informação. Inverter a ordem também falha: o destinatário pode receber um evento cujo estado de origem foi revertido.

Fluxo de Outbox: transação PostgreSQL confirma negócio e mensagem juntos; relay com lease publica ao consumidor; falha entre publicação e ACK causa possível reenvio; chave idempotente impede repetir o efeito.
Diagrama original, com janelas de falha representadas explicitamente. Persistência atômica é local; o envio para outra aplicação não participa da mesma transação.
DUAL WRITE / SEM OUTBOX

Commit bem-sucedido, mensagem perdida.

Um comando SQL seguido de um POST HTTP deixa uma janela entre as duas ações. Retries ad hoc podem duplicar efeitos, e transação SQL não faz rollback de um serviço remoto.

Critério: o resultado deve ser recuperável sem fingir que o banco controla o destinatário.

OUTBOX / INTENÇÃO DURÁVEL

A fila nasce junto da mudança.

O dado de negócio e o registro de evento são confirmados na mesma transação. Uma rotina posterior reivindica mensagens pendentes e registra seu progresso. Não há necessidade de um broker distribuído para compreender o padrão.

Limite: publicar uma mensagem e confirmar seu envio continuam ações separadas.

02 / TRANSAÇÃO LOCAL / SQL REAL

Negócio e intenção de evento confirmam juntos.

O laboratório representa uma transição draft → submitted. O UPDATE verifica tenant, identificador, estado e versão. O resultado alimenta o INSERT na Outbox dentro da mesma transação SQL. Se o evento não puder ser inserido, o banco também desfaz a alteração de negócio.

BEGIN;
-- Uma transição do negócio atualiza a solicitação e gera evento
-- na mesma transação SQL; rowCount=0 significa estado obsoleto.
WITH changed AS (
  UPDATE purchase_requests
  SET status = 'submitted', version = version + 1
  WHERE tenant_id = $1 AND request_id = $2
    AND version = $3 AND status = 'draft'
  RETURNING tenant_id, request_id, version
)
INSERT INTO outbox_events
  (tenant_id, event_id, request_id, aggregate_version,
   event_type, payload)
SELECT tenant_id, $4::uuid, request_id, version,
       'PurchaseSubmitted', $5::jsonb
FROM changed;
COMMIT;

Exemplo didático resumido. O SQL físico usa identificador de schema aleatório validado e parâmetros do driver; não é uma migração do banco ERP.

PROVA / COMMIT

Uma alteração, um registro para publicar.

O teste consulta a solicitação e a tabela de Outbox depois do commit. A chave única por organização, solicitação, versão e tipo impede registrar duas mensagens equivalentes para a mesma transição.

PROVA / ROLLBACK

Erro na mensagem desfaz também o estado.

O ensaio injeta um payload incompatível com a constraint JSON do evento. A alteração de negócio deve permanecer em rascunho e nenhuma mensagem deve ficar disponível.

03 / VÁRIOS TRABALHADORES / FILA SQL

Um worker não deve disputar a mesma linha bloqueada.

O relay utiliza FOR UPDATE SKIP LOCKED para escolher uma mensagem elegível por tenant e reivindicá-la em uma instrução atômica. Isso reduz contenção entre consumidores concorrentes de uma fila. O PostgreSQL alerta que esse mecanismo apresenta uma visão inconsistente dos dados e não serve como critério geral de ordenação ou justiça entre mensagens.

WITH candidate AS (
  SELECT tenant_id, event_id
  FROM outbox_events
  WHERE tenant_id = $4 AND attempts < 3
    AND (
      (status = 'pending' AND available_at <= clock_timestamp())
      OR (status = 'leased' AND lease_until <= clock_timestamp())
    )
  ORDER BY created_at, tenant_id, event_id
  FOR UPDATE SKIP LOCKED
  LIMIT 1
)
UPDATE outbox_events AS o
SET status = 'leased', attempts = attempts + 1,
    leased_by = $1, lease_token = $2::uuid,
    lease_until = clock_timestamp() + ($3 * interval '1 second')
FROM candidate AS c
WHERE o.tenant_id = c.tenant_id AND o.event_id = c.event_id
RETURNING o.tenant_id, o.event_id, o.lease_token, o.payload;

A query real retorna também tentativas e token do lease. O teste usa dois clientes: enquanto um mantém lock sobre uma mensagem, outro worker deve reivindicar uma mensagem diferente, sem duplicar a mesma reivindicação.

04 / RECONHECIMENTO E RECUPERAÇÃO

O token de posse impede um ACK atrasado de vencer.

Cada reivindicação gera um identificador de lease e prazo de validade. Após entrega confirmada pelo publisher, o worker tenta marcar a mensagem como sent usando tenant, ID, status, token atual e prazo ainda válido. Um worker antigo com lease expirado não pode apagar o progresso da nova tentativa.

  1. 01
    Claim

    Reserva uma mensagem elegível com token novo, número de tentativa e prazo. A seleção é isolada por organização; não há autorização de worker implementada no laboratório.

  2. 02
    Publish

    Envia o mesmo ID de evento como chave de idempotência ao destinatário. A operação externa não pode compartilhar transação PostgreSQL local sem protocolo distribuído próprio.

  3. 03
    ACK ou incerteza

    Um ACK confirmado marca sent. Se a conexão de banco falhar depois de publicar, o estado é ack_unknown; o relay não deve inventar confirmação nem disparar retry imediato às cegas.

  4. 04
    Lease expirado

    A mensagem pode voltar a ser reivindicada. O teste impede ACK com token antigo, tenant incorreto ou confirmação repetida.

05 / BACKOFF E RECONCILIAÇÃO

Tentar novamente não é repetir para sempre.

O exemplo usa no máximo três tentativas e atrasos limitados. Falhas de publicação liberam a mensagem para novo processamento; ao atingir o limite, ela passa para dead, com código técnico não sensível. Um processo de reconciliação deve examinar a causa antes de autorizar eventual reprocessamento manual.

const result = await relayOnce(
  verifiedTenantId,
  workerId,
  postgresOutboxAdapter,
  idempotentPublisher,
);
// "sent" = ACK confirmado no banco local;
// "ack_unknown" = efeito externo pode ter acontecido;
// "retry_scheduled" = publicação falhou e nova tentativa foi agendada.
// Um broker real não foi criado neste capítulo.

A função relayOnce implementa um único ciclo. Ela não inicia daemon, não configura credenciais e não envia mensagens reais; seus ports devem receber adaptadores confiáveis antes de qualquer aplicação operacional.

EXPIRAÇÃO NO ÚLTIMO ENVIO

Uma mensagem esgotada não pode ficar travada.

Se o processo morrer com o terceiro lease, uma operação separada move a mensagem expirada para dead. Isso torna o problema visível para investigação, sem descartá-lo silenciosamente.

TRADE-OFF / ENTREGA

Há uma política explícita de tentativas.

Com teto de três tentativas, o laboratório não promete entrega eventual ilimitada. Recuperação exige monitoramento, responsável, autorização e registro de evidência.

06 / AT-LEAST-ONCE / CONSUMIDOR

O mesmo evento pode chegar duas vezes. O efeito não precisa acontecer duas vezes.

O caso adversarial aplica primeiro o efeito no consumidor, mas simula uma falha antes de registrar o ACK na Outbox. Depois que o lease expira, a mesma mensagem é reivindicada outra vez. A tabela de mensagens processadas usa chave composta por consumidor, tenant e evento; o efeito só é aplicado se o primeiro registro de processamento tiver sido inserido dentro da mesma transação do consumidor.

CONSUMER RECEIPT

Gravar o identificador faz parte da decisão.

O teste usa INSERT … ON CONFLICT DO NOTHING RETURNING. Se já existe o evento para aquele consumidor, ele reconhece a duplicata e não repete o efeito. O registro de deduplicação e o efeito ficam na mesma transação local.

LIMITE / SISTEMA EXTERNO

Uma tabela de teste não é um ERP real.

O efeito demonstrativo é gravado em outra tabela do mesmo PostgreSQL descartável. Isso comprova a lógica local quando executada, mas não simula HTTP externo, publicação Kafka, confirmação perdida na rede, semântica do ERP ou proteção exactly-once entre serviços.

SEMÂNTICA / ORDERING

SKIP LOCKED não garante FIFO global.

Uma mensagem bloqueada pode ser ultrapassada por outra. Quando a ordem dos eventos é requisito do domínio, serão necessários contratos de versão por agregado, particionamento, consumidor sequencial ou outra estratégia validada.

07 / TESTE REAL, BANCOS DESCARTÁVEIS

A prova precisa sair do PostgreSQL, não do comentário.

A suíte de integração cria um schema aleatório apenas após validar nome, host e confirmação do banco descartável. Ela executa testes em PostgreSQL real e remove o schema ao terminar. Os resultados devem ser registrados com SHA, versão do banco e logs, não inferidos pela presença dos arquivos.

# Com Docker local, Node e dependências já instaladas:
bash scripts/atlas/postgresql/outbox/run-local-docker.sh

# Com PostgreSQL local explicitamente descartável:
ATLAS_PG_TEST_URL="postgresql://atlas_lab:...@127.0.0.1:5432/fc_atlas_test_local" \
ATLAS_PG_TEST_CONFIRM=I_UNDERSTAND_DISPOSABLE_DATABASE \
pnpm --filter fanpage-frontend test:outbox:postgres

# Sem banco, apenas contratos e unidade TypeScript:
pnpm --filter fanpage-frontend test:outbox:contract
pnpm --filter fanpage-frontend test:outbox:unit

O runner Docker publica uma porta aleatória somente em 127.0.0.1, usa credenciais temporárias e não consulta DATABASE_URL. Sem Docker/pg instalado, o teste real continua NOT_RUN.

MATRIZ DE TESTES / O QUE PRECISA PASSAR

Falhas que reproduzem problemas operacionais.

O laboratório está desenhado para verificar commit/rollback conjunto, dois workers com SKIP LOCKED, falha após o efeito e antes do ACK, lease token expirado, tentativas esgotadas e atomicidade do consumidor.

WRITEMutação e evento no mesmo COMMIT.

ROLLBACKEvento inválido desfaz a alteração de domínio.

CLAIMDois workers não reivindicam a mesma mensagem bloqueada.

REPLAYNova entrega não repete o efeito local já registrado.

ACKToken obsoleto não confirma uma mensagem.

RETRYFalhas repetidas levam a análise explícita em dead.

08 / FONTES E LIMITES

O padrão reduz uma janela de perda. Não elimina todas as falhas.

Uma solução de produção precisa tratar autorização do worker, secrets, schema migration, monitoramento de backlog, retries, dead-letter, privacidade, idempotência do consumidor externo, versões do evento e ordenação de mensagens por agregado quando exigida. O cenário permanece fictício e independente dos ambientes de ERP do usuário.

REFERÊNCIA / PADRÃO

Chris Richardson — Transactional Outbox.

Descrição clássica do problema de dual-write, da gravação do evento na transação local, do relay e da possibilidade de duplicação após falhas.

Consultar padrão

REFERÊNCIA / CONSUMIDOR

Idempotent Consumer.

Explica a deduplicação com chave de mensagem processada e efeito de negócio dentro da mesma transação do consumidor.

Consultar padrão

REFERÊNCIA / SQL

PostgreSQL — SELECT locking.

Documenta SKIP LOCKED como alternativa útil para tabelas semelhantes a filas e esclarece que pode produzir uma visão inconsistente.

Consultar PostgreSQL 17

CONEXÃO / LABORATÓRIO ANTERIOR

Concorrência e versionamento continuam válidos.

O estudo de PostgreSQL aprofunda versões esperadas, isolamento por tenant e transações concorrentes. A Outbox adiciona intenção durável de comunicação, sem substituí-los.

Revisar concorrência PostgreSQL

Código didático em branch: a presença de SQL, testes e exemplo de relay não comprova execução de banco, broker real, envio ao ERP, exactly-once, homologação visual, merge ou deploy em produção.

Continue explorando.Engenharia de software ↗
Vamos conversar?

Explore o portfólio

26 páginas encontradas · Esc para fechar