Compare commits

...

3 Commits

Author SHA1 Message Date
Christoph Strobl
7e7c37d646 Don't forget to translate WriteErrors to DataIntegrityViolationException.
Should not have been an UncategorizedMongoDbException in first place.
2021-05-25 11:37:55 +02:00
Christoph Strobl
9a2bc7d004 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
2021-05-25 10:03:31 +02:00
Christoph Strobl
85d48f054c DATAMONGO-2073 - Prepare issue branch. 2021-05-25 09:49:55 +02:00
11 changed files with 291 additions and 51 deletions

View File

@@ -5,7 +5,7 @@
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>3.3.0-SNAPSHOT</version>
<version>3.3.0.DATAMONGO-2073-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Spring Data MongoDB</name>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>3.3.0-SNAPSHOT</version>
<version>3.3.0.DATAMONGO-2073-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@@ -14,7 +14,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>3.3.0-SNAPSHOT</version>
<version>3.3.0.DATAMONGO-2073-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@@ -11,7 +11,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>3.3.0-SNAPSHOT</version>
<version>3.3.0.DATAMONGO-2073-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -21,7 +21,6 @@ import java.util.HashSet;
import java.util.Set;
import org.bson.BsonInvalidOperationException;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.DataAccessResourceFailureException;
import org.springframework.dao.DataIntegrityViolationException;
@@ -29,9 +28,11 @@ import org.springframework.dao.DuplicateKeyException;
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;
@@ -39,7 +40,6 @@ import org.springframework.util.ClassUtils;
import com.mongodb.MongoBulkWriteException;
import com.mongodb.MongoException;
import com.mongodb.MongoServerException;
import com.mongodb.MongoSocketException;
import com.mongodb.bulk.BulkWriteError;
@@ -75,6 +75,22 @@ public class MongoExceptionTranslator implements PersistenceExceptionTranslator
@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) {
@@ -101,13 +117,13 @@ public class MongoExceptionTranslator implements PersistenceExceptionTranslator
if (DATA_INTEGRITY_EXCEPTIONS.contains(exception)) {
if (ex instanceof MongoServerException) {
if (((MongoServerException) ex).getCode() == 11000) {
if (ex instanceof MongoException) {
if (MongoDbErrorCodes.isDataDuplicateKeyError(ex)) {
return new DuplicateKeyException(ex.getMessage(), ex);
}
if (ex instanceof MongoBulkWriteException) {
for (BulkWriteError x : ((MongoBulkWriteException) ex).getWriteErrors()) {
if (x.getCode() == 11000) {
for (BulkWriteError writeError : ((MongoBulkWriteException) ex).getWriteErrors()) {
if (MongoDbErrorCodes.isDuplicateKeyCode(writeError.getCode())) {
return new DuplicateKeyException(ex.getMessage(), ex);
}
}
@@ -120,21 +136,31 @@ public class MongoExceptionTranslator implements PersistenceExceptionTranslator
// All other MongoExceptions
if (ex instanceof MongoException) {
int code = ((MongoException) ex).getCode();
MongoException mongoException = (MongoException) ex;
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) {
return new InvalidDataAccessApiUsageException(ex.getMessage(), ex);
} else if (MongoDbErrorCodes.isPermissionDeniedCode(code)) {
}
{
int code = mongoException.getCode();
if (MongoDbErrorCodes.isInvalidDataAccessApiUsageError(mongoException) || code == 12001 || code == 12010
|| code == 12011 || code == 12012) {
return new InvalidDataAccessApiUsageException(ex.getMessage(), ex);
}
}
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);
}
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);
}
return new UncategorizedMongoDbException(ex.getMessage(), ex);
@@ -155,4 +181,25 @@ public class MongoExceptionTranslator implements PersistenceExceptionTranslator
// 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);
}
}

View File

@@ -27,7 +27,6 @@ import org.springframework.data.mapping.PersistentEntity;
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;
@@ -155,7 +154,7 @@ public class MongoPersistentEntityIndexCreator implements ApplicationListener<Ma
IndexOperations indexOperations = indexOperationsProvider.indexOps(indexDefinition.getCollection());
indexOperations.ensureIndex(indexDefinition);
} catch (UncategorizedMongoDbException ex) {
} catch (DataIntegrityViolationException ex) {
if (ex.getCause() instanceof MongoException
&& MongoDbErrorCodes.isDataIntegrityViolationCode(((MongoException) ex.getCause()).getCode())) {

View File

@@ -23,10 +23,10 @@ import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;
@@ -117,7 +117,7 @@ class IndexEnsuringQueryCreationListener implements QueryCreationListener<PartTr
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) {

View File

@@ -19,6 +19,8 @@ import java.util.HashMap;
import org.springframework.lang.Nullable;
import com.mongodb.MongoException;
/**
* {@link MongoDbErrorCodes} holds MongoDB specific error codes outlined in {@literal mongo/base/error_codes.err}.
*
@@ -34,7 +36,6 @@ public final class MongoDbErrorCodes {
static HashMap<Integer, String> invalidDataAccessApiUsageExeption;
static HashMap<Integer, String> permissionDeniedCodes;
static HashMap<Integer, String> clientSessionCodes;
static HashMap<Integer, String> transactionCodes;
static HashMap<Integer, String> errorCodes;
@@ -96,6 +97,7 @@ public final class MongoDbErrorCodes {
invalidDataAccessApiUsageExeption.put(72, "InvalidOptions");
invalidDataAccessApiUsageExeption.put(115, "CommandNotSupported");
invalidDataAccessApiUsageExeption.put(116, "DocTooLargeForCapped");
invalidDataAccessApiUsageExeption.put(10003, "CannotGrowDocumentInCappedNamespace");
invalidDataAccessApiUsageExeption.put(130, "SymbolNotFound");
invalidDataAccessApiUsageExeption.put(17280, "KeyTooLong");
invalidDataAccessApiUsageExeption.put(13334, "ShardKeyTooBig");
@@ -113,18 +115,18 @@ 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");
clientSessionCodes.put(267, "PreparedTransactionInProgress");
clientSessionCodes.put(290, "TransactionExceededLifetimeLimitSeconds");
errorCodes = new HashMap<>();
errorCodes.putAll(dataAccessResourceFailureCodes);
@@ -139,22 +141,100 @@ public final class MongoDbErrorCodes {
return errorCode == null ? false : 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 ? false : 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 ? false : 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 ? false : 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 ? false : invalidDataAccessApiUsageExeption.containsKey(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;
}
public static String getErrorDescription(@Nullable Integer errorCode) {
return errorCode == null ? null : errorCodes.get(errorCode);
}
@@ -171,13 +251,15 @@ public final class MongoDbErrorCodes {
}
/**
* Check if the given error code matches a know transaction related error.
*
* @param errorCode the error code to check.
* @return {@literal true} if error matches.
* @since 2.1
* @param exception can be {@literal null}.
* @return
* @since 3.3
*/
public static boolean isTransactionFailureCode(@Nullable Integer errorCode) {
return errorCode == null ? false : transactionCodes.containsKey(errorCode);
public static boolean isClientSessionFailure(@Nullable Exception exception) {
if(exception instanceof MongoException) {
return isClientSessionFailureCode(((MongoException) exception).getCode());
}
return false;
}
}

View File

@@ -20,15 +20,16 @@ import static org.assertj.core.api.Assertions.*;
import org.bson.BsonDocument;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
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;
@@ -38,7 +39,9 @@ import com.mongodb.MongoInternalException;
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}.
@@ -79,15 +82,13 @@ class MongoExceptionTranslatorUnitTests {
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);
}
@@ -171,6 +172,38 @@ class MongoExceptionTranslatorUnitTests {
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<? extends Exception> clazz, int code) {
DataAccessException translated = translator.translateExceptionIfPossible(new MongoException(code, ""));