DATAMONGO-1125 - Improve exception message for index creation errors.

We now use MongoExceptionTranslator to potentially convert exceptions during index creation into Springs DataAccessException hierarchy. In case we encounter an error code indicating DataIntegrityViolation we try to fetch existing index data and append it to the exceptions message.

Original pull request: #302.
This commit is contained in:
Christoph Strobl
2015-06-23 16:39:54 +02:00
committed by Oliver Gierke
parent 5c0707d221
commit 1dbe3b62d7
4 changed files with 259 additions and 7 deletions

View File

@@ -16,6 +16,7 @@
package org.springframework.data.mongodb.core;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
@@ -25,6 +26,7 @@ import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.dao.InvalidDataAccessResourceUsageException;
import org.springframework.dao.PermissionDeniedDataAccessException;
import org.springframework.dao.support.PersistenceExceptionTranslator;
import org.springframework.data.mongodb.UncategorizedMongoDbException;
import org.springframework.util.ClassUtils;
@@ -86,12 +88,15 @@ public class MongoExceptionTranslator implements PersistenceExceptionTranslator
int code = ((MongoException) ex).getCode();
if (code == 11000 || code == 11001) {
if (MongoDbErrorCodes.isDuplicateKeyCode(code)) {
throw new DuplicateKeyException(ex.getMessage(), ex);
} else if (code == 12000 || code == 13440) {
} else if (MongoDbErrorCodes.isDataAccessResourceFailureCode(code)) {
throw new DataAccessResourceFailureException(ex.getMessage(), ex);
} else if (code == 10003 || code == 12001 || code == 12010 || code == 12011 || code == 12012) {
} else if (MongoDbErrorCodes.isInvalidDataAccessApiUsageCode(code) || code == 10003 || code == 12001
|| code == 12010 || code == 12011 || code == 12012) {
throw new InvalidDataAccessApiUsageException(ex.getMessage(), ex);
} else if (MongoDbErrorCodes.isPermissionDeniedCode(code)) {
throw new PermissionDeniedDataAccessException(ex.getMessage(), ex);
}
return new UncategorizedMongoDbException(ex.getMessage(), ex);
}
@@ -101,4 +106,126 @@ public class MongoExceptionTranslator implements PersistenceExceptionTranslator
// that translation should not occur.
return null;
}
/**
* {@link MongoDbErrorCodes} holds MongoDB specific error codes outlined in {@literal mongo/base/error_codes.err}.
*
* @author Christoph Strobl
* @since 1.8
*/
public static final class MongoDbErrorCodes {
static HashMap<Integer, String> dataAccessResourceFailureCodes;
static HashMap<Integer, String> dataIntegrityViolationCodes;
static HashMap<Integer, String> duplicateKeyCodes;
static HashMap<Integer, String> invalidDataAccessApiUsageExeption;
static HashMap<Integer, String> permissionDeniedCodes;
static HashMap<Integer, String> errorCodes;
static {
dataAccessResourceFailureCodes = new HashMap<Integer, String>(10);
dataAccessResourceFailureCodes.put(6, "HostUnreachable");
dataAccessResourceFailureCodes.put(7, "HostNotFound");
dataAccessResourceFailureCodes.put(89, "NetworkTimeout");
dataAccessResourceFailureCodes.put(91, "ShutdownInProgress");
dataAccessResourceFailureCodes.put(12000, "SlaveDelayDifferential");
dataAccessResourceFailureCodes.put(10084, "CannotFindMapFile64Bit");
dataAccessResourceFailureCodes.put(10085, "CannotFindMapFile");
dataAccessResourceFailureCodes.put(10357, "ShutdownInProgress");
dataAccessResourceFailureCodes.put(10359, "Header==0");
dataAccessResourceFailureCodes.put(13440, "BadOffsetInFile");
dataAccessResourceFailureCodes.put(13441, "BadOffsetInFile");
dataAccessResourceFailureCodes.put(13640, "DataFileHeaderCorrupt");
dataIntegrityViolationCodes = new HashMap<Integer, String>(6);
dataIntegrityViolationCodes.put(67, "CannotCreateIndex");
dataIntegrityViolationCodes.put(68, "IndexAlreadyExists");
dataIntegrityViolationCodes.put(85, "IndexOptionsConflict");
dataIntegrityViolationCodes.put(86, "IndexKeySpecsConflict");
dataIntegrityViolationCodes.put(112, "WriteConflict");
dataIntegrityViolationCodes.put(117, "ConflictingOperationInProgress");
duplicateKeyCodes = new HashMap<Integer, String>(3);
duplicateKeyCodes.put(3, "OBSOLETE_DuplicateKey");
duplicateKeyCodes.put(84, "DuplicateKeyValue");
duplicateKeyCodes.put(11000, "DuplicateKey");
duplicateKeyCodes.put(11001, "DuplicateKey");
invalidDataAccessApiUsageExeption = new HashMap<Integer, String>();
invalidDataAccessApiUsageExeption.put(5, "GraphContainsCycle");
invalidDataAccessApiUsageExeption.put(9, "FailedToParse");
invalidDataAccessApiUsageExeption.put(14, "TypeMismatch");
invalidDataAccessApiUsageExeption.put(15, "Overflow");
invalidDataAccessApiUsageExeption.put(16, "InvalidLength");
invalidDataAccessApiUsageExeption.put(20, "IllegalOperation");
invalidDataAccessApiUsageExeption.put(21, "EmptyArrayOperation");
invalidDataAccessApiUsageExeption.put(22, "InvalidBSON");
invalidDataAccessApiUsageExeption.put(23, "AlreadyInitialized");
invalidDataAccessApiUsageExeption.put(29, "NonExistentPath");
invalidDataAccessApiUsageExeption.put(30, "InvalidPath");
invalidDataAccessApiUsageExeption.put(40, "ConflictingUpdateOperators");
invalidDataAccessApiUsageExeption.put(45, "UserDataInconsistent");
invalidDataAccessApiUsageExeption.put(30, "DollarPrefixedFieldName");
invalidDataAccessApiUsageExeption.put(52, "InvalidPath");
invalidDataAccessApiUsageExeption.put(53, "InvalidIdField");
invalidDataAccessApiUsageExeption.put(54, "NotSingleValueField");
invalidDataAccessApiUsageExeption.put(55, "InvalidDBRef");
invalidDataAccessApiUsageExeption.put(56, "EmptyFieldName");
invalidDataAccessApiUsageExeption.put(57, "DottedFieldName");
invalidDataAccessApiUsageExeption.put(59, "CommandNotFound");
invalidDataAccessApiUsageExeption.put(60, "DatabaseNotFound");
invalidDataAccessApiUsageExeption.put(61, "ShardKeyNotFound");
invalidDataAccessApiUsageExeption.put(62, "OplogOperationUnsupported");
invalidDataAccessApiUsageExeption.put(66, "ImmutableField");
invalidDataAccessApiUsageExeption.put(72, "InvalidOptions");
invalidDataAccessApiUsageExeption.put(115, "CommandNotSupported");
invalidDataAccessApiUsageExeption.put(116, "DocTooLargeForCapped");
invalidDataAccessApiUsageExeption.put(130, "SymbolNotFound");
invalidDataAccessApiUsageExeption.put(17280, "KeyTooLong");
invalidDataAccessApiUsageExeption.put(13334, "ShardKeyTooBig");
permissionDeniedCodes = new HashMap<Integer, String>();
permissionDeniedCodes.put(11, "UserNotFound");
permissionDeniedCodes.put(18, "AuthenticationFailed");
permissionDeniedCodes.put(31, "RoleNotFound");
permissionDeniedCodes.put(32, "RolesNotRelated");
permissionDeniedCodes.put(33, "PrvilegeNotFound");
permissionDeniedCodes.put(15847, "CannotAuthenticate");
permissionDeniedCodes.put(16704, "CannotAuthenticateToAdminDB");
permissionDeniedCodes.put(16705, "CannotAuthenticateToAdminDB");
errorCodes = new HashMap<Integer, String>();
errorCodes.putAll(dataAccessResourceFailureCodes);
errorCodes.putAll(dataIntegrityViolationCodes);
errorCodes.putAll(duplicateKeyCodes);
errorCodes.putAll(invalidDataAccessApiUsageExeption);
errorCodes.putAll(permissionDeniedCodes);
}
public static boolean isDataIntegrityViolationCode(Integer errorCode) {
return errorCode == null ? false : dataIntegrityViolationCodes.containsKey(errorCode);
}
public static boolean isDataAccessResourceFailureCode(Integer errorCode) {
return errorCode == null ? false : dataAccessResourceFailureCodes.containsKey(errorCode);
}
public static boolean isDuplicateKeyCode(Integer errorCode) {
return errorCode == null ? false : duplicateKeyCodes.containsKey(errorCode);
}
public static boolean isPermissionDeniedCode(Integer errorCode) {
return errorCode == null ? false : permissionDeniedCodes.containsKey(errorCode);
}
public static boolean isInvalidDataAccessApiUsageCode(Integer errorCode) {
return errorCode == null ? false : invalidDataAccessApiUsageExeption.containsKey(errorCode);
}
public static String getErrorDescription(Integer errorCode) {
return errorCode == null ? null : errorCodes.get(errorCode);
}
}
}

