Repository navigation
✨ feat(outbox)!: carry context, retry with backoff and claim under lock - #3
Merged
Merged
Conversation
A relay delivers out of band, so anything that only exists during the originating request — the acting user, its address, a tenant — is gone by the time a handler runs. `Enqueue` now takes a `context` that travels with the message; `CorrelationId` is a single opaque string, enough to stitch traces together but not to answer "who did this". A failed delivery used to be terminal: one exception and the message was `Failed` for good. `MarkFailed` now takes the retry policy and decides — while attempts remain the message stays pending and is rescheduled with backoff, and only once they are exhausted is it dead-lettered. Passing the policy in keeps the decision where the failure is observed, so a relay can be tuned without reconfiguring the store. `GetPending` used to hand the same batch to every relay that asked. Where the store can claim rows it now does so atomically and leases them — PostgreSQL through `FOR UPDATE SKIP LOCKED`, Redis through a script, Mongo through `findOneAndUpdate`; where it cannot, the implementation says it is at-least-once and expects a single relay. Also fixes `MarkProcessed` on PostgreSQL writing `Processing` where it meant `Processed`: `CleanupProcessed` matched the same wrong state, so the two bugs hid each other and processed messages were never swept. Outbox behaviour now has tests on every store it ships for: PostgreSQL, MongoDB, Redis, EF Core and in-memory. BREAKING CHANGE: `IOutboxRepository.MarkFailed` takes `maxAttempts` and `backoff`; `IOutbox.Enqueue` takes an optional `context` ahead of the cancellation token; `IOutboxMessage` gains `NextAttemptAt` and `Context`. Custom implementations of either interface must be updated. Existing data survives: the PostgreSQL table picks up its two columns through `ADD COLUMN IF NOT EXISTS`, while EF Core deployments need a migration for the new `NextAttemptAt`/`Context` properties and the widened outbox index.
|
🎉 This PR is included in version 3.0.0 🎉 The release is available on:
Your semantic-release bot 📦🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fecha as três lacunas do outbox: a mensagem não carregava contexto, uma falha era terminal, e
GetPendingentregava o mesmo lote a todos os relays.Contrato (breaking)
IOutboxMessageganhaNextAttemptAteContextIOutbox.Enqueuerecebecontextantes doCancellationTokenIOutboxRepository.MarkFailedrecebemaxAttemptsebackoff— a política de retry vai como parâmetro, então quem decide é o relay, onde a falha é observada, sem reconfigurar o storeComportamento
Uma falha com tentativas restantes deixa a mensagem
Pendinge reagenda com backoff escalado pelo número de tentativas; só ao esgotar viraFailed— dead letter preservada para inspeção.GetPendingignora quem ainda está em backoff.Onde o store sabe travar, o lote passa a ser reivindicado atomicamente e com lease de 1 min: PostgreSQL via
FOR UPDATE SKIP LOCKEDdentro de um CTE (comORDER BY created_atno select externo, porqueRETURNINGsozinho não garante ordem), Redis via script Lua, Mongo viafindOneAndUpdate. Onde não sabe — EF Core eNativeOutboxRepository, já queIAsyncRepositorynão expressa claim limitada sob lock — a implementação documenta que é at-least-once e espera um único relay.Correção de bug
MarkProcessedno PostgreSQL gravavaProcessing(1) onde queria dizerProcessed(2).CleanupProcessedfiltrava pelo mesmo valor errado, então os dois bugs se escondiam e mensagens processadas nunca eram varridas.Testes
93 → 119. O outbox só tinha cobertura no Redis; agora tem em cinco stores — PostgreSQL, MongoDB, Redis, EF Core e in-memory — cobrindo contexto no round-trip, ordem/batch, claim que não entrega a mesma mensagem a um segundo relay, backoff que segura, backoff vencido que reoferece, e dead letter.
Migração
Dados existentes sobrevivem: a tabela PostgreSQL ganha as duas colunas via
ADD COLUMN IF NOT EXISTS. Deployments EF Core precisam de migration paraNextAttemptAt/Contexte para o índice ampliado(State, NextAttemptAt, CreatedAt). Implementações próprias deIOutboxRepositoryouIOutboxprecisam ser atualizadas.