Skip to content

[Mocha] Fix Postgres outbox reordering messages from one transaction - #10699

Merged
PascalSenn merged 2 commits into
ChilliCream:mainfrom
alisan3:ali/mocha-postgres-outbox-send-order
Oct 28, 2026
Merged

PascalSenn merged 2 commits into
ChilliCream:mainfrom
alisan3:ali/mocha-postgres-outbox-send-order

Conversation

@alisan3

@alisan3 alisan3 commented Oct 26, 2026

Copy link
Copy Markdown
Contributor

With the Postgres outbox, messages published inside one database transaction were sent in reverse order. A handler that publishes two events for the same stream in one transaction delivered the second before the first.

The insert stamps created_at with NOW(), which is the transaction start, so every row of one transaction gets the same value. The claim picked the next row with ORDER BY "created_at" ... LIMIT 1 and no tie-breaker. The planner serves that through a backward scan of the descending created_at index, which returns tied rows newest first. The row id could not break the tie: Guid.CreateVersion7() is random within a millisecond, and the net8 path used Guid.NewGuid().

This change:

  • generates outbox ids with a new internal OutboxMessageIdGenerator. It produces UUIDv7 values that strictly increase in uuid byte order within the process, using a counter after the millisecond timestamp, and is the same on every target framework.
  • orders the claim by created_at, then id.

There is no schema, model or migration change. The existing created_at index still drives the claim: Postgres uses an incremental sort over it and only sorts the rows that share the oldest created_at.

New tests:

  • Outbox_Should_DeliverMessagesInPublishOrder_When_PublishedInOneTransaction publishes 50 messages in one transaction and checks the delivery order. Before the fix it failed on every run, with the messages arriving from 49 down to 0.
  • Outbox_Should_DeliverMessagesInPublishOrder_When_EachPublishedInOwnTransaction is a regression guard for one message per transaction.
  • OutboxMessageIdGeneratorTests checks that ids generated within the same millisecond strictly increase, and that version and variant bits are set.

Outbox rows written in one transaction all get created_at = NOW(), the
transaction start, and the claim ordered by created_at alone. Through a
backward scan of the descending created_at index, tied rows were claimed
newest first, so messages from one transaction were sent in reverse order.

Generate outbox ids that strictly increase within the process (UUIDv7 with
a counter after the millisecond timestamp) and order the claim by
created_at, then id. No schema or migration change.
@PascalSenn
PascalSenn merged commit d50b3d2 into ChilliCream:main Oct 28, 2026
3 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants