Skip to content

fix(outbox): discard clears incoming envelope after rolled-back outbox#3327

Merged
jeremydmiller merged 2 commits into
JasperFx:mainfrom
robertdusek:fix/discard-after-failed-outbox-commit
Jul 9, 2026
Merged

fix(outbox): discard clears incoming envelope after rolled-back outbox#3327
jeremydmiller merged 2 commits into
JasperFx:mainfrom
robertdusek:fix/discard-after-failed-outbox-commit

Conversation

@robertdusek

Copy link
Copy Markdown
Contributor

Problem

When a durable-local-queue handler enlists the Marten (or Polecat) outbox and SaveChangesAsync rolls back — e.g. a document insert hits a unique constraint — an .OnException<T>().Discard() policy fails to remove the incoming envelope. It stays status='Incoming' in wolverine_incoming_envelopes and is reprocessed on every InboxStaleTime reclaim / restart.

Root cause

FlushOutgoingMessagesOnCommit.BeforeSaveChangesAsync flips Envelope.Status = Handled in memory before the commit, alongside queuing the mark-handled UPDATE into the same batch. On rollback the DB row stays Incoming, but the in-memory flag is not reverted. The discard path then hits the _markAsHandled optimization in DurableReceiver (if (e.Status == Handled) return;), which trusts the stale flag and skips the real UPDATE — so the row is never marked handled.

Fix

Defer the in-memory Status = Handled flip to AfterCommitAsync, which only runs once the commit is durable. Polecat's ITransactionParticipant path has no after-commit hook, so it simply stops setting the premature flag — the idempotent _markAsHandled UPDATE covers the success path.

Testing

  • New red-green regression tests for Marten and Polecat (both fail before the fix with Incoming == 1, pass after; verified against the reverted code on both stores).

Robert Dusek and others added 2 commits July 6, 2026 14:39
…x commit

The Marten and Polecat FlushOutgoingMessagesOnCommit listeners flipped
Envelope.Status to Handled in BeforeSaveChangesAsync, before the commit. On
rollback (e.g. a duplicate document insert violating the unique PK) the DB row
stayed 'Incoming' but the stale in-memory flag made DurableReceiver._markAsHandled
skip the real UPDATE, so a .Discard() policy left the envelope stuck as 'Incoming'
and it was reprocessed on every InboxStaleTime reclaim / restart.

Defer the in-memory status flip to AfterCommitAsync, which only runs once the
commit is durable. Polecat's ITransactionParticipant path has no after-commit hook,
so it simply no longer sets the premature flag (the idempotent _markAsHandled
UPDATE covers the success path).

Adds red-green regression tests for both Marten and Polecat.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jeremydmiller
jeremydmiller merged commit dd3ed9b into JasperFx:main Jul 9, 2026
25 checks passed
This was referenced Jul 9, 2026
This was referenced Jul 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants