From 422a9ca7511be0362c7978a4d484e899d2051114 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 30 Aug 2024 11:11:07 +0200 Subject: [PATCH 1/5] Prepare issue branch. --- pom.xml | 2 +- spring-data-mongodb-benchmarks/pom.xml | 2 +- spring-data-mongodb-distribution/pom.xml | 2 +- spring-data-mongodb/pom.xml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index f785c3872d..9f9e11b3c4 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 4.4.0-SNAPSHOT + 4.4.0-DATAMONGO-2073-SNAPSHOT pom Spring Data MongoDB diff --git a/spring-data-mongodb-benchmarks/pom.xml b/spring-data-mongodb-benchmarks/pom.xml index a3dc49f892..4a4f560269 100644 --- a/spring-data-mongodb-benchmarks/pom.xml +++ b/spring-data-mongodb-benchmarks/pom.xml @@ -7,7 +7,7 @@ org.springframework.data spring-data-mongodb-parent - 4.4.0-SNAPSHOT + 4.4.0-DATAMONGO-2073-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index acdc13437d..e10c2d3312 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -15,7 +15,7 @@ org.springframework.data spring-data-mongodb-parent - 4.4.0-SNAPSHOT + 4.4.0-DATAMONGO-2073-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index fafe9c8793..45744cda94 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -13,7 +13,7 @@ org.springframework.data spring-data-mongodb-parent - 4.4.0-SNAPSHOT + 4.4.0-DATAMONGO-2073-SNAPSHOT ../pom.xml From b233c8f5da03fbf0a1d541ca679d1debbe0da16c Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 28 Aug 2018 13:06:00 +0200 Subject: [PATCH 2/5] DATAMONGO-2073 - Evaluate exception label when translating MongoExceptions. We now distinguish between Transient and NonTransient failures by checking the Error labels of an Error and create the according DataAccessException based on that information. These URLs were switched to an https URL with a 2xx status. While the status was successful, your review is still recommended. * [ ] http://www.apache.org/licenses/ with 1 occurrences migrated to: https://www.apache.org/licenses/ ([https](https://www.apache.org/licenses/) result 200). * [ ] http://www.apache.org/licenses/LICENSE-2.0 with 852 occurrences migrated to: https://www.apache.org/licenses/LICENSE-2.0 ([https](https://www.apache.org/licenses/LICENSE-2.0) result 200). Original Pull Request: #721 --- .../TransientClientSessionException.java | 39 ++++++ .../mongodb/TransientMongoDbException.java | 40 ++++++ .../core/MongoExceptionTranslator.java | 84 ++++++++++--- .../MongoPersistentEntityIndexCreator.java | 3 +- .../IndexEnsuringQueryCreationListener.java | 4 +- .../data/mongodb/util/MongoDbErrorCodes.java | 118 ++++++++++++++++-- .../MongoExceptionTranslatorUnitTests.java | 46 ++++++- 7 files changed, 296 insertions(+), 38 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransientClientSessionException.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransientMongoDbException.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransientClientSessionException.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransientClientSessionException.java new file mode 100644 index 0000000000..fd16ae97ec --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransientClientSessionException.java @@ -0,0 +1,39 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb; + +import org.springframework.dao.TransientDataAccessException; +import org.springframework.lang.Nullable; + +/** + * {@link TransientDataAccessException} specific to MongoDB {@link com.mongodb.session.ClientSession} related data + * access failures such as reading data using an already closed session. + * + * @author Christoph Strobl + * @since 3.3 + */ +public class TransientClientSessionException extends TransientMongoDbException { + + /** + * Constructor for {@link TransientClientSessionException}. + * + * @param msg the detail message. Can be {@literal null}. + * @param cause the root cause. Can be {@literal null}. + */ + public TransientClientSessionException(@Nullable String msg, @Nullable Throwable cause) { + super(msg, cause); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransientMongoDbException.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransientMongoDbException.java new file mode 100644 index 0000000000..2a253c2b37 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransientMongoDbException.java @@ -0,0 +1,40 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb; + +import org.springframework.dao.TransientDataAccessException; +import org.springframework.lang.Nullable; + +/** + * Root of the hierarchy of MongoDB specific data access exceptions that are considered transient such as + * {@link com.mongodb.MongoException MongoExceptions} carrying {@link com.mongodb.MongoException#hasErrorLabel(String) + * specific labels}. + * + * @author Christoph Strobl + * @since 3.3 + */ +public class TransientMongoDbException extends TransientDataAccessException { + + /** + * Constructor for {@link TransientMongoDbException}. + * + * @param msg the detail message. Can be {@literal null}. + * @param cause the root cause. Can be {@literal null}. + */ + public TransientMongoDbException(String msg, @Nullable Throwable cause) { + super(msg, cause); + } +} 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 4775a4a4d2..bf09363ef2 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 @@ -25,9 +25,11 @@ import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.dao.InvalidDataAccessResourceUsageException; import org.springframework.dao.PermissionDeniedDataAccessException; +import org.springframework.dao.TransientDataAccessException; import org.springframework.dao.support.PersistenceExceptionTranslator; import org.springframework.data.mongodb.ClientSessionException; -import org.springframework.data.mongodb.MongoTransactionException; +import org.springframework.data.mongodb.TransientClientSessionException; +import org.springframework.data.mongodb.TransientMongoDbException; import org.springframework.data.mongodb.UncategorizedMongoDbException; import org.springframework.data.mongodb.util.MongoDbErrorCodes; import org.springframework.lang.Nullable; @@ -65,9 +67,26 @@ public class MongoExceptionTranslator implements PersistenceExceptionTranslator private static final Set SECURITY_EXCEPTIONS = Set.of("MongoCryptException"); + @Override @Nullable public DataAccessException translateExceptionIfPossible(RuntimeException ex) { + DataAccessException translatedException = doTranslateException(ex); + if (translatedException == null) { + return null; + } + + // Translated exceptions that per se are not be recoverable (eg. WriteConflicts), might still be transient inside a + // transaction. Let's wrap those. + return (isTransientFailure(ex) && !(translatedException instanceof TransientDataAccessException)) + ? new TransientMongoDbException(ex.getMessage(), translatedException) + : translatedException; + + } + + @Nullable + DataAccessException doTranslateException(RuntimeException ex) { + // Check for well-known MongoException subclasses. if (ex instanceof BsonInvalidOperationException) { @@ -94,13 +113,13 @@ public DataAccessException translateExceptionIfPossible(RuntimeException ex) { if (DATA_INTEGRITY_EXCEPTIONS.contains(exception)) { - if (ex instanceof MongoServerException mse) { - if (mse.getCode() == 11000) { + if (ex instanceof MongoServerException) { + if (MongoDbErrorCodes.isDataDuplicateKeyError(ex)) { return new DuplicateKeyException(ex.getMessage(), ex); } if (ex instanceof MongoBulkWriteException bulkException) { - for (BulkWriteError x : bulkException.getWriteErrors()) { - if (x.getCode() == 11000) { + for (BulkWriteError writeError : bulkException.getWriteErrors()) { + if (MongoDbErrorCodes.isDuplicateKeyCode(writeError.getCode())) { return new DuplicateKeyException(ex.getMessage(), ex); } } @@ -115,20 +134,34 @@ public DataAccessException translateExceptionIfPossible(RuntimeException ex) { int code = mongoException.getCode(); - if (MongoDbErrorCodes.isDuplicateKeyCode(code)) { + if (MongoDbErrorCodes.isDuplicateKeyError(mongoException)) { return new DuplicateKeyException(ex.getMessage(), ex); - } else if (MongoDbErrorCodes.isDataAccessResourceFailureCode(code)) { + } + if (MongoDbErrorCodes.isDataAccessResourceError(mongoException)) { return new DataAccessResourceFailureException(ex.getMessage(), ex); - } else if (MongoDbErrorCodes.isInvalidDataAccessApiUsageCode(code) || code == 10003 || code == 12001 - || code == 12010 || code == 12011 || code == 12012) { + } + if (MongoDbErrorCodes.isInvalidDataAccessApiUsageError(mongoException) || code == 12001 || code == 12010 + || code == 12011 || code == 12012) { return new InvalidDataAccessApiUsageException(ex.getMessage(), ex); - } else if (MongoDbErrorCodes.isPermissionDeniedCode(code)) { + } + if (MongoDbErrorCodes.isPermissionDeniedError(mongoException)) { return new PermissionDeniedDataAccessException(ex.getMessage(), ex); - } else if (MongoDbErrorCodes.isClientSessionFailureCode(code)) { - return new ClientSessionException(ex.getMessage(), ex); - } else if (MongoDbErrorCodes.isTransactionFailureCode(code)) { - return new MongoTransactionException(ex.getMessage(), ex); - } else if(ex.getCause() != null && SECURITY_EXCEPTIONS.contains(ClassUtils.getShortName(ex.getCause().getClass()))) { + } + if (MongoDbErrorCodes.isDataIntegrityViolationError(mongoException)) { + return new DataIntegrityViolationException(mongoException.getMessage(), mongoException); + } + if (MongoDbErrorCodes.isClientSessionFailure(mongoException)) { + return isTransientFailure(mongoException) ? new TransientClientSessionException(ex.getMessage(), ex) + : new ClientSessionException(ex.getMessage(), ex); + } + if (MongoDbErrorCodes.isDataIntegrityViolationError(mongoException)) { + return new DataIntegrityViolationException(mongoException.getMessage(), mongoException); + } + if (MongoDbErrorCodes.isClientSessionFailure(mongoException)) { + return isTransientFailure(mongoException) ? new TransientClientSessionException(ex.getMessage(), ex) + : new ClientSessionException(ex.getMessage(), ex); + } + if (ex.getCause() != null && SECURITY_EXCEPTIONS.contains(ClassUtils.getShortName(ex.getCause().getClass()))) { return new PermissionDeniedDataAccessException(ex.getMessage(), ex); } @@ -150,4 +183,25 @@ public DataAccessException translateExceptionIfPossible(RuntimeException ex) { // that translation should not occur. return null; } + + /** + * Check if a given exception holds an error label indicating a transient failure. + * + * @param e + * @return {@literal true} if the given {@link Exception} is a {@link MongoException} holding one of the transient + * exception error labels. + * @see MongoException#hasErrorLabel(String) + * @since 3.3 + */ + public static boolean isTransientFailure(Exception e) { + + if (!(e instanceof MongoException)) { + return false; + } + + MongoException mongoException = (MongoException) e; + + return mongoException.hasErrorLabel(MongoException.TRANSIENT_TRANSACTION_ERROR_LABEL) + || mongoException.hasErrorLabel(MongoException.UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL); + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexCreator.java index fa201d40ea..4f4b9b72e5 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexCreator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexCreator.java @@ -28,7 +28,6 @@ import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.context.MappingContextEvent; import org.springframework.data.mongodb.MongoDatabaseFactory; -import org.springframework.data.mongodb.UncategorizedMongoDbException; import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexResolver.IndexDefinitionHolder; import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; @@ -152,7 +151,7 @@ void createIndex(IndexDefinitionHolder indexDefinition) { IndexOperations indexOperations = indexOperationsProvider.indexOps(indexDefinition.getCollection()); indexOperations.ensureIndex(indexDefinition); - } catch (UncategorizedMongoDbException ex) { + } catch (DataIntegrityViolationException ex) { if (ex.getCause() instanceof MongoException mongoException && MongoDbErrorCodes.isDataIntegrityViolationCode(mongoException.getCode())) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/IndexEnsuringQueryCreationListener.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/IndexEnsuringQueryCreationListener.java index 8683ba2439..9f642c1b64 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/IndexEnsuringQueryCreationListener.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/IndexEnsuringQueryCreationListener.java @@ -21,10 +21,10 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Direction; import org.springframework.data.domain.Sort.Order; -import org.springframework.data.mongodb.UncategorizedMongoDbException; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.index.Index; import org.springframework.data.mongodb.core.index.IndexOperationsProvider; @@ -111,7 +111,7 @@ public void onCreation(PartTreeMongoQuery query) { MongoEntityMetadata metadata = query.getQueryMethod().getEntityInformation(); try { indexOperationsProvider.indexOps(metadata.getCollectionName(), metadata.getJavaType()).ensureIndex(index); - } catch (UncategorizedMongoDbException e) { + } catch (DataIntegrityViolationException e) { if (e.getCause() instanceof MongoException mongoException) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoDbErrorCodes.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoDbErrorCodes.java index f2e02ae7b9..2b7ccde4d5 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoDbErrorCodes.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoDbErrorCodes.java @@ -19,6 +19,8 @@ import org.springframework.lang.Nullable; +import com.mongodb.MongoException; + /** * {@link MongoDbErrorCodes} holds MongoDB specific error codes outlined in {@literal mongo/base/error_codes.yml}. * @@ -97,6 +99,7 @@ public final class MongoDbErrorCodes { invalidDataAccessApiUsageException.put(72, "InvalidOptions"); invalidDataAccessApiUsageException.put(115, "CommandNotSupported"); invalidDataAccessApiUsageException.put(116, "DocTooLargeForCapped"); + invalidDataAccessApiUsageException.put(10003, "CannotGrowDocumentInCappedNamespace"); invalidDataAccessApiUsageException.put(130, "SymbolNotFound"); invalidDataAccessApiUsageException.put(17280, "KeyTooLong"); invalidDataAccessApiUsageException.put(13334, "ShardKeyTooBig"); @@ -114,19 +117,17 @@ public final class MongoDbErrorCodes { clientSessionCodes = new HashMap<>(4, 1f); clientSessionCodes.put(206, "NoSuchSession"); clientSessionCodes.put(213, "DuplicateSession"); + clientSessionCodes.put(217, "IncompleteTransactionHistory"); + clientSessionCodes.put(225, "TransactionTooOld"); clientSessionCodes.put(228, "SessionTransferIncomplete"); + clientSessionCodes.put(244, "TransactionAborted"); + clientSessionCodes.put(251, "NoSuchTransaction"); + clientSessionCodes.put(256, "TransactionCommitted"); + clientSessionCodes.put(257, "TransactionToLarge"); + clientSessionCodes.put(261, "TooManyLogicalSessions"); + clientSessionCodes.put(263, "OperationNotSupportedInTransaction"); clientSessionCodes.put(264, "TooManyLogicalSessions"); - transactionCodes = new HashMap<>(8, 1f); - transactionCodes.put(217, "IncompleteTransactionHistory"); - transactionCodes.put(225, "TransactionTooOld"); - transactionCodes.put(244, "TransactionAborted"); - transactionCodes.put(251, "NoSuchTransaction"); - transactionCodes.put(256, "TransactionCommitted"); - transactionCodes.put(257, "TransactionToLarge"); - transactionCodes.put(263, "OperationNotSupportedInTransaction"); - transactionCodes.put(267, "PreparedTransactionInProgress"); - errorCodes = new HashMap<>(); errorCodes.putAll(dataAccessResourceFailureCodes); errorCodes.putAll(dataIntegrityViolationCodes); @@ -136,29 +137,107 @@ public final class MongoDbErrorCodes { errorCodes.putAll(clientSessionCodes); } + @Nullable + public static String getErrorDescription(@Nullable Integer errorCode) { + return errorCode == null ? null : errorCodes.get(errorCode); + } + public static boolean isDataIntegrityViolationCode(@Nullable Integer errorCode) { return errorCode != null && dataIntegrityViolationCodes.containsKey(errorCode); } + /** + * @param exception can be {@literal null}. + * @return + * @since 3.3 + */ + public static boolean isDataIntegrityViolationError(@Nullable Exception exception) { + + if(exception instanceof MongoException) { + return isDataIntegrityViolationCode(((MongoException) exception).getCode()); + } + return false; + } + public static boolean isDataAccessResourceFailureCode(@Nullable Integer errorCode) { return errorCode != null && dataAccessResourceFailureCodes.containsKey(errorCode); } + /** + * @param exception can be {@literal null}. + * @return + * @since 3.3 + */ + public static boolean isDataAccessResourceError(@Nullable Exception exception) { + + if(exception instanceof MongoException) { + return isDataAccessResourceFailureCode(((MongoException) exception).getCode()); + } + return false; + } + public static boolean isDuplicateKeyCode(@Nullable Integer errorCode) { return errorCode != null && duplicateKeyCodes.containsKey(errorCode); } + /** + * @param exception can be {@literal null}. + * @return + * @since 3.3 + */ + public static boolean isDuplicateKeyError(@Nullable Exception exception) { + + if(exception instanceof MongoException) { + return isDuplicateKeyCode(((MongoException) exception).getCode()); + } + return false; + } + + /** + * @param exception can be {@literal null}. + * @return + * @since 3.3 + */ + public static boolean isDataDuplicateKeyError(@Nullable Exception exception) { + + if(exception instanceof MongoException) { + return isDuplicateKeyCode(((MongoException) exception).getCode()); + } + return false; + } + public static boolean isPermissionDeniedCode(@Nullable Integer errorCode) { return errorCode != null && permissionDeniedCodes.containsKey(errorCode); } + /** + * @param exception can be {@literal null}. + * @return + * @since 3.3 + */ + public static boolean isPermissionDeniedError(@Nullable Exception exception) { + + if(exception instanceof MongoException) { + return isPermissionDeniedCode(((MongoException) exception).getCode()); + } + return false; + } + public static boolean isInvalidDataAccessApiUsageCode(@Nullable Integer errorCode) { return errorCode != null && invalidDataAccessApiUsageException.containsKey(errorCode); } - @Nullable - public static String getErrorDescription(@Nullable Integer errorCode) { - return errorCode == null ? null : errorCodes.get(errorCode); + /** + * @param exception can be {@literal null}. + * @return + * @since 3.3 + */ + public static boolean isInvalidDataAccessApiUsageError(@Nullable Exception exception) { + + if(exception instanceof MongoException) { + return isInvalidDataAccessApiUsageCode(((MongoException) exception).getCode()); + } + return false; } /** @@ -182,4 +261,17 @@ public static boolean isClientSessionFailureCode(@Nullable Integer errorCode) { public static boolean isTransactionFailureCode(@Nullable Integer errorCode) { return errorCode != null && transactionCodes.containsKey(errorCode); } + + /** + * @param exception can be {@literal null}. + * @return + * @since 3.3 + */ + public static boolean isClientSessionFailure(@Nullable Exception exception) { + + if(exception instanceof MongoException) { + return isClientSessionFailureCode(((MongoException) exception).getCode()); + } + return false; + } } 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 ff74786cb4..bfffa4afa0 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 @@ -25,11 +25,13 @@ import org.springframework.core.NestedRuntimeException; import org.springframework.dao.DataAccessException; import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.dao.DuplicateKeyException; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.dao.InvalidDataAccessResourceUsageException; import org.springframework.data.mongodb.ClientSessionException; import org.springframework.data.mongodb.MongoTransactionException; +import org.springframework.data.mongodb.TransientMongoDbException; import org.springframework.data.mongodb.UncategorizedMongoDbException; import org.springframework.lang.Nullable; @@ -39,7 +41,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}. @@ -80,15 +84,13 @@ void translateSocketException() { void translateSocketExceptionSubclasses() { expectExceptionWithCauseMessage( - translator.translateExceptionIfPossible( - new MongoSocketWriteException("intermediate message", new ServerAddress(), new Exception(EXCEPTION_MESSAGE)) - ), + translator.translateExceptionIfPossible(new MongoSocketWriteException("intermediate message", + new ServerAddress(), new Exception(EXCEPTION_MESSAGE))), DataAccessResourceFailureException.class, EXCEPTION_MESSAGE); expectExceptionWithCauseMessage( - translator.translateExceptionIfPossible( - new MongoSocketReadTimeoutException("intermediate message", new ServerAddress(), new Exception(EXCEPTION_MESSAGE)) - ), + translator.translateExceptionIfPossible(new MongoSocketReadTimeoutException("intermediate message", + new ServerAddress(), new Exception(EXCEPTION_MESSAGE))), DataAccessResourceFailureException.class, EXCEPTION_MESSAGE); } @@ -172,6 +174,38 @@ void translateTransactionExceptions() { checkTranslatedMongoException(MongoTransactionException.class, 267); } + @Test // DATAMONGO-2073 + public void translateTransientTransactionExceptions() { + + MongoException source = new MongoException(267, "PreparedTransactionInProgress"); + source.addLabel(MongoException.TRANSIENT_TRANSACTION_ERROR_LABEL); + + expectExceptionWithCauseMessage(translator.translateExceptionIfPossible(source), TransientMongoDbException.class, + "PreparedTransactionInProgress"); + } + + @Test // DATAMONGO-2073 + public void translateMongoExceptionWithTransientLabelToTransientMongoDbException() { + + MongoException exception = new MongoException(0, ""); + exception.addLabel(MongoException.UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL); + DataAccessException translatedException = translator.translateExceptionIfPossible(exception); + + expectExceptionWithCauseMessage(translatedException, TransientMongoDbException.class); + } + + @Test // DATAMONGO-2073 + public void wrapsTranslatedExceptionsWhenTransientLabelPresent() { + + MongoException exception = new MongoWriteException(new WriteError(112, "WriteConflict", new BsonDocument()), null); + exception.addLabel(MongoException.UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL); + + DataAccessException translatedException = translator.translateExceptionIfPossible(exception); + + assertThat(translatedException).isInstanceOf(TransientMongoDbException.class); + assertThat(translatedException.getCause()).isInstanceOf(DataIntegrityViolationException.class); + } + private void checkTranslatedMongoException(Class clazz, int code) { DataAccessException translated = translator.translateExceptionIfPossible(new MongoException(code, "")); From c0b82fc933720ca9d8e786af489d4cc1c02bae1e Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 30 Aug 2024 10:38:12 +0200 Subject: [PATCH 3/5] Polishing. --- .../TransientClientSessionException.java | 9 ++- .../mongodb/TransientMongoDbException.java | 9 ++- .../core/MongoExceptionTranslator.java | 22 ++----- .../data/mongodb/util/MongoDbErrorCodes.java | 61 +++++++++---------- 4 files changed, 43 insertions(+), 58 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransientClientSessionException.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransientClientSessionException.java index fd16ae97ec..153240a79c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransientClientSessionException.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransientClientSessionException.java @@ -16,24 +16,23 @@ package org.springframework.data.mongodb; import org.springframework.dao.TransientDataAccessException; -import org.springframework.lang.Nullable; /** * {@link TransientDataAccessException} specific to MongoDB {@link com.mongodb.session.ClientSession} related data * access failures such as reading data using an already closed session. * * @author Christoph Strobl - * @since 3.3 + * @since 4.4 */ public class TransientClientSessionException extends TransientMongoDbException { /** * Constructor for {@link TransientClientSessionException}. * - * @param msg the detail message. Can be {@literal null}. - * @param cause the root cause. Can be {@literal null}. + * @param msg the detail message. + * @param cause the root cause. */ - public TransientClientSessionException(@Nullable String msg, @Nullable Throwable cause) { + public TransientClientSessionException(String msg, Throwable cause) { super(msg, cause); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransientMongoDbException.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransientMongoDbException.java index 2a253c2b37..af3bd0d326 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransientMongoDbException.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransientMongoDbException.java @@ -16,7 +16,6 @@ package org.springframework.data.mongodb; import org.springframework.dao.TransientDataAccessException; -import org.springframework.lang.Nullable; /** * Root of the hierarchy of MongoDB specific data access exceptions that are considered transient such as @@ -24,17 +23,17 @@ * specific labels}. * * @author Christoph Strobl - * @since 3.3 + * @since 4.4 */ public class TransientMongoDbException extends TransientDataAccessException { /** * Constructor for {@link TransientMongoDbException}. * - * @param msg the detail message. Can be {@literal null}. - * @param cause the root cause. Can be {@literal null}. + * @param msg the detail message. + * @param cause the root cause. */ - public TransientMongoDbException(String msg, @Nullable Throwable cause) { + public TransientMongoDbException(String msg, Throwable cause) { super(msg, cause); } } 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 bf09363ef2..8ce27e7898 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 @@ -18,6 +18,7 @@ import java.util.Set; import org.bson.BsonInvalidOperationException; + import org.springframework.dao.DataAccessException; import org.springframework.dao.DataAccessResourceFailureException; import org.springframework.dao.DataIntegrityViolationException; @@ -25,11 +26,9 @@ import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.dao.InvalidDataAccessResourceUsageException; import org.springframework.dao.PermissionDeniedDataAccessException; -import org.springframework.dao.TransientDataAccessException; import org.springframework.dao.support.PersistenceExceptionTranslator; import org.springframework.data.mongodb.ClientSessionException; import org.springframework.data.mongodb.TransientClientSessionException; -import org.springframework.data.mongodb.TransientMongoDbException; import org.springframework.data.mongodb.UncategorizedMongoDbException; import org.springframework.data.mongodb.util.MongoDbErrorCodes; import org.springframework.lang.Nullable; @@ -70,18 +69,7 @@ public class MongoExceptionTranslator implements PersistenceExceptionTranslator @Override @Nullable public DataAccessException translateExceptionIfPossible(RuntimeException ex) { - - DataAccessException translatedException = doTranslateException(ex); - if (translatedException == null) { - return null; - } - - // Translated exceptions that per se are not be recoverable (eg. WriteConflicts), might still be transient inside a - // transaction. Let's wrap those. - return (isTransientFailure(ex) && !(translatedException instanceof TransientDataAccessException)) - ? new TransientMongoDbException(ex.getMessage(), translatedException) - : translatedException; - + return doTranslateException(ex); } @Nullable @@ -187,13 +175,13 @@ DataAccessException doTranslateException(RuntimeException ex) { /** * Check if a given exception holds an error label indicating a transient failure. * - * @param e + * @param e the exception to inspect. * @return {@literal true} if the given {@link Exception} is a {@link MongoException} holding one of the transient * exception error labels. * @see MongoException#hasErrorLabel(String) - * @since 3.3 + * @since 4.4 */ - public static boolean isTransientFailure(Exception e) { + public boolean isTransientFailure(Exception e) { if (!(e instanceof MongoException)) { return false; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoDbErrorCodes.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoDbErrorCodes.java index 2b7ccde4d5..30cee7b950 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoDbErrorCodes.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoDbErrorCodes.java @@ -128,7 +128,10 @@ public final class MongoDbErrorCodes { clientSessionCodes.put(263, "OperationNotSupportedInTransaction"); clientSessionCodes.put(264, "TooManyLogicalSessions"); - errorCodes = new HashMap<>(); + errorCodes = new HashMap<>( + dataAccessResourceFailureCodes.size() + dataIntegrityViolationCodes.size() + duplicateKeyCodes.size() + + invalidDataAccessApiUsageException.size() + permissionDeniedCodes.size() + clientSessionCodes.size(), + 1f); errorCodes.putAll(dataAccessResourceFailureCodes); errorCodes.putAll(dataIntegrityViolationCodes); errorCodes.putAll(duplicateKeyCodes); @@ -149,12 +152,12 @@ public static boolean isDataIntegrityViolationCode(@Nullable Integer errorCode) /** * @param exception can be {@literal null}. * @return - * @since 3.3 + * @since 4.4 */ - public static boolean isDataIntegrityViolationError(@Nullable Exception exception) { + public static boolean isDataIntegrityViolationError(Exception exception) { - if(exception instanceof MongoException) { - return isDataIntegrityViolationCode(((MongoException) exception).getCode()); + if (exception instanceof MongoException me) { + return isDataIntegrityViolationCode(me.getCode()); } return false; } @@ -166,12 +169,12 @@ public static boolean isDataAccessResourceFailureCode(@Nullable Integer errorCod /** * @param exception can be {@literal null}. * @return - * @since 3.3 + * @since 4.4 */ - public static boolean isDataAccessResourceError(@Nullable Exception exception) { + public static boolean isDataAccessResourceError(Exception exception) { - if(exception instanceof MongoException) { - return isDataAccessResourceFailureCode(((MongoException) exception).getCode()); + if (exception instanceof MongoException me) { + return isDataAccessResourceFailureCode(me.getCode()); } return false; } @@ -183,12 +186,12 @@ public static boolean isDuplicateKeyCode(@Nullable Integer errorCode) { /** * @param exception can be {@literal null}. * @return - * @since 3.3 + * @since 4.4 */ - public static boolean isDuplicateKeyError(@Nullable Exception exception) { + public static boolean isDuplicateKeyError(Exception exception) { - if(exception instanceof MongoException) { - return isDuplicateKeyCode(((MongoException) exception).getCode()); + if (exception instanceof MongoException me) { + return isDuplicateKeyCode(me.getCode()); } return false; } @@ -196,14 +199,10 @@ public static boolean isDuplicateKeyError(@Nullable Exception exception) { /** * @param exception can be {@literal null}. * @return - * @since 3.3 + * @since 4.4 */ - public static boolean isDataDuplicateKeyError(@Nullable Exception exception) { - - if(exception instanceof MongoException) { - return isDuplicateKeyCode(((MongoException) exception).getCode()); - } - return false; + public static boolean isDataDuplicateKeyError(Exception exception) { + return isDuplicateKeyError(exception); } public static boolean isPermissionDeniedCode(@Nullable Integer errorCode) { @@ -213,11 +212,11 @@ public static boolean isPermissionDeniedCode(@Nullable Integer errorCode) { /** * @param exception can be {@literal null}. * @return - * @since 3.3 + * @since 4.4 */ - public static boolean isPermissionDeniedError(@Nullable Exception exception) { + public static boolean isPermissionDeniedError(Exception exception) { - if(exception instanceof MongoException) { + if (exception instanceof MongoException) { return isPermissionDeniedCode(((MongoException) exception).getCode()); } return false; @@ -230,12 +229,12 @@ public static boolean isInvalidDataAccessApiUsageCode(@Nullable Integer errorCod /** * @param exception can be {@literal null}. * @return - * @since 3.3 + * @since 4.4 */ - public static boolean isInvalidDataAccessApiUsageError(@Nullable Exception exception) { + public static boolean isInvalidDataAccessApiUsageError(Exception exception) { - if(exception instanceof MongoException) { - return isInvalidDataAccessApiUsageCode(((MongoException) exception).getCode()); + if (exception instanceof MongoException me) { + return isInvalidDataAccessApiUsageCode(me.getCode()); } return false; } @@ -265,12 +264,12 @@ public static boolean isTransactionFailureCode(@Nullable Integer errorCode) { /** * @param exception can be {@literal null}. * @return - * @since 3.3 + * @since 4.4 */ - public static boolean isClientSessionFailure(@Nullable Exception exception) { + public static boolean isClientSessionFailure(Exception exception) { - if(exception instanceof MongoException) { - return isClientSessionFailureCode(((MongoException) exception).getCode()); + if (exception instanceof MongoException me) { + return isClientSessionFailureCode(me.getCode()); } return false; } From 3dd86c1ab8a6391490e6af7fb784a2a45869ef6a Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 30 Aug 2024 11:10:34 +0200 Subject: [PATCH 4/5] Allow configuration of ExceptionTranslator in MongoDatabaseFactorySupport. --- .../mongodb/core/MongoClientFactoryBean.java | 33 ++++++++++--------- .../core/MongoDatabaseFactorySupport.java | 28 +++++++++++----- .../core/MongoExceptionTranslator.java | 6 ++-- .../core/ReactiveMongoClientFactoryBean.java | 16 ++++----- .../SimpleMongoClientDatabaseFactory.java | 2 +- .../SimpleReactiveMongoDatabaseFactory.java | 26 +++++++++++---- .../MongoExceptionTranslatorUnitTests.java | 25 +++----------- 7 files changed, 73 insertions(+), 63 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientFactoryBean.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientFactoryBean.java index 64a12e9c0f..231a4df4a4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientFactoryBean.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientFactoryBean.java @@ -55,8 +55,6 @@ */ public class MongoClientFactoryBean extends AbstractFactoryBean implements PersistenceExceptionTranslator { - private static final PersistenceExceptionTranslator DEFAULT_EXCEPTION_TRANSLATOR = new MongoExceptionTranslator(); - private @Nullable MongoClientSettings mongoClientSettings; private @Nullable String host; private @Nullable Integer port; @@ -64,7 +62,7 @@ public class MongoClientFactoryBean extends AbstractFactoryBean imp private @Nullable ConnectionString connectionString; private @Nullable String replicaSet = null; - private PersistenceExceptionTranslator exceptionTranslator = DEFAULT_EXCEPTION_TRANSLATOR; + private PersistenceExceptionTranslator exceptionTranslator = MongoExceptionTranslator.DEFAULT_EXCEPTION_TRANSLATOR; /** * Set the {@link MongoClientSettings} to be used when creating {@link MongoClient}. @@ -116,23 +114,34 @@ public void setReplicaSet(@Nullable String replicaSet) { * @param exceptionTranslator */ public void setExceptionTranslator(@Nullable PersistenceExceptionTranslator exceptionTranslator) { - this.exceptionTranslator = exceptionTranslator == null ? DEFAULT_EXCEPTION_TRANSLATOR : exceptionTranslator; - } - - public Class getObjectType() { - return MongoClient.class; + this.exceptionTranslator = exceptionTranslator == null ? MongoExceptionTranslator.DEFAULT_EXCEPTION_TRANSLATOR + : exceptionTranslator; } + @Override @Nullable public DataAccessException translateExceptionIfPossible(RuntimeException ex) { return exceptionTranslator.translateExceptionIfPossible(ex); } + @Override + public Class getObjectType() { + return MongoClient.class; + } + @Override protected MongoClient createInstance() throws Exception { return createMongoClient(computeClientSetting()); } + @Override + protected void destroyInstance(@Nullable MongoClient instance) throws Exception { + + if (instance != null) { + instance.close(); + } + } + /** * Create {@link MongoClientSettings} based on configuration and priority (lower is better). *
    @@ -324,14 +333,6 @@ private T computeSettingsValue(T defaultValue, T fromSettings, T fromConnect return !fromConnectionStringIsDefault ? fromConnectionString : defaultValue; } - @Override - protected void destroyInstance(@Nullable MongoClient instance) throws Exception { - - if (instance != null) { - instance.close(); - } - } - private MongoClient createMongoClient(MongoClientSettings settings) throws UnknownHostException { return MongoClients.create(settings, SpringDataMongoDB.driverInformation()); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoDatabaseFactorySupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoDatabaseFactorySupport.java index 7e363632df..a73b426dc1 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoDatabaseFactorySupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoDatabaseFactorySupport.java @@ -32,8 +32,7 @@ /** * Common base class for usage with both {@link com.mongodb.client.MongoClients} defining common properties such as - * database name and exception translator. - *
    + * database name and exception translator.
    * Not intended to be used directly. * * @author Christoph Strobl @@ -47,8 +46,8 @@ public abstract class MongoDatabaseFactorySupport implements MongoDatabaseFac private final C mongoClient; private final String databaseName; private final boolean mongoInstanceCreated; - private final PersistenceExceptionTranslator exceptionTranslator; + private PersistenceExceptionTranslator exceptionTranslator; private @Nullable WriteConcern writeConcern; /** @@ -75,15 +74,31 @@ protected MongoDatabaseFactorySupport(C mongoClient, String databaseName, boolea this.exceptionTranslator = exceptionTranslator; } + /** + * Configures the {@link PersistenceExceptionTranslator} to be used. + * + * @param exceptionTranslator the exception translator to set. + * @since 4.4 + */ + public void setExceptionTranslator(PersistenceExceptionTranslator exceptionTranslator) { + this.exceptionTranslator = exceptionTranslator; + } + + @Override + public PersistenceExceptionTranslator getExceptionTranslator() { + return this.exceptionTranslator; + } + /** * Configures the {@link WriteConcern} to be used on the {@link MongoDatabase} instance being created. * - * @param writeConcern the writeConcern to set + * @param writeConcern the writeConcern to set. */ public void setWriteConcern(WriteConcern writeConcern) { this.writeConcern = writeConcern; } + @Override public MongoDatabase getMongoDatabase() throws DataAccessException { return getMongoDatabase(getDefaultDatabaseName()); } @@ -116,10 +131,7 @@ public void destroy() throws Exception { } } - public PersistenceExceptionTranslator getExceptionTranslator() { - return this.exceptionTranslator; - } - + @Override public MongoDatabaseFactory withSession(ClientSession session) { return new MongoDatabaseFactorySupport.ClientSessionBoundMongoDbFactory(session, this); } 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 8ce27e7898..8fa4503058 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 @@ -52,6 +52,8 @@ */ public class MongoExceptionTranslator implements PersistenceExceptionTranslator { + public static final MongoExceptionTranslator DEFAULT_EXCEPTION_TRANSLATOR = new MongoExceptionTranslator(); + private static final Set DUPLICATE_KEY_EXCEPTIONS = Set.of("MongoException.DuplicateKey", "DuplicateKeyException"); @@ -183,12 +185,10 @@ DataAccessException doTranslateException(RuntimeException ex) { */ public boolean isTransientFailure(Exception e) { - if (!(e instanceof MongoException)) { + if (!(e instanceof MongoException mongoException)) { return false; } - MongoException mongoException = (MongoException) e; - return mongoException.hasErrorLabel(MongoException.TRANSIENT_TRANSACTION_ERROR_LABEL) || mongoException.hasErrorLabel(MongoException.UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoClientFactoryBean.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoClientFactoryBean.java index f7755773d9..615599de36 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoClientFactoryBean.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoClientFactoryBean.java @@ -36,13 +36,11 @@ public class ReactiveMongoClientFactoryBean extends AbstractFactoryBean implements PersistenceExceptionTranslator { - private static final PersistenceExceptionTranslator DEFAULT_EXCEPTION_TRANSLATOR = new MongoExceptionTranslator(); - private @Nullable String connectionString; private @Nullable String host; private @Nullable Integer port; private @Nullable MongoClientSettings mongoClientSettings; - private PersistenceExceptionTranslator exceptionTranslator = DEFAULT_EXCEPTION_TRANSLATOR; + private PersistenceExceptionTranslator exceptionTranslator = MongoExceptionTranslator.DEFAULT_EXCEPTION_TRANSLATOR; /** * Configures the host to connect to. @@ -86,7 +84,13 @@ public void setMongoClientSettings(@Nullable MongoClientSettings mongoClientSett * @param exceptionTranslator */ public void setExceptionTranslator(@Nullable PersistenceExceptionTranslator exceptionTranslator) { - this.exceptionTranslator = exceptionTranslator == null ? DEFAULT_EXCEPTION_TRANSLATOR : exceptionTranslator; + this.exceptionTranslator = exceptionTranslator == null ? MongoExceptionTranslator.DEFAULT_EXCEPTION_TRANSLATOR + : exceptionTranslator; + } + + @Override + public DataAccessException translateExceptionIfPossible(RuntimeException ex) { + return exceptionTranslator.translateExceptionIfPossible(ex); } @Override @@ -123,8 +127,4 @@ protected void destroyInstance(@Nullable MongoClient instance) throws Exception instance.close(); } - @Override - public DataAccessException translateExceptionIfPossible(RuntimeException ex) { - return exceptionTranslator.translateExceptionIfPossible(ex); - } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SimpleMongoClientDatabaseFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SimpleMongoClientDatabaseFactory.java index e1e77c75e9..6d61d8a8b4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SimpleMongoClientDatabaseFactory.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SimpleMongoClientDatabaseFactory.java @@ -72,7 +72,7 @@ public SimpleMongoClientDatabaseFactory(MongoClient mongoClient, String database * @param mongoInstanceCreated */ SimpleMongoClientDatabaseFactory(MongoClient mongoClient, String databaseName, boolean mongoInstanceCreated) { - super(mongoClient, databaseName, mongoInstanceCreated, new MongoExceptionTranslator()); + super(mongoClient, databaseName, mongoInstanceCreated, MongoExceptionTranslator.DEFAULT_EXCEPTION_TRANSLATOR); } @Override diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SimpleReactiveMongoDatabaseFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SimpleReactiveMongoDatabaseFactory.java index 65e97831e4..d3a3e1556a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SimpleReactiveMongoDatabaseFactory.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SimpleReactiveMongoDatabaseFactory.java @@ -51,8 +51,7 @@ public class SimpleReactiveMongoDatabaseFactory implements DisposableBean, React private final String databaseName; private final boolean mongoInstanceCreated; - private final PersistenceExceptionTranslator exceptionTranslator; - + private PersistenceExceptionTranslator exceptionTranslator = MongoExceptionTranslator.DEFAULT_EXCEPTION_TRANSLATOR; private @Nullable WriteConcern writeConcern; /** @@ -85,7 +84,21 @@ private SimpleReactiveMongoDatabaseFactory(MongoClient client, String databaseNa this.mongo = client; this.databaseName = databaseName; this.mongoInstanceCreated = mongoInstanceCreated; - this.exceptionTranslator = new MongoExceptionTranslator(); + } + + /** + * Configures the {@link PersistenceExceptionTranslator} to be used. + * + * @param exceptionTranslator the exception translator to set. + * @since 4.4 + */ + public void setExceptionTranslator(PersistenceExceptionTranslator exceptionTranslator) { + this.exceptionTranslator = exceptionTranslator; + } + + @Override + public PersistenceExceptionTranslator getExceptionTranslator() { + return this.exceptionTranslator; } /** @@ -97,10 +110,12 @@ public void setWriteConcern(WriteConcern writeConcern) { this.writeConcern = writeConcern; } + @Override public Mono getMongoDatabase() throws DataAccessException { return getMongoDatabase(databaseName); } + @Override public Mono getMongoDatabase(String dbName) throws DataAccessException { Assert.hasText(dbName, "Database name must not be empty"); @@ -118,6 +133,7 @@ public Mono getMongoDatabase(String dbName) throws DataAccessExce * * @see DisposableBean#destroy() */ + @Override public void destroy() throws Exception { if (mongoInstanceCreated) { @@ -125,10 +141,6 @@ public void destroy() throws Exception { } } - public PersistenceExceptionTranslator getExceptionTranslator() { - return this.exceptionTranslator; - } - @Override public CodecRegistry getCodecRegistry() { return this.mongo.getDatabase(databaseName).getCodecRegistry(); 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 bfffa4afa0..908e128e43 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 @@ -20,18 +20,16 @@ import org.bson.BsonDocument; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; - import org.mockito.Mockito; + import org.springframework.core.NestedRuntimeException; import org.springframework.dao.DataAccessException; import org.springframework.dao.DataAccessResourceFailureException; -import org.springframework.dao.DataIntegrityViolationException; import org.springframework.dao.DuplicateKeyException; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.dao.InvalidDataAccessResourceUsageException; import org.springframework.data.mongodb.ClientSessionException; import org.springframework.data.mongodb.MongoTransactionException; -import org.springframework.data.mongodb.TransientMongoDbException; import org.springframework.data.mongodb.UncategorizedMongoDbException; import org.springframework.lang.Nullable; @@ -41,9 +39,7 @@ 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}. @@ -180,30 +176,19 @@ public void translateTransientTransactionExceptions() { MongoException source = new MongoException(267, "PreparedTransactionInProgress"); source.addLabel(MongoException.TRANSIENT_TRANSACTION_ERROR_LABEL); - expectExceptionWithCauseMessage(translator.translateExceptionIfPossible(source), TransientMongoDbException.class, + expectExceptionWithCauseMessage(translator.translateExceptionIfPossible(source), + UncategorizedMongoDbException.class, "PreparedTransactionInProgress"); } @Test // DATAMONGO-2073 - public void translateMongoExceptionWithTransientLabelToTransientMongoDbException() { + public void translateMongoExceptionWithTransientLabel() { MongoException exception = new MongoException(0, ""); exception.addLabel(MongoException.UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL); DataAccessException translatedException = translator.translateExceptionIfPossible(exception); - expectExceptionWithCauseMessage(translatedException, TransientMongoDbException.class); - } - - @Test // DATAMONGO-2073 - public void wrapsTranslatedExceptionsWhenTransientLabelPresent() { - - MongoException exception = new MongoWriteException(new WriteError(112, "WriteConflict", new BsonDocument()), null); - exception.addLabel(MongoException.UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL); - - DataAccessException translatedException = translator.translateExceptionIfPossible(exception); - - assertThat(translatedException).isInstanceOf(TransientMongoDbException.class); - assertThat(translatedException.getCause()).isInstanceOf(DataIntegrityViolationException.class); + expectExceptionWithCauseMessage(translatedException, UncategorizedMongoDbException.class); } private void checkTranslatedMongoException(Class clazz, int code) { From 31fe900442f093b9217ebf8c008fdd9c3cac1cd2 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 3 Sep 2024 15:22:37 +0200 Subject: [PATCH 5/5] Add documentation. --- .../ROOT/pages/mongodb/template-api.adoc | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/src/main/antora/modules/ROOT/pages/mongodb/template-api.adoc b/src/main/antora/modules/ROOT/pages/mongodb/template-api.adoc index ab34706180..f2a7a19bd6 100644 --- a/src/main/antora/modules/ROOT/pages/mongodb/template-api.adoc +++ b/src/main/antora/modules/ROOT/pages/mongodb/template-api.adoc @@ -31,7 +31,8 @@ The `execute` callbacks gives you a reference to either a `MongoCollection` or a * ` T` *execute* `(String collectionName, CollectionCallback action)`: Runs the given `CollectionCallback` on the collection of the given name. -* ` T` *execute* `(DbCallback action)`: Runs a DbCallback, translating any exceptions as necessary. Spring Data MongoDB provides support for the Aggregation Framework introduced to MongoDB in version 2.2. +* ` T` *execute* `(DbCallback action)`: Runs a DbCallback, translating any exceptions as necessary. +Spring Data MongoDB provides support for the Aggregation Framework introduced to MongoDB in version 2.2. * ` T` *execute* `(String collectionName, DbCallback action)`: Runs a `DbCallback` on the collection of the given name translating any exceptions as necessary. @@ -90,6 +91,7 @@ List all = template.query(SWCharacter.class) <1> .matching(query(where("jedi").is(true))) <4> .all(); ---- + <1> The type used to map fields used in the query to. <2> The collection name to use if not defined on the domain type. <3> Result type if not using the original domain type. @@ -107,9 +109,8 @@ Flux all = template.query(SWCharacter.class) ---- ====== -NOTE: Using projections allows `MongoTemplate` to optimize result mapping by limiting the actual response to fields required -by the projection target type. This applies as long as the javadoc:org.springframework.data.mongodb.core.query.Query[] itself does not contain any field restriction and the -target type is a closed interface or DTO projection. +NOTE: Using projections allows `MongoTemplate` to optimize result mapping by limiting the actual response to fields required by the projection target type. +This applies as long as the javadoc:org.springframework.data.mongodb.core.query.Query[] itself does not contain any field restriction and the target type is a closed interface or DTO projection. WARNING: Projections must not be applied to xref:mongodb/mapping/document-references.adoc[DBRefs]. @@ -143,8 +144,8 @@ Flux> results = template.query(SWCharacter.class) [[mongo-template.exception-translation]] == Exception Translation -The Spring framework provides exception translation for a wide variety of database and mapping technologies. T -his has traditionally been for JDBC and JPA. +The Spring framework provides exception translation for a wide variety of database and mapping technologies. +This has traditionally been for JDBC and JPA. The Spring support for MongoDB extends this feature to the MongoDB Database by providing an implementation of the `org.springframework.dao.support.PersistenceExceptionTranslator` interface. The motivation behind mapping to Spring's link:{springDocsUrl}/data-access.html#dao-exceptions[consistent data access exception hierarchy] is that you are then able to write portable and descriptive exception handling code without resorting to coding against MongoDB error codes. @@ -152,9 +153,25 @@ All of Spring's data access exceptions are inherited from the root `DataAccessEx Note that not all exceptions thrown by the MongoDB driver inherit from the `MongoException` class. The inner exception and message are preserved so that no information is lost. -Some of the mappings performed by the `MongoExceptionTranslator` are `com.mongodb.Network to DataAccessResourceFailureException` and `MongoException` error codes 1003, 12001, 12010, 12011, and 12012 to `InvalidDataAccessApiUsageException`. +Some of the mappings performed by the javadoc:org.springframework.data.mongodb.core.MongoExceptionTranslator[] are `com.mongodb.Network` to `DataAccessResourceFailureException` and `MongoException` error codes 1003, 12001, 12010, 12011, and 12012 to `InvalidDataAccessApiUsageException`. Look into the implementation for more details on the mapping. +Exception Translation can be configured by setting a customized javadoc:org.springframework.data.mongodb.core.MongoExceptionTranslator[] on your `MongoDatabaseFactory` or its reactive variant. +You might also want to set the exception translator on the corresponding `MongoClientFactoryBean`. + +.Configuring `MongoExceptionTranslator` +==== +[source,java] +---- +ConnectionString uri = new ConnectionString("mongodb://username:password@localhost/database"); +SimpleMongoClientDatabaseFactory mongoDbFactory = new SimpleMongoClientDatabaseFactory(uri); +mongoDbFactory.setExceptionTranslator(myCustomExceptionTranslator); +---- +==== + +A motivation to customize exception can be MongoDB's behavior during transactions where some failures (such as write conflicts) can become transient and where a retry could lead to a successful operation. +In such a case, you could wrap exceptions with a specific MongoDB label and apply a different exception translation stragegy. + [[mongo-template.type-mapping]] == Domain Type Mapping