Skip to content

GH-4511: throw SagaConcurrencyException from the Sqlite lightweight saga storage - #4533

Merged
jeremydmiller merged 1 commit into
mainfrom
GH-4511-sqlite-saga-concurrency
Sep 22, 2026
Merged

jeremydmiller merged 1 commit into
mainfrom
GH-4511-sqlite-saga-concurrency

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Closes #4511.

The Sqlite lightweight saga storage threw a bare System.Exception when an optimistic-concurrency update touched zero rows:

// before
throw new Exception($"Saga version mismatch for {typeof(T).FullName} with id {id}. Possible concurrent update detected.");

Every other lightweight provider throws SagaConcurrencyException at that point — Postgres, SQL Server, MySQL, Oracle — as do the EF Core, CosmosDB, S3, Azure Blob and Redis saga paths. SagaConcurrencyException derives from JasperFx.ConcurrencyException (GH-3444) so that one OnException<ConcurrencyException>().RetryWithCooldown(...) policy covers saga concurrency failures on every provider.

Wolverine's failure-rule matching is ex is T, so on Sqlite the bare Exception fell straight through that policy to the default MoveToErrorQueue continuation. Same saga, same handler code, a different outcome depending on which persistence package is referenced. Fisher-backed hosts inherit it, since Wolverine.Fisher composes over the Sqlite persistence.

Change

src/Persistence/Wolverine.Sqlite/Sagas/DatabaseSagaSchema.cs now throws SagaConcurrencyException with the same message shape as the other providers, so the wording is uniform too.

Why this was invisible

SqliteTests/Sagas/saga_storage_operations.cs already had a concurrency_exception_when_version_does_not_match test — asserting Should.ThrowAsync<Exception>(), which matches anything that throws at all. It passed over the defect for as long as it has existed. It now asserts the exception type, its ConcurrencyException ancestry (the property the retry policy actually depends on), and the message shape.

Verified as a negative control: with the production change stashed, that test fails (1 of 6); with it applied, all 6 pass.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VDUrBeB4tTnKj4AExCS1nj

…aga storage

The Sqlite saga schema threw a bare System.Exception when an optimistic-concurrency
update touched zero rows. Every other lightweight provider (Postgres, SQL Server,
MySQL, Oracle) and every document-store saga path throws SagaConcurrencyException
at the same point, which derives from JasperFx.ConcurrencyException precisely so a
single OnException<ConcurrencyException>() policy covers saga concurrency on all of
them. Wolverine's failure-rule matching is `ex is T`, so the bare Exception fell
through that policy to the default MoveToErrorQueue continuation: same saga, same
code, a different outcome depending on which persistence package is referenced.
Fisher-backed hosts inherit this, since Wolverine.Fisher composes over the Sqlite
persistence.

The existing Sqlite test asserted Should.ThrowAsync<Exception>, which matches
anything at all, so it passed over the defect. It now asserts the type, its
ConcurrencyException ancestry, and the message shape the other providers use.
Verified as a negative control: that test fails against the old bare throw.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VDUrBeB4tTnKj4AExCS1nj
@jeremydmiller
jeremydmiller merged commit 5132224 into main Sep 22, 2026
44 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SQLite lightweight saga storage throws bare Exception instead of SagaConcurrencyException

1 participant