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 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..153240a79c --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransientClientSessionException.java @@ -0,0 +1,38 @@ +/* + * 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; + +/** + * {@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 4.4 + */ +public class TransientClientSessionException extends TransientMongoDbException { + + /** + * Constructor for {@link TransientClientSessionException}. + * + * @param msg the detail message. + * @param cause the root 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 new file mode 100644 index 0000000000..af3bd0d326 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransientMongoDbException.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; + +/** + * 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 4.4 + */ +public class TransientMongoDbException extends TransientDataAccessException { + + /** + * Constructor for {@link TransientMongoDbException}. + * + * @param msg the detail message. + * @param cause the root cause. + */ + public TransientMongoDbException(String msg, Throwable cause) { + super(msg, cause); + } +} 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 4775a4a4d2..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 @@ -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; @@ -27,7 +28,7 @@ import org.springframework.dao.PermissionDeniedDataAccessException; 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.UncategorizedMongoDbException; import org.springframework.data.mongodb.util.MongoDbErrorCodes; import org.springframework.lang.Nullable; @@ -51,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"); @@ -65,8 +68,14 @@ public class MongoExceptionTranslator implements PersistenceExceptionTranslator private static final Set SECURITY_EXCEPTIONS = Set.of("MongoCryptException"); + @Override @Nullable public DataAccessException translateExceptionIfPossible(RuntimeException ex) { + return doTranslateException(ex); + } + + @Nullable + DataAccessException doTranslateException(RuntimeException ex) { // Check for well-known MongoException subclasses. @@ -94,13 +103,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 +124,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 +173,23 @@ 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 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 4.4 + */ + public boolean isTransientFailure(Exception e) { + + if (!(e instanceof MongoException mongoException)) { + return false; + } + + 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/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..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 @@ -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,20 +117,21 @@ 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 = new HashMap<>( + dataAccessResourceFailureCodes.size() + dataIntegrityViolationCodes.size() + duplicateKeyCodes.size() + + invalidDataAccessApiUsageException.size() + permissionDeniedCodes.size() + clientSessionCodes.size(), + 1f); errorCodes.putAll(dataAccessResourceFailureCodes); errorCodes.putAll(dataIntegrityViolationCodes); errorCodes.putAll(duplicateKeyCodes); @@ -136,29 +140,103 @@ 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 4.4 + */ + public static boolean isDataIntegrityViolationError(Exception exception) { + + if (exception instanceof MongoException me) { + return isDataIntegrityViolationCode(me.getCode()); + } + return false; + } + public static boolean isDataAccessResourceFailureCode(@Nullable Integer errorCode) { return errorCode != null && dataAccessResourceFailureCodes.containsKey(errorCode); } + /** + * @param exception can be {@literal null}. + * @return + * @since 4.4 + */ + public static boolean isDataAccessResourceError(Exception exception) { + + if (exception instanceof MongoException me) { + return isDataAccessResourceFailureCode(me.getCode()); + } + return false; + } + public static boolean isDuplicateKeyCode(@Nullable Integer errorCode) { return errorCode != null && duplicateKeyCodes.containsKey(errorCode); } + /** + * @param exception can be {@literal null}. + * @return + * @since 4.4 + */ + public static boolean isDuplicateKeyError(Exception exception) { + + if (exception instanceof MongoException me) { + return isDuplicateKeyCode(me.getCode()); + } + return false; + } + + /** + * @param exception can be {@literal null}. + * @return + * @since 4.4 + */ + public static boolean isDataDuplicateKeyError(Exception exception) { + return isDuplicateKeyError(exception); + } + public static boolean isPermissionDeniedCode(@Nullable Integer errorCode) { return errorCode != null && permissionDeniedCodes.containsKey(errorCode); } + /** + * @param exception can be {@literal null}. + * @return + * @since 4.4 + */ + public static boolean isPermissionDeniedError(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 4.4 + */ + public static boolean isInvalidDataAccessApiUsageError(Exception exception) { + + if (exception instanceof MongoException me) { + return isInvalidDataAccessApiUsageCode(me.getCode()); + } + return false; } /** @@ -182,4 +260,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 4.4 + */ + public static boolean isClientSessionFailure(Exception exception) { + + if (exception instanceof MongoException me) { + return isClientSessionFailureCode(me.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..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,8 +20,8 @@ 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; @@ -80,15 +80,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 +170,27 @@ 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), + UncategorizedMongoDbException.class, + "PreparedTransactionInProgress"); + } + + @Test // DATAMONGO-2073 + public void translateMongoExceptionWithTransientLabel() { + + MongoException exception = new MongoException(0, ""); + exception.addLabel(MongoException.UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL); + DataAccessException translatedException = translator.translateExceptionIfPossible(exception); + + expectExceptionWithCauseMessage(translatedException, UncategorizedMongoDbException.class); + } + private void checkTranslatedMongoException(Class clazz, int code) { DataAccessException translated = translator.translateExceptionIfPossible(new MongoException(code, "")); 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