From f966252ee4769599413cd5de03f2ea54e3608f4e Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 24 Jun 2016 17:07:46 +0200 Subject: [PATCH] DATAMONGO-1451 - Translate write concern timeouts to TransientDataAccessResourceException. We now translate timeouts (caused by replication or write concern timeout) into TransientDataAccessResourceException. --- .../core/MongoExceptionTranslator.java | 11 +++- .../core/ReflectiveWriteResultInvoker.java | 34 +++++++++- .../MongoExceptionTranslatorUnitTests.java | 62 ++++++++++++++++++- 3 files changed, 104 insertions(+), 3 deletions(-) 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 bf94e9c51..96af768cb 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.DuplicateKeyException; 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 org.springframework.util.ClassUtils; 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 @@ import com.mongodb.MongoException; * @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 24c8eb724..206ecdab6 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.data.mongodb.util.MongoClientVersion.*; 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 @@ import com.mongodb.WriteResult; * * @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 @@ final class 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 @@ final class ReflectiveWriteResultInvoker { 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 6f6fa5e76..519b26c2d 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 @@ package org.springframework.data.mongodb.core; 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.DataAccessResourceFailureException; 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 class MongoExceptionTranslatorUnitTests { 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() {