Translate transient WriteConflict to ConcurrencyFailureException#5195
Translate transient WriteConflict to ConcurrencyFailureException#5195seonwooj0810 wants to merge 1 commit into
Conversation
When a MongoException carries a TransientTransactionError label, return ConcurrencyFailureException (a TransientDataAccessException) instead of DataIntegrityViolationException so retry policies keyed on Spring's exception hierarchy can react accordingly. Applied in both the DATA_INTEGRITY_EXCEPTIONS branch and the isDataIntegrityViolationError fall-through branch. Closes spring-projects#5150 Signed-off-by: seonwooj0810 <seonwooj0810@gmail.com>
| } | ||
| } | ||
|
|
||
| if (isTransientFailure(ex)) { |
There was a problem hiding this comment.
I am not sure this is the best approach. We would not catch all exceptions and I would like to explore a broader wrapping, maybe even introducing a TransactionalMongoExceptionTranslator variant that wraps retryable exceptions in ConcurrencyFailureException.
In combination with MongoTransactionManager and Spring Framework 7 RetryOperations this could result in a good change to opt in to retries along with a signal for retryable operations.
There was a problem hiding this comment.
Thanks @mp911de — that's a fair critique, and I agree the change as it stands is narrower than the problem deserves.
You're right that gating on isTransientFailure(...) inside the two data-integrity branches only rescues the WriteConflict-shaped path; any other retryable transient error that surfaces through a different branch still lands on a non-transient DataAccessException, so it's a partial signal at best.
The TransactionalMongoExceptionTranslator variant you're describing sounds like the right shape. Instead of sprinkling per-branch isTransientFailure checks, do a single up-front pass that maps anything carrying TransientTransactionError (and, on the commit side, UnknownTransactionCommitResult) to ConcurrencyFailureException, then delegate everything else to the standard translation. Keeping it as a separate opt-in translator rather than changing the default MongoExceptionTranslator behavior avoids surprising users who rely on today's mapping, and it composes cleanly: wire it through MongoTransactionManager, and Spring Framework 7 RetryOperations gets a consistent transient signal to key retries on.
A couple of things I'd like your steer on before I rework the PR:
- Opt-in surface — should the transactional translator be selected automatically when a
MongoTransactionManageris in play, or exposed as an explicit configuration knob? - Scope of "retryable" — beyond the
TransientTransactionErrorlabel, should it also treatUnknownTransactionCommitResultas retryable, and should the decision lean on the error labels only, or also on an explicit code set (112WriteConflict, 24LockTimeout, …)?
Happy to take a first pass at TransactionalMongoExceptionTranslator along these lines if that direction works for you.
WriteConflict (code 112) returned with a `TransientTransactionError` label is
a retryable concurrency failure, but `MongoExceptionTranslator` mapped it to
`DataIntegrityViolationException` — a `NonTransientDataAccessException` —
which silently breaks retry policies keyed on Spring's exception hierarchy.
This change checks `isTransientFailure(ex)` before falling through to
`DataIntegrityViolationException` and returns `ConcurrencyFailureException`
(a `TransientDataAccessException`) instead. The check is applied in both
the `DATA_INTEGRITY_EXCEPTIONS` branch (real `MongoWriteException`) and the
`isDataIntegrityViolationError` fall-through branch (plain `MongoException`
carrying a data-integrity error code).
Scope note: `MongoBulkWriteException` continues to return
`BulkOperationException` because bulk failures require per-write inspection
and were deemed out of scope for this fix.
Closes #5150