[DRK-75] Atomic idempotency key reservation - #340
Conversation
IsKeyProcessedAsync now atomically reserves a composite key (HTTP 102 placeholder) before returning, so concurrent requests with the same idempotency key can no longer all pass the check before any of them completes. The SQL store enforces this via UX_CompositeKey; the distributed-cache store narrows (documented, not eliminated) the same race since IDistributedCache has no compare-and-set primitive. Closes architecture finding IDEM-CONCURRENCY-001 (DRK-75). Co-authored-by: multica-agent <github@multica.ai>
Sibling test projects already reference it; this project was missing it, so coverage % couldn't be measured for touched non-SQL-Server classes. Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
📊 Code Coverage Report |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## dev #340 +/- ##
==========================================
+ Coverage 79.09% 83.14% +4.04%
==========================================
Files 169 169
Lines 4196 4247 +51
Branches 613 617 +4
==========================================
+ Hits 3319 3531 +212
+ Misses 695 530 -165
- Partials 182 186 +4
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
CreateItem_ConcurrentRequestsWithSameKey_OnlyOneProcessed (the only test that ever proved the atomic reservation under real concurrency) is permanently [Skip]d since bf50729 retired the SQL Server TestContainers path. Add IdempotencySqlServerStoreConcurrencyTests, exercising IdempotencySqlServerStore.IsKeyProcessedAsync directly against a file-based SQLite IdempotencyDbContext (not InMemory - needs real unique index enforcement across concurrent connections) using the same IdempotencyKeyConfiguration the SQL Server store ships with. Fires 5 concurrent reservation attempts for an identical composite key and asserts exactly one wins - mirroring the retired HTTP-level test, but at the store layer so it needs no Docker/SQL Server. Two Sqlite-provider incompatibilities in the shared configuration needed a test-local workaround (no production code touched): - Body's raw HasColumnType("nvarchar(max)") isn't valid SQLite syntax. - The Sqlite provider can't translate > /< on a DateTimeOffset column (only equality), which IsKeyProcessedAsync's expiry check relies on. Both are patched via a test-only IModelCustomizer that strips the column-type override and stores ExpiresAt as UTC ticks instead. Verified: reverting IsKeyProcessedAsync to the pre-fix check-then-act shape (commit b85e224's parent) makes this test fail (5/5 callers see (false, null) instead of 1/5) - confirming it would have caught DRK-75's original race. Restored, it passes cleanly, 10/10 repeated runs, and dotnet build/format come back clean. Refs DRK-176, DRK-174, DRK-75 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai>
📊 Code Coverage Report |
Automated Review Gate — PR #340 ([DRK-75] Atomic idempotency key reservation)Score: 7.9 / 10 → REWORK (round 1) SummaryThe fix correctly closes the non-atomic check-then-act race in The single issue preventing approval: test coverage. CI measures diff coverage at 53.65%, well below the squad's ≥90% threshold. The new SQLite-backed concurrency test covers the store-level race path well, but the Score breakdown
Hard caps applied: Coverage below 90% → 7.9 max. Final: 7.9. FindingsBlockingNone. Important
Nit
Praise
Out-of-scope (not scored, captured for follow-up tracking)
Auto-merge preconditions
Verdict: REWORKThe implementation is sound. One fix round: bring diff coverage to ≥90% by adding tests for the uncovered paths — specifically the distributed-cache store changes, the |
…r ≥90% diff coverage - SQL store: full lifecycle (reserve→complete→replay), in-flight recheck, collision re-query returning a completed response, expired-reservation collision allowing a fresh reservation, and the defensive entity-is-null fallback in MarkKeyAsProcessedAsync - Distributed cache store: reservation placeholder write/in-flight recheck, reservation→complete replay, and InFlightReservationTimeout expiry - IdempotencyOptions: default and custom InFlightReservationTimeout Addresses DRK-183 (PR #340 review, coverage gap 53.65% -> local run shows IdempotencySqlServerStore.cs and IdempotencyDistributedCacheStore.cs fully exercised) Co-authored-by: multica-agent <github@multica.ai>
Automated Review Gate — PR #340, round 2 (re-armed)Score: 6.9 / 10 → REWORK (round 2 of 2) Verdict: BLOCKING — CI failure from merge conflict with
|
| Category | Weight | Score | Notes |
|---|---|---|---|
| Correctness & logic | 25% | 8 | The new test files look correct; CI failure prevents full verification |
| Security | 20% | 10 | No findings |
| Testing & coverage | 20% | — | Cannot verify — CI build failed before tests ran; no codecov result |
| Maintainability & design | 15% | 9 | Test additions well-structured; merge conflict is temp infra |
| Spec conformance | 10% | 10 | DRK-183 diff is test-only, matches constraint |
| Style & conventions | 5% | 10 | Consistent |
| AI-slop gate | 5% | 10 | No anti-patterns in test additions |
Hard cap: CI failing → 6.9 max. Final: 6.9.
Findings
Blocking
- [blocking] CI
build-test-coveragefailed — duplicateInFlightReservationTimeouton merge withdev.dev'sIdempotencyOptions.cs:81added the same property (via an already-merged PR) at a different position than the feature branch'sIdempotencyOptions.cs:73. This is a git/semantic merge conflict — not caught textually, fails at compile.
Praise
- [praise]
IdempotencySqlServerStoreLifecycleTests.cs(203 lines) — comprehensive lifecycle coverage: reserve, complete, re-check, in-flight collision, expired reservation. Exactly the coverage gap from round 1's review. - [praise]
IdempotencyDistributedCacheReservationTests.cs(115 lines) — exercises the distributed-cache reservation path (miss, in-flight, completed), filling the second major gap.
What went well, what's needed
The test additions from DRK-183 are well-structured and address all the coverage gaps I flagged in round 1. The fix is a one-line rebase to reconcile InFlightReservationTimeout with dev's now-canonical version (keep dev's placement + doc, remove the feature branch's duplicate). No code changes beyond that — the tests themselves don't need modification.
Auto-merge preconditions
| Precondition | Status |
|---|---|
| No blocking findings | ❌ CI compile failure |
| CI not failing | ❌ build-test-coverage failed |
| Coverage ≥ 90% | Unknown (CI didn't reach tests) |
| All others | ✅ |
Gate action
This is a git/branch-mechanics issue (rebase needed to reconcile with external dev changes), not a code defect. Routing to dev-leader per git/PR-mechanics findings contract — the implementer is forbidden from opening or editing PRs.
@dev-leader — branch needs a rebase on dev to remove the duplicate InFlightReservationTimeout (keep dev's version at line 81). Once CI passes and codecov reports diff coverage, the review re-arms for a third attempt (rework cap is 2, so this will be the final gate pass or escalate).
Merging origin/dev (which now carries DRK-175's independently-added identical property) produced no textual conflict but a duplicate declaration (CS0102) since both branches added the same TimeSpan property with the same default. Kept dev's canonical declaration, removed this branch's earlier duplicate. Co-authored-by: multica-agent <github@multica.ai>
📊 Code Coverage Report |
Automated Review Gate — PR #340, final passScore: 9.6 / 10 → APPROVED & MERGED PR merged into Score breakdown
FindingsNit
Praise
Auto-merge preconditions — all passed
Vote skipped (PR self-authored by |
[DRK-75] Atomic idempotency key reservation
Related to DRK-75 (architecture finding
IDEM-CONCURRENCY-001, severity critical).Problem
IdempotencyEndpointFilter.InvokeAsyncdid a non-atomic check-then-act: it read "has this key been processed?" and, only if not, invoked the protected handler. Two concurrent requests with the same idempotency key could both observe "not processed" and both execute the side-effecting operation — the exact retry scenario idempotency exists to prevent.Fix
IdempotencySqlServerStore.IsKeyProcessedAsyncnow atomically reserves the composite key (an HTTP102 Processingplaceholder row) before returning "not processed," relying on the existingUX_CompositeKeyunique index to guarantee exactly one winner under concurrency.IdempotencyKeyEntity.Complete(CachedResponse)added;MarkKeyAsProcessedAsyncnow completes the reservation row in place instead of a blind insert.IdempotencyDistributedCacheStoremirrors the same reservation shape (documented as narrowing, not eliminating, the race —IDistributedCachehas no compare-and-set).IdempotencyOptions.InFlightReservationTimeout(30s default) bounds how long a reservation is honoured, so a crashed handler doesn't permanently block retries.IIdempotencyKeyStore.IsKeyProcessedAsync's contract doc updated to state the new atomic check-and-reserve guarantee. No public signature changes.CreateItem_ConcurrentRequestsWithSameKey_OnlyOneProcessedto assert exactly 1×201/4×409 instead of tolerating more than one success.Verification
AspCore.Idempotency.MsSqlStore.Tests(real SQL Server, GitHub Actionsremote-tests.yml, run 30971952365 on this branch's current tip): 9/9 passed, including the concurrency test.AspCore.Idempotency.Tests(non-SQL, local): 54/54 passed.dotnet packforDKNet.AspCore.IdempotencyandDKNet.AspCore.Idempotency.MsSqlStore.IdempotencySqlServerStore.cs/IdempotencyKeyEntity.csagainst the squad's ≥90% target — localTestContainers.MsSqlcannot start in this sandbox (arm64 Docker networking), andremote-tests.ymldoesn't collect coverage by design. Functional correctness on these files is proven (9/9 including the concurrency case); the actual number should surface once this PR'sbuild-test-coverage.yml/SonarCloud run completes.EfCore.Events.Tests,EfCore.AuditLogs.Tests,Svc.PdfGenerators.Tests) observed on the full-solution run — not touched by this diff, not a regression here.Scope
Only the files in
DKNet.AspCore.Idempotency,DKNet.AspCore.Idempotency.MsSqlStore, andAspCore.Idempotency.MsSqlStore.Testslisted above, plus acoverlet.collectoraddition toAspCore.Idempotency.Tests.csproj.