Skip to content

feat(outbox): single-commit connection sharing for consumer writes - #39

Merged
asawicki merged 2 commits into
mainfrom
asawicki/feat/outbox-single-commit-connection-sharing
Jun 28, 2026
Merged

feat(outbox): single-commit connection sharing for consumer writes#39
asawicki merged 2 commits into
mainfrom
asawicki/feat/outbox-single-commit-connection-sharing

Conversation

@asawicki

Copy link
Copy Markdown
Collaborator

…ommit writes

Expose the database connection the transactional outbox middleware pins for the
in-flight message via a new public IOutboxConnectionAccessor. A consumer DbContext
can use that same connection so its business write commits single-phase with the
buffered outbox messages and the inbox processed marker — one physical connection,
one enlistment — instead of opening a second connection and forcing
System.Transactions to escalate to a two-phase (prepared) commit.

A single-phase commit is faster (no PREPARE/COMMIT PREPARED round-trip and fsync
per message) and removes the PostgreSQL max_prepared_transactions requirement.
Verified end-to-end against PostgreSQL with max_prepared_transactions=0: the
OrderedConsumers sample processes every message with zero "55000: prepared
transactions are disabled" errors and creates no prepared transactions, while
per-key ordering and inbox deduplication remain intact.

- TransactionalOutboxMiddleware publishes its pinned DbConnection on an AsyncLocal
  for the duration of the consume operation and clears it on completion, so a
  consumer resolved in its own DI scope (same async flow) can share it.
- OutboxConnectionAccessor (internal) reads it; registered as a singleton by
  AddBareWireOutbox.
- OrderedConsumers sample shares the outbox connection instead of opening its own.
- EF package README leads with the single-commit pattern and a working example;
  2PC is documented as the fallback. samples/README re-points the 2PC note to
  TransactionalOutbox and InboxDeduplication, which still rely on it.
@asawicki
asawicki changed the base branch from main to asawicki/fix/outbox-hardening-and-sample-2pc June 28, 2026 15:18
… InboxDeduplication; drop 2PC

Apply the single-commit connection-sharing pattern to the remaining outbox samples whose consumers
persist business state during consume: TransactionalOutbox (TransferConsumer updates the Transfer row)
and InboxDeduplication (Email/Audit consumers append a NotificationLog). Each now binds its DbContext to
the connection the outbox middleware pinned for the in-flight message (IOutboxConnectionAccessor), so the
business write commits single-phase with the outbox/inbox writes instead of opening a second connection
and escalating to a two-phase (prepared) commit.

With every outbox sample sharing the connection, no sample needs prepared transactions: the Aspire
AppHost no longer sets max_prepared_transactions and the standalone-run instructions drop the
-c max_prepared_transactions=100 flag. PostgreSQL runs with its default (2PC disabled).

Verified end-to-end against PostgreSQL with max_prepared_transactions=0:
- TransactionalOutbox: 3 transfers all reach Completed, zero "55000" errors, no prepared transactions.
- InboxDeduplication: 2 payments produce 4 NotificationLogs (Audit=2, Email=2), zero "55000" errors.
Base automatically changed from asawicki/fix/outbox-hardening-and-sample-2pc to main June 28, 2026 20:51
@asawicki
asawicki merged commit 3490f65 into main Jun 28, 2026
@asawicki
asawicki deleted the asawicki/feat/outbox-single-commit-connection-sharing branch June 28, 2026 20:51
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.

1 participant