View File

@@ -21,15 +21,21 @@ import java.util.concurrent.ConcurrentHashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationListener;
import org.springframework.dao.DataIntegrityViolationException;
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.MongoDbFactory;
import org.springframework.data.mongodb.core.MongoExceptionTranslator.MongoDbErrorCodes;
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;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import com.mongodb.DBObject;
import com.mongodb.MongoException;
/**
* Component that inspects {@link MongoPersistentEntity} instances contained in the given {@link MongoMappingContext}
@@ -129,9 +135,34 @@ public class MongoPersistentEntityIndexCreator implements ApplicationListener<Ma
}
}
private void createIndex(IndexDefinitionHolder indexDefinition) {
mongoDbFactory.getDb().getCollection(indexDefinition.getCollection()).createIndex(indexDefinition.getIndexKeys(),
indexDefinition.getIndexOptions());
void createIndex(IndexDefinitionHolder indexDefinition) {
try {
mongoDbFactory.getDb().getCollection(indexDefinition.getCollection()).createIndex(indexDefinition.getIndexKeys(),
indexDefinition.getIndexOptions());
} catch (MongoException ex) {
if (MongoDbErrorCodes.isDataIntegrityViolationCode(ex.getCode())) {
DBObject existingIndex = fetchIndexInformation(indexDefinition);
String message = "Cannot create index for '%s' in collection '%s' with keys '%s' and options '%s'.";
if (existingIndex != null) {
message += " Index already defined as '%s'.";
}
throw new DataIntegrityViolationException(
String.format(message, indexDefinition.getPath(), indexDefinition.getCollection(),
indexDefinition.getIndexKeys(), indexDefinition.getIndexOptions(), existingIndex),
ex);
}
RuntimeException exceptionToThrow = mongoDbFactory.getExceptionTranslator().translateExceptionIfPossible(ex);
throw exceptionToThrow != null ? exceptionToThrow : ex;
}
}
/**
@@ -143,4 +174,28 @@ public class MongoPersistentEntityIndexCreator implements ApplicationListener<Ma
public boolean isIndexCreatorFor(MappingContext<?, ?> context) {
return this.mappingContext.equals(context);
}
private DBObject fetchIndexInformation(IndexDefinitionHolder indexDefinition) {
if (indexDefinition == null) {
return null;
}
try {
Object indexNameToLookUp = indexDefinition.getIndexOptions().get("name");
for (DBObject index : mongoDbFactory.getDb().getCollection(indexDefinition.getCollection()).getIndexInfo()) {
if (ObjectUtils.nullSafeEquals(indexNameToLookUp, index.get("name"))) {
return index;
}
}
} catch (Exception e) {
LOGGER.debug(
String.format("Failed to load index information for collection '%s'.", indexDefinition.getCollection()), e);
}
return null;
}
}

View File

@@ -18,25 +18,37 @@ package org.springframework.data.mongodb.core.index;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.List;
import org.hamcrest.Matchers;
import org.hamcrest.core.IsInstanceOf;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.rules.RuleChain;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.data.annotation.Id;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.SimpleMongoDbFactory;
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;
import org.springframework.data.mongodb.test.util.CleanMongoDB;
import org.springframework.data.mongodb.test.util.MongoVersionRule;
import org.springframework.data.util.Version;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.mongodb.MongoClient;
import com.mongodb.MongoCommandException;
/**
* Integration tests for {@link MongoPersistentEntityIndexCreator}.
*
@@ -54,6 +66,8 @@ public class MongoPersistentEntityIndexCreatorIntegrationTests {
public static @ClassRule RuleChain rules = RuleChain.outerRule(MongoVersionRule.atLeast(new Version(2, 6))).around(
CleanMongoDB.indexes(Arrays.asList(SAMPLE_TYPE_COLLECTION_NAME, RECURSIVE_TYPE_COLLECTION_NAME)));
public @Rule ExpectedException expectedException = ExpectedException.none();
@Autowired @Qualifier("mongo1") MongoOperations templateOne;
@Autowired @Qualifier("mongo2") MongoOperations templateTwo;
@@ -81,6 +95,28 @@ public class MongoPersistentEntityIndexCreatorIntegrationTests {
assertThat(indexInfo, Matchers.<IndexInfo> hasItem(hasProperty("name", is("firstName"))));
}
/**
* @DATAMONGO-1125
*/
@Test
public void createIndexShouldThrowMeaningfulExceptionWhenIndexCreationFails() throws UnknownHostException {
expectedException.expect(DataIntegrityViolationException.class);
expectedException.expectMessage("collection 'datamongo-1125'");
expectedException.expectMessage("dalinar.kohlin");
expectedException.expectMessage("lastname");
expectedException.expectCause(IsInstanceOf.<Throwable> instanceOf(MongoCommandException.class));
MongoPersistentEntityIndexCreator indexCreator = new MongoPersistentEntityIndexCreator(new MongoMappingContext(),
new SimpleMongoDbFactory(new MongoClient(), "issue"));
indexCreator.createIndex(new IndexDefinitionHolder("dalinar.kohlin", new Index().named("stormlight")
.on("lastname", Direction.ASC).unique(), "datamongo-1125"));
indexCreator.createIndex(new IndexDefinitionHolder("dalinar.kohlin", new Index().named("stormlight")
.on("lastname", Direction.ASC).sparse(), "datamongo-1125"));
}
@Document(collection = RECURSIVE_TYPE_COLLECTION_NAME)
static abstract class RecursiveGenericType<RGT extends RecursiveGenericType<RGT>> {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2014 the original author or authors.
* Copyright 2012-2015 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.
@@ -28,11 +28,14 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.context.ApplicationContext;
import org.springframework.dao.DataAccessException;
import org.springframework.data.geo.Point;
import org.springframework.data.mapping.context.MappingContextEvent;
import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.data.mongodb.core.MongoExceptionTranslator;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
@@ -43,6 +46,7 @@ import com.mongodb.BasicDBObjectBuilder;
import com.mongodb.DB;
import com.mongodb.DBCollection;
import com.mongodb.DBObject;
import com.mongodb.MongoException;
/**
* Unit tests for {@link MongoPersistentEntityIndexCreator}.
@@ -211,6 +215,36 @@ public class MongoPersistentEntityIndexCreatorUnitTests {
assertThat(collectionNameCapturer.getValue(), equalTo("indexedDocumentWrapper"));
}
/**
* @see DATAMONGO-1125
*/
@Test(expected = DataAccessException.class)
public void createIndexShouldUsePersistenceExceptionTranslatorForNonDataIntegrityConcerns() {
when(factory.getExceptionTranslator()).thenReturn(new MongoExceptionTranslator());
doThrow(new MongoException(6, "HostUnreachable")).when(collection).createIndex(Mockito.any(DBObject.class),
Mockito.any(DBObject.class));
MongoMappingContext mappingContext = prepareMappingContext(Person.class);
new MongoPersistentEntityIndexCreator(mappingContext, factory);
}
/**
* @see DATAMONGO-1125
*/
@Test(expected = ClassCastException.class)
public void createIndexShouldNotConvertUnknownExceptionTypes() {
when(factory.getExceptionTranslator()).thenReturn(new MongoExceptionTranslator());
doThrow(new ClassCastException("o_O")).when(collection).createIndex(Mockito.any(DBObject.class),
Mockito.any(DBObject.class));
MongoMappingContext mappingContext = prepareMappingContext(Person.class);
new MongoPersistentEntityIndexCreator(mappingContext, factory);
}
private static MongoMappingContext prepareMappingContext(Class<?> type) {
MongoMappingContext mappingContext = new MongoMappingContext();