fix(outbox): provider-aware PerKey guard + OrderedConsumers sample 2PC fix - #38
Merged
Merged
Conversation
…tup gating - PerKey ordering startup guard is now provider-aware: it enforces the dialect's head-of-line capability only when that dialect is the active claim path (the active EF Core provider matches the dialect ProviderName). Providers that use the client-side fallback claim enforce ordering themselves, so a valid single-instance deployment is no longer falsely rejected at startup or forced to set AllowDegradedOrdering. - Gate OutboxCleanupService's destructive DELETEs on IHostApplicationLifetime.ApplicationStarted, mirroring the dispatcher, so cleanup never runs from a process that never became healthy. - Dispatcher drain loop paces with a relative Task.Delay and a burst cap instead of a fixed timer grid, and gates its first poll on ApplicationStarted. - Add an explicit SupportsPerKeyHeadOfLineOrdering capability flag to IOutboxSqlDialect with a flag-plus-SQL consistency check, and declare it on PostgresOutboxSqlDialect. - Add the AllowDegradedOrdering opt-out to downgrade the PerKey fail-fast to a warning. - Document the custom-dialect trust boundary and the PostgreSQL prepared-transactions (2PC) requirement in the outbox README.
…essed records The OrderedConsumers consumer persists a ProcessedRecord through its own DbContext inside the transactional outbox middleware's TransactionScope. That second database connection makes the scope escalate to a two-phase (prepared) commit, which PostgreSQL rejects by default (max_prepared_transactions=0, SqlState 55000 "prepared transactions are disabled"). Every message was retried and dead-lettered, and no ProcessedRecord was written. - AppHost: start PostgreSQL with -c max_prepared_transactions=100. - README: correct the single-sample docker command (it set the wrong password and created no matching database) and add the prepared-transactions flag, so it matches the samples' default connection string. - Document the 2PC requirement at the OrderedConsumers DbContext registration. Verified end to end: the OrderedConsumers smoke test passes, and a standalone run produces the expected per-key records with only the poison head dead-lettered.
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.
Summary
Two related changes in the outbox area, hardening the transactional outbox and fixing the
OrderedConsumers sample so it actually delivers end to end.
1. Outbox hardening (
fix(outbox): …)head-of-line capability only when that dialect is the active claim path (the active EF Core
provider matches the dialect
ProviderName). When another provider is active, the store uses theclient-side fallback claim, which enforces per-key head-of-line ordering itself — so a valid
single-instance deployment is no longer falsely rejected at startup or forced to set
AllowDegradedOrdering.OutboxCleanupService's destructiveDELETEs are gated onIHostApplicationLifetime.ApplicationStarted, mirroring the dispatcher, so cleanup never runs froma process that never became healthy.
Task.Delayand a burst cap (insteadof a fixed timer grid) and gates its first poll on
ApplicationStarted.IOutboxSqlDialect.SupportsPerKeyHeadOfLineOrderingis an explicitdeclaration (with a flag-plus-SQL consistency check);
PostgresOutboxSqlDialectdeclares it.AllowDegradedOrderingopt-out downgrades the PerKey fail-fast to a warning.requirement in the outbox README.
2. OrderedConsumers sample fix (
fix(samples): …)Root cause: the OrderedConsumers consumer persists a
ProcessedRecordthrough its ownDbContextinside the transactional outbox middleware'sTransactionScope. That second databaseconnection makes the scope escalate to a two-phase (prepared) commit, which PostgreSQL rejects by
default (
max_prepared_transactions=0,SqlState 55000). Every message was retried anddead-lettered, and no record was written — pre-existing, independent of the hardening above.
Fix:
-c max_prepared_transactions=100.database) and gains the prepared-transactions flag, matching the samples' default connection string.
DbContextregistration.Verification
ConsumerRoutingKeys). Before the fix OrderedConsumers dead-lettered all 20 messages; after it,
the SAC endpoint records 15 healthy + 4 poison-resume per run and only the poison head is parked.