diff --git a/pom.xml b/pom.xml index 75fa63795e..ccfc9e83b9 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 1.11.0.BUILD-SNAPSHOT + 1.11.0.DATAMONGO-1451-SNAPSHOT pom Spring Data MongoDB diff --git a/spring-data-mongodb-cross-store/pom.xml b/spring-data-mongodb-cross-store/pom.xml index dc5a04a7ad..675a812764 100644 --- a/spring-data-mongodb-cross-store/pom.xml +++ b/spring-data-mongodb-cross-store/pom.xml @@ -6,7 +6,7 @@ org.springframework.data spring-data-mongodb-parent - 1.11.0.BUILD-SNAPSHOT + 1.11.0.DATAMONGO-1451-SNAPSHOT ../pom.xml @@ -48,7 +48,7 @@ org.springframework.data spring-data-mongodb - 1.11.0.BUILD-SNAPSHOT + 1.11.0.DATAMONGO-1451-SNAPSHOT diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index 9876e11ee3..77953b2358 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -13,7 +13,7 @@ org.springframework.data spring-data-mongodb-parent - 1.11.0.BUILD-SNAPSHOT + 1.11.0.DATAMONGO-1451-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb-log4j/pom.xml b/spring-data-mongodb-log4j/pom.xml index c0436bac90..dcea71220a 100644 --- a/spring-data-mongodb-log4j/pom.xml +++ b/spring-data-mongodb-log4j/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 1.11.0.BUILD-SNAPSHOT + 1.11.0.DATAMONGO-1451-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index ea1b594b99..448bc12dde 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -11,7 +11,7 @@ org.springframework.data spring-data-mongodb-parent - 1.11.0.BUILD-SNAPSHOT + 1.11.0.DATAMONGO-1451-SNAPSHOT ../pom.xml 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 bf94e9c519..96af768cb9 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 @@ -1,5 +1,5 @@ /* - * Copyright 2010-2015 the original author or authors. + * Copyright 2010-2016 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. @@ -26,6 +26,7 @@ import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.dao.InvalidDataAccessResourceUsageException; import org.springframework.dao.PermissionDeniedDataAccessException; +import org.springframework.dao.TransientDataAccessResourceException; import org.springframework.dao.support.PersistenceExceptionTranslator; import org.springframework.data.mongodb.BulkOperationException; import org.springframework.data.mongodb.UncategorizedMongoDbException; @@ -34,6 +35,7 @@ import com.mongodb.BulkWriteException; import com.mongodb.MongoException; +import com.mongodb.WriteConcernException; /** * Simple {@link PersistenceExceptionTranslator} for Mongo. Convert the given runtime exception to an appropriate @@ -43,6 +45,7 @@ * @author Oliver Gierke * @author Michal Vich * @author Christoph Strobl + * @author Mark Paluch */ public class MongoExceptionTranslator implements PersistenceExceptionTranslator { @@ -65,6 +68,12 @@ public class MongoExceptionTranslator implements PersistenceExceptionTranslator */ public DataAccessException translateExceptionIfPossible(RuntimeException ex) { + // Check for a timeout exception + + if (ex instanceof WriteConcernException && ReflectiveWriteResultInvoker.wasTimeout((WriteConcernException) ex)) { + return new TransientDataAccessResourceException(ex.getMessage(), ex); + } + // Check for well-known MongoException subclasses. String exception = ClassUtils.getShortName(ClassUtils.getUserClass(ex.getClass())); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReflectiveWriteResultInvoker.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReflectiveWriteResultInvoker.java index 24c8eb7241..206ecdab6f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReflectiveWriteResultInvoker.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReflectiveWriteResultInvoker.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-2016 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. @@ -19,8 +19,10 @@ import static org.springframework.util.ReflectionUtils.*; import java.lang.reflect.Method; +import java.util.Map; import com.mongodb.MongoException; +import com.mongodb.WriteConcernException; import com.mongodb.WriteResult; /** @@ -29,12 +31,15 @@ * * @author Christoph Strobl * @author Oliver Gierke + * @author Mark Paluch * @since 1.7 */ final class ReflectiveWriteResultInvoker { private static final Method GET_ERROR_METHOD; private static final Method WAS_ACKNOWLEDGED_METHOD; + private static final Method GET_RESPONSE; + private static final Method GET_COMMAND_RESULT; private ReflectiveWriteResultInvoker() {} @@ -42,6 +47,8 @@ private ReflectiveWriteResultInvoker() {} GET_ERROR_METHOD = findMethod(WriteResult.class, "getError"); WAS_ACKNOWLEDGED_METHOD = findMethod(WriteResult.class, "wasAcknowledged"); + GET_RESPONSE = findMethod(WriteConcernException.class, "getResponse"); + GET_COMMAND_RESULT = findMethod(WriteConcernException.class, "getCommandResult"); } /** @@ -64,4 +71,29 @@ public static String getError(WriteResult writeResult) { public static boolean wasAcknowledged(WriteResult writeResult) { return isMongo3Driver() ? ((Boolean) invokeMethod(WAS_ACKNOWLEDGED_METHOD, writeResult)).booleanValue() : true; } + + /** + * @param writeConcernException + * @return return {@literal true} if the {@link WriteConcernException} indicates a write concern timeout as reason + * @since 1.10 + */ + @SuppressWarnings("unchecked") + public static boolean wasTimeout(WriteConcernException writeConcernException) { + + Map response; + if (isMongo3Driver()) { + response = (Map) invokeMethod(GET_RESPONSE, writeConcernException); + } else { + response = (Map) invokeMethod(GET_COMMAND_RESULT, writeConcernException); + } + + if (response != null && response.containsKey("wtimeout")) { + Object wtimeout = response.get("wtimeout"); + if (wtimeout != null && wtimeout.toString().contains("true")) { + return true; + } + } + + 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 6f6fa5e766..519b26c2d2 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 @@ -17,10 +17,15 @@ import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; +import static org.junit.Assume.*; import static org.mockito.Mockito.*; import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.net.UnknownHostException; +import java.util.Map; import org.junit.Before; import org.junit.Test; @@ -33,20 +38,25 @@ import org.springframework.dao.DuplicateKeyException; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.dao.InvalidDataAccessResourceUsageException; +import org.springframework.dao.TransientDataAccessResourceException; import org.springframework.data.mongodb.UncategorizedMongoDbException; +import org.springframework.data.mongodb.util.MongoClientVersion; +import org.springframework.test.util.ReflectionTestUtils; import com.mongodb.MongoCursorNotFoundException; import com.mongodb.MongoException; import com.mongodb.MongoInternalException; import com.mongodb.MongoSocketException; import com.mongodb.ServerAddress; +import com.mongodb.WriteConcernException; /** * Unit tests for {@link MongoExceptionTranslator}. - * + * * @author Michal Vich * @author Oliver Gierke * @author Christoph Strobl + * @author Mark Paluch */ @RunWith(MockitoJUnitRunner.class) public class MongoExceptionTranslatorUnitTests { @@ -133,6 +143,56 @@ public void translateMongoInternalException() { expectExceptionWithCauseMessage(translatedException, InvalidDataAccessResourceUsageException.class); } + @Test // DATAMONGO-1451 + @SuppressWarnings("unchecked") + public void translateTimeoutToTransientDataAccessResourceExceptionWith2xDriver() throws Exception { + + assumeThat(MongoClientVersion.isMongo3Driver(), is(false)); + + Constructor constructor = Class.forName("com.mongodb.CommandResult").getDeclaredConstructor(ServerAddress.class); + constructor.setAccessible(true); + + Map commandResult = (Map) constructor.newInstance(new ServerAddress("localhost")); + commandResult.put("wtimeout", true); + commandResult.put("ok", 1); + commandResult.put("n", 0); + commandResult.put("err", "waiting for replication timed out"); + commandResult.put("code", 64); + + DataAccessException translatedException = translator.translateExceptionIfPossible( + (RuntimeException) ReflectionTestUtils.invokeMethod(commandResult, "getException")); + + expectExceptionWithCauseMessage(translatedException, TransientDataAccessResourceException.class); + } + + @Test // DATAMONGO-1451 + public void translateTimeoutToTransientDataAccessResourceExceptionWith3xDriver() throws Exception { + + assumeThat(MongoClientVersion.isMongo3Driver(), is(true)); + + Class bsonDocumentClass = Class.forName("org.bson.BsonDocument"); + + Method getWriteResult = Class.forName("com.mongodb.connection.ProtocolHelper").getDeclaredMethod("getWriteResult", + bsonDocumentClass, ServerAddress.class); + + String response = "{ \"serverUsed\" : \"10.10.17.35:27017\" , \"ok\" : 1 , \"n\" : 0 , \"wtimeout\" : true , \"err\" : \"waiting for replication timed out\" , \"code\" : 64}"; + Object bsonDocument = bsonDocumentClass.getDeclaredMethod("parse", String.class).invoke(null, response); + + try { + getWriteResult.setAccessible(true); + getWriteResult.invoke(null, bsonDocument, new ServerAddress("localhost")); + fail("Missing Exception"); + } catch (InvocationTargetException e) { + + assertThat(e.getTargetException(), is(instanceOf(WriteConcernException.class))); + + DataAccessException translatedException = translator + .translateExceptionIfPossible((RuntimeException) e.getTargetException()); + expectExceptionWithCauseMessage(translatedException, TransientDataAccessResourceException.class); + } + + } + @Test public void translateUnsupportedException() {