Browse Source

Refine exception translation for client session exceptions.

Closes #3148
See #2939
Original Pull Request: #604
pull/4778/merge
Christoph Strobl 8 years ago committed by Mark Paluch
parent
commit
dced760d9f
No known key found for this signature in database
GPG Key ID: 55BC6374BAA9D973
  1. 39
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransientClientSessionException.java
  2. 40
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransientMongoDbException.java
  3. 77
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoExceptionTranslator.java
  4. 3
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexCreator.java
  5. 4
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/IndexEnsuringQueryCreationListener.java
  6. 118
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoDbErrorCodes.java
  7. 46
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoExceptionTranslatorUnitTests.java

39
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransientClientSessionException.java

@ -0,0 +1,39 @@ @@ -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);
}
}

40
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransientMongoDbException.java

@ -0,0 +1,40 @@ @@ -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);
}
}

77
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoExceptionTranslator.java

@ -25,9 +25,11 @@ import org.springframework.dao.DuplicateKeyException; @@ -25,9 +25,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;
@ -65,9 +67,26 @@ public class MongoExceptionTranslator implements PersistenceExceptionTranslator @@ -65,9 +67,26 @@ public class MongoExceptionTranslator implements PersistenceExceptionTranslator
private static final Set<String> 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 class MongoExceptionTranslator implements PersistenceExceptionTranslator @@ -94,13 +113,13 @@ public class MongoExceptionTranslator implements PersistenceExceptionTranslator
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,27 @@ public class MongoExceptionTranslator implements PersistenceExceptionTranslator @@ -115,20 +134,27 @@ public class MongoExceptionTranslator implements PersistenceExceptionTranslator
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 (ex.getCause() != null && SECURITY_EXCEPTIONS.contains(ClassUtils.getShortName(ex.getCause().getClass()))) {
return new PermissionDeniedDataAccessException(ex.getMessage(), ex);
}
@ -150,4 +176,25 @@ public class MongoExceptionTranslator implements PersistenceExceptionTranslator @@ -150,4 +176,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);
}
}

3
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexCreator.java

@ -28,7 +28,6 @@ import org.springframework.data.mapping.PersistentEntity; @@ -28,7 +28,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;
@ -152,7 +151,7 @@ public class MongoPersistentEntityIndexCreator implements ApplicationListener<Ma @@ -152,7 +151,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 mongoException
&& MongoDbErrorCodes.isDataIntegrityViolationCode(mongoException.getCode())) {

4
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/IndexEnsuringQueryCreationListener.java

@ -21,10 +21,10 @@ import java.util.Set; @@ -21,10 +21,10 @@ import java.util.Set;
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 @@ class IndexEnsuringQueryCreationListener implements QueryCreationListener<PartTr @@ -111,7 +111,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 mongoException) {

118
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoDbErrorCodes.java

@ -19,6 +19,8 @@ import java.util.HashMap; @@ -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.yml}.
*
@ -97,6 +99,7 @@ public final class MongoDbErrorCodes { @@ -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 { @@ -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 { @@ -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 final class MongoDbErrorCodes { @@ -182,4 +261,17 @@ public final class MongoDbErrorCodes {
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;
}
}

46
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoExceptionTranslatorUnitTests.java

@ -25,11 +25,13 @@ import org.mockito.Mockito; @@ -25,11 +25,13 @@ 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;
@ -39,7 +41,9 @@ import com.mongodb.MongoInternalException; @@ -39,7 +41,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}.
@ -80,15 +84,13 @@ class MongoExceptionTranslatorUnitTests { @@ -80,15 +84,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);
}
@ -172,6 +174,38 @@ class MongoExceptionTranslatorUnitTests { @@ -172,6 +174,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, ""));

Loading…
Cancel
Save