feat(storage): EfMessageStore — SQL claim (UPDATE…RETURNING) + idempotent fan-out (S2 of #37)#59
Merged
Merged
Conversation
…tent fan-out Sub-task 2 of 3 for #37 (durable SQLite store), under M2 epic #8. Implements the internal IMessageStore on EF Core / SQLite, mirroring the in-memory store's semantics. - ClaimDueAsync pushes the claim into one atomic UPDATE … WHERE id IN (SELECT … ORDER BY next_attempt_at LIMIT) … RETURNING. SQLite serializes writers, so a competing dispatcher's identical statement sees the already-claimed state and excludes those rows — no double-claim, no SKIP LOCKED needed. Due = pending, next attempt arrived, unowned or lease lapsed (lease_until < now is strict). - AddAsync is idempotent on (event_id, endpoint_id) via INSERT … ON CONFLICT DO NOTHING; the rows-affected sum is the count actually inserted. - MarkDelivered / Reschedule / DeadLetter via EF ExecuteUpdateAsync (set-based, no load). - A fresh context + connection per operation (DbContext is not thread-safe), so the store is safe under concurrent dispatchers; deps are IDbContextFactory + TimeProvider. - Core exposes its internal resource-access contracts to the provider via InternalsVisibleTo. Tests mirror InMemoryMessageStoreTests against a real SQLite file — claim/lease/ crash-recovery, idempotent fan-out, batch order, the exact-expiry boundary, and no-double-claim across 8 concurrent workers over 200 messages — plus constructor guards and full-message round-trip. 175 tests green on net8.0 + net10.0, zero warnings. Mutation: EfMessageStore.cs scores 100% locally (Stryker, --project EFCore). The EF project is not yet in the CI Stryker scope; wiring it in (and covering the #45 config files via #47's endpoint round-trip tests) is the follow-up that lands the provider in the mutation gate. Closes #46. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 tasks
This was referenced Jun 27, 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.
What
Sub-task 2 of 3 for #37 (durable SQLite store), under M2 epic #8. Implements the internal
IMessageStoreon EF Core / SQLite, mirroring the in-memory store's semantics exactly.The claim path (the interesting bit)
ClaimDueAsyncpushes the claim into one atomic statement:SQLite serializes writers, so a competing dispatcher's identical statement runs against the already-claimed state and its
WHEREexcludes those rows — no double-claim, noSKIP LOCKEDneeded (that's the Postgres story, #38). Due = pending + next attempt arrived + (unowned or lease lapsed);lease_until < nowis strict, so a lease is still held at the exact expiry instant.The rest
AddAsync— idempotent on(event_id, endpoint_id)viaINSERT … ON CONFLICT DO NOTHING; the rows-affected sum is the count actually inserted.MarkDelivered/Reschedule/DeadLetter— EFExecuteUpdateAsync(set-based, no load).IDbContextFactory+TimeProvider.InternalsVisibleTo.Verification
InMemoryMessageStoreTestsagainst a real SQLite file: claim/lease/crash-recovery, idempotent fan-out, batch order, the exact-expiry boundary, no-double-claim across 8 concurrent workers over 200 messages, constructor guards, full-message round-trip.EfMessageStore.cs= 100% locally (dotnet stryker --project Caliber.Webhooks.EntityFrameworkCore).Follow-up (called out, not in this PR)
The EF provider isn't in the CI Stryker scope yet (config is core-only). Wiring it in — together with covering the #45 config files (
EndpointConfiguration/WebhookMessageConfiguration, currently 0%, exercised once #47's endpoint round-trip tests land) — is the next quality-gate step so the provider lands in the mutation gate with a green full score.risk:core— human review + merge. Closes #46.