R-004 — Despacho de eventos do outbox — 3 tentativas, sem distinção de erro
TLDR:
OutboxEventsai dependingparasentnuma entrega bem-sucedida, ou parafaileddepois da 3ª falha — qualquer falha soma 1 tentativa, sem diferenciar erro retryable de definitivo. O reenvio manual (ActiveAdmin) zeraattemptse volta o evento parapending.
Given / When / Then
Dado um OutboxEvent pending,
Quando Outbox::DispatchEventsJob despacha e recebe uma resposta de sucesso (HTTP 2xx),
Então o evento vira sent, com sent_at preenchido.
Dado um OutboxEvent pending com attempts menor que 2,
Quando o despacho falha (HTTP não-2xx, exceção ou timeout),
Então attempts soma 1, last_error é preenchido, e o evento continua pending — o job se reenfileira e tenta de novo em 5 segundos.
Dado um OutboxEvent pending com attempts igual a 2,
Quando o despacho falha novamente (a 3ª tentativa),
Então o evento vira failed — não há mais tentativa automática.
Dado um OutboxEvent failed,
Quando um operador aciona “Reenviar” no ActiveAdmin,
Então o evento volta para pending com attempts: 0 e last_error: nil — as 3 tentativas recomeçam do zero.
Tabela de decisão
| Situação | attempts antes |
Resultado do despacho | status depois |
|---|---|---|---|
| Sucesso (2xx) | qualquer | sucesso | sent |
| Falha (4xx, 5xx, exceção, timeout) | 0 ou 1 | falha | pending, attempts + 1 |
| Falha (4xx, 5xx, exceção, timeout) | 2 | falha | failed, attempts: 3 |
| Reenvio manual | 3, failed |
— | pending, attempts: 0 |
Restrições
- Não há distinção entre erro retryable (
429/5xx/timeout) e definitivo (4xx): as 3 tentativas valem para qualquer falha. Um erro de payload/mapeamento (ex.:company_not_found) também consome as 3 tentativas antes de ficar visível comofailed. - O espaçamento entre tentativas é o
retry_ondo próprio job (5 segundos), não um campo na tabela (next_attempt_at). Cada evento tem o seu job: não há varredura periódica. - Sem alerta automático (Sentry, e-mail) na falha definitiva — a visibilidade é só pelo recurso
outbox_eventsno ActiveAdmin (namespace: :system_manager). - A tabela e o job são genéricos: outros tipos de evento reutilizam o mecanismo registrando um despachante em
Outbox::DispatchRegistry::DISPATCHERS, sem migração nova — já confirmado por três consumidores reais além do primeiro (accounts.purchase_create,accounts.installment_paideaccounts.installment_overdue), sem qualquer mudança na tabela, no model ou no job. - Um despachante atende mais de um
event_name: os três eventos doaccountsapontam paraOutbox::Dispatchers::AccountsEvents, porque o destino é o mesmo endpoint (POST /api/v1/events) com os mesmos cabeçalhos — o que distingue um evento do outro é oevent_typedentro do payload, não a classe que despacha.
Teste vinculado
spec/models/outbox_event_spec.rb (#register_failure!, #mark_sent!, #retry!) e spec/jobs/outbox/dispatch_events_job_spec.rb (transições via o job, incluindo a 3ª falha e o evento sem despachante registrado).
Referências
- ../../specs/20260914102226_repayment_sync_outbox_pattern.md — decisões e escopo
- ../../reference/payments/new_checkout_repayment_endpoint.md — contrato do primeiro evento despachado por este mecanismo
- ../../reference/payments/accounts_purchase_created_event.md — contrato do segundo evento,
accounts.purchase_create - ../../reference/payments/accounts_installment_paid_event.md — contrato do terceiro evento,
accounts.installment_paid - ../../reference/payments/accounts_installment_overdue_event.md — contrato do quarto evento,
accounts.installment_overdue