Skip to content

Commit b94c3ca

Browse files
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.
1 parent 03ac834 commit b94c3ca

File tree

5 files changed

+231
-3
lines changed

5 files changed

+231
-3
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright 2018 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.mongodb;
17+
18+
import org.springframework.dao.TransientDataAccessException;
19+
import org.springframework.lang.Nullable;
20+
21+
/**
22+
* {@link TransientDataAccessException} specific to MongoDB {@link com.mongodb.session.ClientSession} related data
23+
* access failures such as reading data using an already closed session.
24+
*
25+
* @author Christoph Strobl
26+
* @since 2.1
27+
*/
28+
public class TransientClientSessionException extends TransientMongoDbException {
29+
30+
/**
31+
* Constructor for {@link TransientClientSessionException}.
32+
*
33+
* @param msg the detail message. Must not be {@literal null}.
34+
*/
35+
public TransientClientSessionException(String msg) {
36+
super(msg);
37+
}
38+
39+
/**
40+
* Constructor for {@link TransientClientSessionException}.
41+
*
42+
* @param msg the detail message. Can be {@literal null}.
43+
* @param cause the root cause. Can be {@literal null}.
44+
*/
45+
public TransientClientSessionException(@Nullable String msg, @Nullable Throwable cause) {
46+
super(msg, cause);
47+
}
48+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright 2018 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.mongodb;
17+
18+
import org.springframework.dao.TransientDataAccessException;
19+
import org.springframework.lang.Nullable;
20+
21+
/**
22+
* Root of the hierarchy of MongoDB specific data access exceptions that are considered transient such as
23+
* {@link com.mongodb.MongoException MongoExceptions} carrying {@link com.mongodb.MongoException#hasErrorLabel(String)
24+
* specific labels}.
25+
*
26+
* @author Christoph Strobl
27+
* @since 2.1
28+
*/
29+
public class TransientMongoDbException extends TransientDataAccessException {
30+
31+
/**
32+
* Constructor for {@link TransientMongoDbException}.
33+
*
34+
* @param msg the detail message. Must not be {@literal null}.
35+
*/
36+
public TransientMongoDbException(String msg) {
37+
super(msg);
38+
}
39+
40+
/**
41+
* Constructor for {@link TransientMongoDbException}.
42+
*
43+
* @param msg the detail message. Can be {@literal null}.
44+
* @param cause the root cause. Can be {@literal null}.
45+
*/
46+
public TransientMongoDbException(String msg, @Nullable Throwable cause) {
47+
super(msg, cause);
48+
}
49+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright 2018 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.mongodb;
17+
18+
import org.springframework.lang.Nullable;
19+
20+
/**
21+
* A specific {@link TransientClientSessionException} related to issues with a transaction such as fails on commit.
22+
*
23+
* @author Christoph Strobl
24+
* @since 2.1
25+
*/
26+
public class TransientMongoDbTransactionException extends TransientClientSessionException {
27+
28+
/**
29+
* Constructor for {@link TransientMongoDbTransactionException}.
30+
*
31+
* @param msg the detail message. Must not be {@literal null}.
32+
*/
33+
public TransientMongoDbTransactionException(String msg) {
34+
super(msg);
35+
}
36+
37+
/**
38+
* Constructor for {@link ClientSessionException}.
39+
*
40+
* @param msg the detail message. Can be {@literal null}.
41+
* @param cause the root cause. Can be {@literal null}.
42+
*/
43+
public TransientMongoDbTransactionException(@Nullable String msg, @Nullable Throwable cause) {
44+
super(msg, cause);
45+
}
46+
}

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

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,14 @@
2828
import org.springframework.dao.InvalidDataAccessApiUsageException;
2929
import org.springframework.dao.InvalidDataAccessResourceUsageException;
3030
import org.springframework.dao.PermissionDeniedDataAccessException;
31+
import org.springframework.dao.TransientDataAccessException;
3132
import org.springframework.dao.support.PersistenceExceptionTranslator;
3233
import org.springframework.data.mongodb.BulkOperationException;
3334
import org.springframework.data.mongodb.ClientSessionException;
3435
import org.springframework.data.mongodb.MongoTransactionException;
36+
import org.springframework.data.mongodb.TransientClientSessionException;
37+
import org.springframework.data.mongodb.TransientMongoDbException;
38+
import org.springframework.data.mongodb.TransientMongoDbTransactionException;
3539
import org.springframework.data.mongodb.UncategorizedMongoDbException;
3640
import org.springframework.data.mongodb.util.MongoDbErrorCodes;
3741
import org.springframework.lang.Nullable;
@@ -74,6 +78,21 @@ public class MongoExceptionTranslator implements PersistenceExceptionTranslator
7478
@Nullable
7579
public DataAccessException translateExceptionIfPossible(RuntimeException ex) {
7680

81+
DataAccessException translatedException = doTranslateException(ex);
82+
if (translatedException == null) {
83+
return null;
84+
}
85+
86+
// Translated exceptions that per se are not be recoverable (eg. WriteConflicts), might still be transient inside a
87+
// transaction. Let's wrap those.
88+
return (isTransientFailure(ex) && !(translatedException instanceof TransientDataAccessException))
89+
? new TransientMongoDbException(ex.getMessage(), translatedException) : translatedException;
90+
91+
}
92+
93+
@Nullable
94+
DataAccessException doTranslateException(RuntimeException ex) {
95+
7796
// Check for well-known MongoException subclasses.
7897

7998
if (ex instanceof BsonInvalidOperationException) {
@@ -119,7 +138,9 @@ public DataAccessException translateExceptionIfPossible(RuntimeException ex) {
119138
// All other MongoExceptions
120139
if (ex instanceof MongoException) {
121140

122-
int code = ((MongoException) ex).getCode();
141+
MongoException mongoException = (MongoException) ex;
142+
int code = mongoException.getCode();
143+
boolean isTransient = isTransientFailure(mongoException);
123144

124145
if (MongoDbErrorCodes.isDuplicateKeyCode(code)) {
125146
return new DuplicateKeyException(ex.getMessage(), ex);
@@ -131,10 +152,13 @@ public DataAccessException translateExceptionIfPossible(RuntimeException ex) {
131152
} else if (MongoDbErrorCodes.isPermissionDeniedCode(code)) {
132153
return new PermissionDeniedDataAccessException(ex.getMessage(), ex);
133154
} else if (MongoDbErrorCodes.isClientSessionFailureCode(code)) {
134-
return new ClientSessionException(ex.getMessage(), ex);
155+
return isTransient ? new TransientClientSessionException(ex.getMessage(), ex)
156+
: new ClientSessionException(ex.getMessage(), ex);
135157
} else if (MongoDbErrorCodes.isTransactionFailureCode(code)) {
136-
return new MongoTransactionException(ex.getMessage(), ex);
158+
return isTransient ? new TransientMongoDbTransactionException(ex.getMessage(), ex)
159+
: new MongoTransactionException(ex.getMessage(), ex);
137160
}
161+
138162
return new UncategorizedMongoDbException(ex.getMessage(), ex);
139163
}
140164

@@ -153,4 +177,25 @@ public DataAccessException translateExceptionIfPossible(RuntimeException ex) {
153177
// that translation should not occur.
154178
return null;
155179
}
180+
181+
/**
182+
* Check if a given exception holds an error label indicating a transient failure.
183+
*
184+
* @param e
185+
* @return {@literal true} if the given {@link Exception} is a {@link MongoException} holding one of the transient
186+
* exception error labels.
187+
* @see MongoException#hasErrorLabel(String)
188+
* @since 2.1
189+
*/
190+
public static boolean isTransientFailure(Exception e) {
191+
192+
if (!(e instanceof MongoException)) {
193+
return false;
194+
}
195+
196+
MongoException mongoException = (MongoException) e;
197+
198+
return mongoException.hasErrorLabel(MongoException.TRANSIENT_TRANSACTION_ERROR_LABEL)
199+
|| mongoException.hasErrorLabel(MongoException.UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL);
200+
}
156201
}

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,27 @@
1818
import static org.hamcrest.CoreMatchers.*;
1919
import static org.junit.Assert.*;
2020

21+
import java.beans.Transient;
2122
import java.net.UnknownHostException;
2223

24+
import com.mongodb.MongoCommandException;
25+
import com.mongodb.MongoWriteException;
26+
import com.mongodb.WriteError;
2327
import org.bson.BsonDocument;
2428
import org.junit.Before;
2529
import org.junit.Test;
2630
import org.springframework.core.NestedRuntimeException;
2731
import org.springframework.dao.DataAccessException;
2832
import org.springframework.dao.DataAccessResourceFailureException;
33+
import org.springframework.dao.DataIntegrityViolationException;
2934
import org.springframework.dao.DuplicateKeyException;
3035
import org.springframework.dao.InvalidDataAccessApiUsageException;
3136
import org.springframework.dao.InvalidDataAccessResourceUsageException;
37+
import org.springframework.dao.TransientDataAccessException;
3238
import org.springframework.data.mongodb.ClientSessionException;
3339
import org.springframework.data.mongodb.MongoTransactionException;
40+
import org.springframework.data.mongodb.TransientMongoDbException;
41+
import org.springframework.data.mongodb.TransientMongoDbTransactionException;
3442
import org.springframework.data.mongodb.UncategorizedMongoDbException;
3543

3644
import com.mongodb.MongoCursorNotFoundException;
@@ -152,6 +160,38 @@ public void translateTransactionExceptions() {
152160
checkTranslatedMongoException(MongoTransactionException.class, 267);
153161
}
154162

163+
@Test // DATAMONGO-2073
164+
public void translateTransientTransactionExceptions() {
165+
166+
MongoException source = new MongoException(267, "PreparedTransactionInProgress");
167+
source.addLabel(MongoException.TRANSIENT_TRANSACTION_ERROR_LABEL);
168+
169+
expectExceptionWithCauseMessage(translator.translateExceptionIfPossible(source),
170+
TransientMongoDbTransactionException.class, "PreparedTransactionInProgress");
171+
}
172+
173+
@Test // DATAMONGO-2073
174+
public void translateMongoExceptionWithTransientLabelToTransientMongoDbException() {
175+
176+
MongoException exception = new MongoException(0, "");
177+
exception.addLabel(MongoException.UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL);
178+
DataAccessException translatedException = translator.translateExceptionIfPossible(exception);
179+
180+
expectExceptionWithCauseMessage(translatedException, TransientMongoDbException.class);
181+
}
182+
183+
@Test // DATAMONGO-2073
184+
public void wrapsTranslatedExceptionsWhenTransientLabelPresent() {
185+
186+
MongoException exception = new MongoWriteException(new WriteError(112, "WriteConflict", new BsonDocument()), null);
187+
exception.addLabel(MongoException.UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL);
188+
189+
DataAccessException translatedException = translator.translateExceptionIfPossible(exception);
190+
191+
assertThat(translatedException, is(instanceOf(TransientMongoDbException.class)));
192+
assertThat(translatedException.getCause(), is(instanceOf(DataIntegrityViolationException.class)));
193+
}
194+
155195
private void checkTranslatedMongoException(Class<? extends Exception> clazz, int code) {
156196

157197
DataAccessException translated = translator.translateExceptionIfPossible(new MongoException(code, ""));

0 commit comments

Comments
 (0)