From 6d3e496d34bd298928d4a148503324d161cec146 Mon Sep 17 00:00:00 2001 From: seonwooj0810 Date: Tue, 26 May 2026 16:40:43 +0900 Subject: [PATCH] Translate transient WriteConflict to ConcurrencyFailureException. 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 #5150 Signed-off-by: seonwooj0810 --- .../core/MongoExceptionTranslator.java | 9 +++++ .../MongoExceptionTranslatorUnitTests.java | 40 +++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoExceptionTranslator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoExceptionTranslator.java index 66f5484ac0..943b1ce07b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoExceptionTranslator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoExceptionTranslator.java @@ -20,6 +20,7 @@ import com.mongodb.ClientBulkWriteException; import org.bson.BsonInvalidOperationException; import org.jspecify.annotations.Nullable; +import org.springframework.dao.ConcurrencyFailureException; import org.springframework.dao.DataAccessException; import org.springframework.dao.DataAccessResourceFailureException; import org.springframework.dao.DataIntegrityViolationException; @@ -50,6 +51,7 @@ * @author Michal Vich * @author Christoph Strobl * @author Brice Vandeputte + * @author Seonwoo Jung */ public class MongoExceptionTranslator implements PersistenceExceptionTranslator { @@ -120,6 +122,10 @@ DataAccessException doTranslateException(RuntimeException ex) { } } + if (isTransientFailure(ex)) { + return new ConcurrencyFailureException(ex.getMessage(), ex); + } + return new DataIntegrityViolationException(ex.getMessage(), ex); } @@ -142,6 +148,9 @@ DataAccessException doTranslateException(RuntimeException ex) { return new PermissionDeniedDataAccessException(ex.getMessage(), ex); } if (MongoDbErrorCodes.isDataIntegrityViolationError(mongoException)) { + if (isTransientFailure(mongoException)) { + return new ConcurrencyFailureException(mongoException.getMessage(), mongoException); + } return new DataIntegrityViolationException(mongoException.getMessage(), mongoException); } if (MongoDbErrorCodes.isClientSessionFailure(mongoException)) { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoExceptionTranslatorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoExceptionTranslatorUnitTests.java index c4bb509ac8..1ec2262a91 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoExceptionTranslatorUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoExceptionTranslatorUnitTests.java @@ -24,6 +24,7 @@ import org.mockito.Mockito; import org.springframework.core.NestedRuntimeException; +import org.springframework.dao.ConcurrencyFailureException; import org.springframework.dao.DataAccessException; import org.springframework.dao.DataAccessResourceFailureException; import org.springframework.dao.DuplicateKeyException; @@ -39,7 +40,9 @@ import com.mongodb.MongoSocketException; import com.mongodb.MongoSocketReadTimeoutException; import com.mongodb.MongoSocketWriteException; +import com.mongodb.MongoWriteException; import com.mongodb.ServerAddress; +import com.mongodb.WriteError; /** * Unit tests for {@link MongoExceptionTranslator}. @@ -48,6 +51,7 @@ * @author Oliver Gierke * @author Christoph Strobl * @author Brice Vandeputte + * @author Seonwoo Jung */ class MongoExceptionTranslatorUnitTests { @@ -193,6 +197,42 @@ public void translateMongoExceptionWithTransientLabel() { expectExceptionWithCauseMessage(translatedException, UncategorizedMongoDbException.class); } + @Test // GH-5150 + void translateTransientWriteConflictMongoException() { + + MongoException source = new MongoException(112, "WriteConflict"); + source.addLabel(MongoException.TRANSIENT_TRANSACTION_ERROR_LABEL); + + DataAccessException translated = translator.translateExceptionIfPossible(source); + + expectExceptionWithCauseMessage(translated, ConcurrencyFailureException.class, "WriteConflict"); + assertThat(translator.isTransientFailure(translated)).isTrue(); + } + + @Test // GH-5150 + void translateTransientMongoWriteException() { + + WriteError writeError = new WriteError(112, "WriteConflict", new BsonDocument()); + MongoWriteException source = new MongoWriteException(writeError, new ServerAddress()); + source.addLabel(MongoException.TRANSIENT_TRANSACTION_ERROR_LABEL); + + DataAccessException translated = translator.translateExceptionIfPossible(source); + + expectExceptionWithCauseMessage(translated, ConcurrencyFailureException.class, "WriteConflict"); + assertThat(translator.isTransientFailure(translated)).isTrue(); + } + + @Test // GH-5150 + void translateNonTransientWriteConflictMongoException() { + + MongoException source = new MongoException(112, "WriteConflict"); + + DataAccessException translated = translator.translateExceptionIfPossible(source); + + expectExceptionWithCauseMessage(translated, + org.springframework.dao.DataIntegrityViolationException.class, "WriteConflict"); + } + private void checkTranslatedMongoException(Class clazz, int code) { DataAccessException translated = translator.translateExceptionIfPossible(new MongoException(code, ""));