Repository navigation
GH-5108 Logical deduplication claim rides the EF Core transaction - #5111
Merged
Merged
Conversation
On an EF Core chain in Eager mode the deduplication claim was committed on its own connection before BeginTransactionAsync and only released when the chain threw. A process killed between the claim and the EF Core commit left the claim behind and the work undone, so inbox recovery discarded the replay as a duplicate and the message was lost. EFCorePersistenceFrameProvider now implements TryBuildTransactionalDeduplication for Eager mode with a single, non-tenanted DbContext against a relational message database: the claim is an INSERT on the DbContext's connection and transaction, written after the transaction begins and before the handler runs, with no release frame. A concurrent copy blocks on the uncommitted claim and is refused once it commits. Lightweight mode, database-per-tenant and EF Core sagas keep claim-and-release. New ITransactionalDeduplicationStore (Wolverine.RDBMS), implemented by RdbmsDeduplicationStore; PostgreSQL appends "on conflict do nothing" so a duplicate does not abort the caller's transaction. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DCggXgxnG3EBXoR9rrq37U
This was referenced Oct 23, 2026
This was referenced Oct 29, 2026
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.
Closes #5108
Problem
#4505 / #4568 made the logical deduplication claim ride the Marten (and later Polecat/Fisher) transaction. EF Core chains still used claim-and-release:
IMessageDeduplicator.TryClaimAsynccommitted the claim on its own connection beforeBeginTransactionAsync, and the claim was given back only when the chain threw. A process killed between the claim and the EF Core commit left the claim committed and the work undone; inbox recovery then replayed the envelope with the sameDeduplicationIdand it was discarded as a duplicate. The message was lost, and the log said it was deduplicated.Reproduction
src/Persistence/EfCoreTests/deduplication_rides_the_ef_core_transaction_5108.cs, run on PostgreSQL and SQL Server. A process kill can't be staged deterministically, but what decides whether a kill loses the message can: the handler reads the claim table from a separate connection while its transaction is still open (READPAST on SQL Server). Anything visible there survives a kill.Before the fix, 4 of 14 fail on both providers:
After the fix, all 15 pass, including the new Lightweight fallback test.
Fix
EFCorePersistenceFrameProvidernow implementsIPersistenceFrameProvider.TryBuildTransactionalDeduplication, as the issue proposes:EnrollDbContextInTransactionandCommitEfCoreEnvelopeTransaction(Eager mode, a single non-tenanted DbContext) and the store for that DbContext (main, or its ancillary store) is anIMessageDatabasewhose deduplication store implements the newITransactionalDeduplicationStore. This rests on the same assumption the EF Core outbox already makes when it writes envelope rows through the DbContext connection.ClaimDeduplicationIdInEfCoreTransactionFramecallsEfCoreEnvelopeTransaction.TryClaimDeduplicationIdAsync. That runs the claim INSERT on the DbContext's connection and current transaction. Because the frame depends on theEfCoreEnvelopeTransactionvariable, it lands inside the enroll frame's try block, afterBeginTransactionAsyncand before the handler.CommitRaceWrapperis needed.Supporting changes in
Wolverine.RDBMS:ITransactionalDeduplicationStore, implemented byRdbmsDeduplicationStore(PostgreSQL, SQL Server, MySQL, SQLite).protected virtual string? DeduplicationClaimConflictClauseonMessageDatabase. PostgreSQL overrides it withon conflict do nothing, so a duplicate is reported as zero rows instead of an error that would abort the caller's transaction. Other engines keep the existing unique-violation classification; a duplicate-key error leaves their transaction usable.There are no breaking API changes; everything is additive.
Configurations
TransactionMiddlewareMode.LightweightMultiTenantedMessageStoreITransactionalDeduplicationStoreTests run locally
EfCoreTests, the new 5108 classes: 15/15 on PostgreSQL and SQL Server. FullEfCoreTests: 432 tests. The first run's failures were RabbitMQ not running locally. After starting RabbitMQ,EfCoreTests.Bugswas 62/63; the one failure,Bug_2075, has no dedup or EF involvement and passes when run alone.DeduplicationStoreCompliance, with 6 new in-transaction facts (rollback leaves no claim, commit refuses the next claim, a refused claim leaves the transaction committable) under both modes: PostgreSQL 38/38, SQL Server 38/38, SQLite 38/38. MySQL was not run locally.Wolverine.Http.Testsdedup filter: 81/81.CoreTestsdedup: 36/36.MartenTestsdedup: 16/16.dotnet build wolverine.slnx -c Release -f net9.0: clean.Docs: one paragraph in
docs/guide/durability/idempotency.mdsaying when EF Core chains ride the transaction.🤖 Generated with Claude Code
https://claude.ai/code/session_01DCggXgxnG3EBXoR9rrq37U