User Story
As a developer running multiple Pulse instances with a SQL or Entity Framework idempotency store, I want the key reservation to be atomic, so that a command submitted concurrently with the same idempotency key runs its handler exactly once.
Problem
#816 adds IIdempotencyKeyRepository.TryStoreAsync and makes the Redis provider reserve keys atomically. The SQL Server, PostgreSQL, MySQL, SQLite and Entity Framework providers implement TryStoreAsync by composing ExistsAsync and StoreAsync, and their XML remarks say so ("not atomic").
Failure scenario: instances A and B receive the same command at the same time.
- A and B both call
ExistsAsync and get false.
- Both call
StoreAsync. The duplicate insert is swallowed (or updates the row), so neither call fails.
- Both
TryReserveAsync calls return true, and the handler runs twice.
The shared integration test base (tests/NetEvolve.Pulse.Tests.Integration/Idempotency/IdempotencyTestsBase.cs) already contains the concurrency tests. They are skipped for these providers because SupportsAtomicReservation is false.
Requirements
Related: #816, #814.
User Story
As a developer running multiple Pulse instances with a SQL or Entity Framework idempotency store, I want the key reservation to be atomic, so that a command submitted concurrently with the same idempotency key runs its handler exactly once.
Problem
#816 adds
IIdempotencyKeyRepository.TryStoreAsyncand makes the Redis provider reserve keys atomically. The SQL Server, PostgreSQL, MySQL, SQLite and Entity Framework providers implementTryStoreAsyncby composingExistsAsyncandStoreAsync, and their XML remarks say so ("not atomic").Failure scenario: instances A and B receive the same command at the same time.
ExistsAsyncand getfalse.StoreAsync. The duplicate insert is swallowed (or updates the row), so neither call fails.TryReserveAsynccalls returntrue, and the handler runs twice.The shared integration test base (
tests/NetEvolve.Pulse.Tests.Integration/Idempotency/IdempotencyTestsBase.cs) already contains the concurrency tests. They are skipped for these providers becauseSupportsAtomicReservationisfalse.Requirements
TryStoreAsyncatomically in each SQL/EF provider, for example with a singleINSERT ... ON CONFLICT DO UPDATE ... WHERE CreatedAt < @validFrom(PostgreSQL/SQLite),MERGEwithHOLDLOCK(SQL Server),INSERT ... ON DUPLICATE KEY UPDATE(MySQL), and decidetrue/falsefrom the affected row count.CreatedAtis older thanvalidFromis replaced and itsCreatedAtrefreshed (overlaps with fix: Refresh expired idempotency keys on re-store so duplicates are rejected again in all SQL/EF providers andNetEvolve.Pulse.Redis#814).nullTimeToLivesemantics from fix: Honor nullTimeToLive("never expire") instead of a 24h physical expiry inNetEvolve.Pulse.Redis#790: an existing key is never reservable again.SupportsAtomicReservation => truein the provider's integration test class once the provider is atomic, so the concurrency tests run.Related: #816, #814.