You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Recreate for Polecat what #4568 did for Marten: write the logical deduplication claim
inside the Polecat session's own transaction instead of on a separate connection, so a failed or refused
chain never claimed at all and the compensating release disappears.
#4564 is the shared design brief for both stores — read it for what core already provides and why this is
not a copy of the Marten implementation. Fisher is tracked separately.
The one structural difference from Marten
Marten works because Wolverine's message store is built from Marten's own NpgsqlDataSource
(WolverineOptionsMartenExtensions.BuildSinglePostgresqlMessageStore), so QueueOperation on the session
writes on the same connection the handler commits through.
Polecat's is not. WolverineOptionsPolecatExtensions.BuildSqlServerMessageStore passes only the connection string, and SqlServerMessageStore's base call then does:
Same SQL Server database, different DbDataSource and connection pool. So the claim has to be written
on the connection Polecat's session is holding, which means ITransactionParticipant:
// the pattern Wolverine.Polecat/Persistence/Operations/PolecatStorageExtensions.cs already usessession.AddTransactionParticipant(newClaimDeduplicationIdParticipant(store.DeduplicationFullName,id,expires));
ITransactionParticipant.BeforeCommitAsync(SqlConnection, SqlTransaction, CancellationToken) is handed the
live connection and transaction, and runs after Polecat's own SQL but before the commit.
Note also that Polecat's IDocumentOperations.QueueSqlCommand has only the (string, params object[]) overload — no char placeholder variant, unlike Marten and Fisher
(PolecatOps.Documents.cs says so in its file header). A typed participant avoids the question.
The optimistic check
Marten enlists an IQueryHandler<bool> via IBatchedQuery.AddItem, so the "has this been claimed?" query
shares the round trip with a [WriteAggregate]FetchForWriting. PolecatBatchingPolicy and Polecat.Batching.IBatchedQuery have the same shape and the policy is registered
(PolecatIntegration.cs), but whether Polecat.Batching.IBatchedQuery exposes an AddItem<T>
equivalent is unverified — check that first, because if it does not, this needs an upstream Polecat ask
before it can be done in one round trip.
Also: PolecatBatchingPolicy.SortThroughFrames is unconditional — it has no equivalent of Marten's IsBatchable filter. That filter is what lets a chain with an optional deduplication id opt out of the
batch, which is load-bearing: batch enlistment is unconditional code, and unkeyed traffic on a Required = false chain is supposed to cost no round trip at all.
Schema
MessageDatabase.DeduplicationFullName (added in #4568) renders it correctly. The Polecat message-store
schema and the document schema are the same by default (WolverineOptionsPolecatExtensions, default "wolverine"), but not in the multi-tenant path, and the ancillary default
(AncillaryWolverineOptionsPolecatExtensions) does not inherit store.Options.DatabaseSchemaName.
Multi-database tenancy falls back
Polecat has the same split as Marten — store.Options.Tenancy.Cardinality != DatabaseCardinality.Single
builds a separate main store from PolecatIntegration.MainConnectionString. That chain must fall through
to claim-and-release, exactly as the Marten guard does.
The guard is not CanApply
Repeating this from #4564 because getting it wrong is silent and total. CanApply answers "could this
provider own the chain's transaction?", which is true for any chain that merely takes an IDocumentSession. A claim queued onto a unit of work nothing saves is never written — so the endpoint
reports itself as deduplicated and every replay runs, with no exception and nothing logged, while every
source-level assertion still passes. Require the actual CreateDocumentSessionFrameand DocumentSessionSaveChanges postprocessor, as MartenPersistenceFrameProvider.TryBuildTransactionalDeduplication
does.
Wolverine.Http.Tests/Marten/deduplication_on_a_marten_transaction.cs (includes the no-commit fallback)
Two of those need copying carefully rather than simplifying:
The concurrency test. Two plain concurrent sends pass against a build with no commit-race handling
at all, because the optimistic check refuses the second one whenever the first has already committed. It
uses a barrier inside the handler to hold both executions past their check before either commits.
Verified as a negative control on Marten: with the wrapper removed the loser escaped as a raw MartenCommandException. Polecat's equivalent classifier has to be scoped to the deduplication table —
see Polecat and Fisher discard ANY unique-constraint violation, not just a duplicate inbox row #4565, which is a live bug in the existing unscoped Discard() rule.
the_claim_is_written_even_when_it_is_the_only_thing_in_the_unit_of_work. Polecat needed an upstream
fix (polecat#161, shipped in 4.2.1) so SaveChangesAsync still runs queued participants with no document
operations outstanding, which is exactly the shape a claim-only commit takes.
Any test that counts documents must clear them in setup — ResetResourceState clears Wolverine's tables
but leaves the application's documents, so it passes once and fails on every rerun.
Follow-up to #4505. Marten reference implementation: #4568.
Recreate for Polecat what #4568 did for Marten: write the logical deduplication claim
inside the Polecat session's own transaction instead of on a separate connection, so a failed or refused
chain never claimed at all and the compensating release disappears.
#4564 is the shared design brief for both stores — read it for what core already provides and why this is
not a copy of the Marten implementation. Fisher is tracked separately.
The one structural difference from Marten
Marten works because Wolverine's message store is built from Marten's own
NpgsqlDataSource(
WolverineOptionsMartenExtensions.BuildSinglePostgresqlMessageStore), soQueueOperationon the sessionwrites on the same connection the handler commits through.
Polecat's is not.
WolverineOptionsPolecatExtensions.BuildSqlServerMessageStorepasses only theconnection string, and
SqlServerMessageStore's base call then does:Same SQL Server database, different
DbDataSourceand connection pool. So the claim has to be writtenon the connection Polecat's session is holding, which means
ITransactionParticipant:ITransactionParticipant.BeforeCommitAsync(SqlConnection, SqlTransaction, CancellationToken)is handed thelive connection and transaction, and runs after Polecat's own SQL but before the commit.
Note also that Polecat's
IDocumentOperations.QueueSqlCommandhas only the(string, params object[])overload — nochar placeholdervariant, unlike Marten and Fisher(
PolecatOps.Documents.cssays so in its file header). A typed participant avoids the question.The optimistic check
Marten enlists an
IQueryHandler<bool>viaIBatchedQuery.AddItem, so the "has this been claimed?" queryshares the round trip with a
[WriteAggregate]FetchForWriting.PolecatBatchingPolicyandPolecat.Batching.IBatchedQueryhave the same shape and the policy is registered(
PolecatIntegration.cs), but whetherPolecat.Batching.IBatchedQueryexposes anAddItem<T>equivalent is unverified — check that first, because if it does not, this needs an upstream Polecat ask
before it can be done in one round trip.
Also:
PolecatBatchingPolicy.SortThroughFramesis unconditional — it has no equivalent of Marten'sIsBatchablefilter. That filter is what lets a chain with an optional deduplication id opt out of thebatch, which is load-bearing: batch enlistment is unconditional code, and unkeyed traffic on a
Required = falsechain is supposed to cost no round trip at all.Schema
MessageDatabase.DeduplicationFullName(added in #4568) renders it correctly. The Polecat message-storeschema and the document schema are the same by default (
WolverineOptionsPolecatExtensions, default"wolverine"), but not in the multi-tenant path, and the ancillary default(
AncillaryWolverineOptionsPolecatExtensions) does not inheritstore.Options.DatabaseSchemaName.Multi-database tenancy falls back
Polecat has the same split as Marten —
store.Options.Tenancy.Cardinality != DatabaseCardinality.Singlebuilds a separate main store from
PolecatIntegration.MainConnectionString. That chain must fall throughto claim-and-release, exactly as the Marten guard does.
The guard is not
CanApplyRepeating this from #4564 because getting it wrong is silent and total.
CanApplyanswers "could thisprovider own the chain's transaction?", which is true for any chain that merely takes an
IDocumentSession. A claim queued onto a unit of work nothing saves is never written — so the endpointreports itself as deduplicated and every replay runs, with no exception and nothing logged, while every
source-level assertion still passes. Require the actual
CreateDocumentSessionFrameandDocumentSessionSaveChangespostprocessor, asMartenPersistenceFrameProvider.TryBuildTransactionalDeduplicationdoes.
Tests to mirror
From #4568:
MartenTests/deduplication_rides_the_marten_transaction.csMartenTests/AncillaryStores/deduplication_rides_an_ancillary_marten_transaction.csWolverine.Http.Tests/Marten/deduplication_on_a_marten_transaction.cs(includes the no-commit fallback)Two of those need copying carefully rather than simplifying:
at all, because the optimistic check refuses the second one whenever the first has already committed. It
uses a barrier inside the handler to hold both executions past their check before either commits.
Verified as a negative control on Marten: with the wrapper removed the loser escaped as a raw
MartenCommandException. Polecat's equivalent classifier has to be scoped to the deduplication table —see Polecat and Fisher discard ANY unique-constraint violation, not just a duplicate inbox row #4565, which is a live bug in the existing unscoped
Discard()rule.the_claim_is_written_even_when_it_is_the_only_thing_in_the_unit_of_work. Polecat needed an upstreamfix (polecat#161, shipped in 4.2.1) so
SaveChangesAsyncstill runs queued participants with no documentoperations outstanding, which is exactly the shape a claim-only commit takes.
ResetResourceStateclears Wolverine's tablesbut leaves the application's documents, so it passes once and fails on every rerun.
Follow-up to #4505. Marten reference implementation: #4568.