Skip to content

Commit 9a2bc7d

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. These URLs were switched to an https URL with a 2xx status. While the status was successful, your review is still recommended. * [ ] http://www.apache.org/licenses/ with 1 occurrences migrated to: https://www.apache.org/licenses/ ([https](https://www.apache.org/licenses/) result 200). * [ ] http://www.apache.org/licenses/LICENSE-2.0 with 852 occurrences migrated to: https://www.apache.org/licenses/LICENSE-2.0 ([https](https://www.apache.org/licenses/LICENSE-2.0) result 200). Original Pull Request: #721
1 parent 85d48f0 commit 9a2bc7d

File tree

5 files changed

+229
-3
lines changed

5 files changed

+229
-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+
* https://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+
* https://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+
* https://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: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,13 @@
2929
import org.springframework.dao.InvalidDataAccessApiUsageException;
3030
import org.springframework.dao.InvalidDataAccessResourceUsageException;
3131
import org.springframework.dao.PermissionDeniedDataAccessException;
32+
import org.springframework.dao.TransientDataAccessException;
3233
import org.springframework.dao.support.PersistenceExceptionTranslator;
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;
@@ -75,6 +79,21 @@ public class MongoExceptionTranslator implements PersistenceExceptionTranslator
7579
@Nullable
7680
public DataAccessException translateExceptionIfPossible(RuntimeException ex) {
7781

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

8099
if (ex instanceof BsonInvalidOperationException) {
@@ -120,7 +139,9 @@ public DataAccessException translateExceptionIfPossible(RuntimeException ex) {
120139
// All other MongoExceptions
121140
if (ex instanceof MongoException) {
122141

123-
int code = ((MongoException) ex).getCode();
142+
MongoException mongoException = (MongoException) ex;
143+
int code = mongoException.getCode();
144+
boolean isTransient = isTransientFailure(mongoException);
124145

125146
if (MongoDbErrorCodes.isDuplicateKeyCode(code)) {
126147
return new DuplicateKeyException(ex.getMessage(), ex);
@@ -132,9 +153,11 @@ public DataAccessException translateExceptionIfPossible(RuntimeException ex) {
132153
} else if (MongoDbErrorCodes.isPermissionDeniedCode(code)) {
133154
return new PermissionDeniedDataAccessException(ex.getMessage(), ex);
134155
} else if (MongoDbErrorCodes.isClientSessionFailureCode(code)) {
135-
return new ClientSessionException(ex.getMessage(), ex);
156+
return isTransient ? new TransientClientSessionException(ex.getMessage(), ex)
157+
: new ClientSessionException(ex.getMessage(), ex);
136158
} else if (MongoDbErrorCodes.isTransactionFailureCode(code)) {
137-
return new MongoTransactionException(ex.getMessage(), ex);
159+
return isTransient ? new TransientMongoDbTransactionException(ex.getMessage(), ex)
160+
: new MongoTransactionException(ex.getMessage(), ex);
138161
}
139162

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

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,25 @@
1717

1818
import static org.assertj.core.api.Assertions.*;
1919

20+
import com.mongodb.MongoCommandException;
21+
import com.mongodb.MongoWriteException;
22+
import com.mongodb.WriteError;
2023
import org.bson.BsonDocument;
2124
import org.junit.jupiter.api.BeforeEach;
2225
import org.junit.jupiter.api.Test;
2326

2427
import org.springframework.core.NestedRuntimeException;
2528
import org.springframework.dao.DataAccessException;
2629
import org.springframework.dao.DataAccessResourceFailureException;
30+
import org.springframework.dao.DataIntegrityViolationException;
2731
import org.springframework.dao.DuplicateKeyException;
2832
import org.springframework.dao.InvalidDataAccessApiUsageException;
2933
import org.springframework.dao.InvalidDataAccessResourceUsageException;
34+
import org.springframework.dao.TransientDataAccessException;
3035
import org.springframework.data.mongodb.ClientSessionException;
3136
import org.springframework.data.mongodb.MongoTransactionException;
37+
import org.springframework.data.mongodb.TransientMongoDbException;
38+
import org.springframework.data.mongodb.TransientMongoDbTransactionException;
3239
import org.springframework.data.mongodb.UncategorizedMongoDbException;
3340
import org.springframework.lang.Nullable;
3441

@@ -171,6 +178,38 @@ void translateTransactionExceptions() {
171178
checkTranslatedMongoException(MongoTransactionException.class, 267);
172179
}
173180

181+
@Test // DATAMONGO-2073
182+
public void translateTransientTransactionExceptions() {
183+
184+
MongoException source = new MongoException(267, "PreparedTransactionInProgress");
185+
source.addLabel(MongoException.TRANSIENT_TRANSACTION_ERROR_LABEL);
186+
187+
expectExceptionWithCauseMessage(translator.translateExceptionIfPossible(source),
188+
TransientMongoDbTransactionException.class, "PreparedTransactionInProgress");
189+
}
190+
191+
@Test // DATAMONGO-2073
192+
public void translateMongoExceptionWithTransientLabelToTransientMongoDbException() {
193+
194+
MongoException exception = new MongoException(0, "");
195+
exception.addLabel(MongoException.UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL);
196+
DataAccessException translatedException = translator.translateExceptionIfPossible(exception);
197+
198+
expectExceptionWithCauseMessage(translatedException, TransientMongoDbException.class);
199+
}
200+
201+
@Test // DATAMONGO-2073
202+
public void wrapsTranslatedExceptionsWhenTransientLabelPresent() {
203+
204+
MongoException exception = new MongoWriteException(new WriteError(112, "WriteConflict", new BsonDocument()), null);
205+
exception.addLabel(MongoException.UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL);
206+
207+
DataAccessException translatedException = translator.translateExceptionIfPossible(exception);
208+
209+
assertThat(translatedException).isInstanceOf(TransientMongoDbException.class);
210+
assertThat(translatedException.getCause()).isInstanceOf(DataIntegrityViolationException.class);
211+
}
212+
174213
private void checkTranslatedMongoException(Class<? extends Exception> clazz, int code) {
175214

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

0 commit comments

Comments
 (0)