Skip to content

Commit 4fc3436

Browse files
committed
JAVA-2815: Retry a commit or abort of a transaction once
1 parent 66479fe commit 4fc3436

File tree

12 files changed

+4164
-53
lines changed

12 files changed

+4164
-53
lines changed

driver-async/src/main/com/mongodb/async/client/ClientSessionImpl.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.mongodb.async.client;
1818

1919
import com.mongodb.ClientSessionOptions;
20+
import com.mongodb.MongoException;
2021
import com.mongodb.MongoInternalException;
2122
import com.mongodb.ReadConcern;
2223
import com.mongodb.TransactionOptions;
@@ -26,6 +27,8 @@
2627
import com.mongodb.operation.AbortTransactionOperation;
2728
import com.mongodb.operation.CommitTransactionOperation;
2829

30+
import static com.mongodb.MongoException.TRANSIENT_TRANSACTION_ERROR_LABEL;
31+
import static com.mongodb.MongoException.UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL;
2932
import static com.mongodb.assertions.Assertions.isTrue;
3033
import static com.mongodb.assertions.Assertions.notNull;
3134

@@ -117,6 +120,13 @@ public void commitTransaction(final SingleResultCallback<Void> callback) {
117120
public void onResult(final Void result, final Throwable t) {
118121
commitInProgress = false;
119122
transactionState = TransactionState.COMMITTED;
123+
if (t instanceof MongoException) {
124+
MongoException e = (MongoException) t;
125+
if (e.hasErrorLabel(TRANSIENT_TRANSACTION_ERROR_LABEL)) {
126+
e.removeLabel(TRANSIENT_TRANSACTION_ERROR_LABEL);
127+
e.addLabel(UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL);
128+
}
129+
}
120130
callback.onResult(result, t);
121131
}
122132
});

driver-async/src/test/functional/com/mongodb/async/client/TransactionsTest.java

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -291,17 +291,28 @@ public void execute() {
291291
hasErrorCodeNameField(expectedResult));
292292
} catch (RuntimeException e) {
293293
boolean passedAssertion = false;
294-
if (hasErrorLabelContainsField(expectedResult)) {
294+
if (hasErrorLabelsContainField(expectedResult)) {
295295
if (e instanceof MongoException) {
296296
MongoException mongoException = (MongoException) e;
297-
for (String curErrorLabel : getErrorLabelContainsField(expectedResult)) {
297+
for (String curErrorLabel : getErrorLabelsContainField(expectedResult)) {
298298
assertTrue(String.format("Expected error label '%s but found labels '%s'", curErrorLabel,
299299
mongoException.getErrorLabels()),
300300
mongoException.hasErrorLabel(curErrorLabel));
301301
}
302302
passedAssertion = true;
303303
}
304304
}
305+
if (hasErrorLabelsOmitField(expectedResult)) {
306+
if (e instanceof MongoException) {
307+
MongoException mongoException = (MongoException) e;
308+
for (String curErrorLabel : getErrorLabelsOmitField(expectedResult)) {
309+
assertFalse(String.format("Expected error label '%s omitted but found labels '%s'", curErrorLabel,
310+
mongoException.getErrorLabels()),
311+
mongoException.hasErrorLabel(curErrorLabel));
312+
}
313+
passedAssertion = true;
314+
}
315+
}
305316
if (hasErrorContainsField(expectedResult)) {
306317
String expectedError = getErrorContainsField(expectedResult);
307318
assertTrue(String.format("Expected '%s' but got '%s'", expectedError, e.getMessage()),
@@ -358,13 +369,26 @@ private String getErrorField(final BsonValue expectedResult, final String key) {
358369
}
359370
}
360371

361-
private boolean hasErrorLabelContainsField(final BsonValue expectedResult) {
372+
private boolean hasErrorLabelsContainField(final BsonValue expectedResult) {
362373
return hasErrorField(expectedResult, "errorLabelsContain");
363374
}
364375

365-
private List<String> getErrorLabelContainsField(final BsonValue expectedResult) {
376+
private List<String> getErrorLabelsContainField(final BsonValue expectedResult) {
377+
return getListOfStringsFromBsonArrays(expectedResult.asDocument(), "errorLabelsContain");
378+
}
379+
380+
private boolean hasErrorLabelsOmitField(final BsonValue expectedResult) {
381+
return hasErrorField(expectedResult, "errorLabelsOmit");
382+
}
383+
384+
private List<String> getErrorLabelsOmitField(final BsonValue expectedResult) {
385+
return getListOfStringsFromBsonArrays(expectedResult.asDocument(), "errorLabelsOmit");
386+
}
387+
388+
389+
private List<String> getListOfStringsFromBsonArrays(final BsonDocument expectedResult, final String arrayFieldName) {
366390
List<String> errorLabelContainsList = new ArrayList<String>();
367-
for (BsonValue cur : expectedResult.asDocument().getArray("errorLabelsContain")) {
391+
for (BsonValue cur : expectedResult.asDocument().getArray(arrayFieldName)) {
368392
errorLabelContainsList.add(cur.asString().getValue());
369393
}
370394
return errorLabelContainsList;

driver-core/src/main/com/mongodb/MongoException.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@ public class MongoException extends RuntimeException {
3737
*/
3838
public static final String TRANSIENT_TRANSACTION_ERROR_LABEL = "TransientTransactionError";
3939

40+
/**
41+
* An error label indicating that the exception can be treated as an unknown transaction commit result.
42+
*
43+
* @see #hasErrorLabel(String)
44+
* @since 3.8
45+
*/
46+
public static final String UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL = "UnknownTransactionCommitResult";
47+
4048
private static final long serialVersionUID = -4415279469780082174L;
4149

4250
private final int code;
@@ -129,6 +137,18 @@ public void addLabel(final String errorLabel) {
129137
errorLabels.add(errorLabel);
130138
}
131139

140+
/**
141+
* Removes the given error label from the exception.
142+
*
143+
* @param errorLabel the non-null error label to remove from the exception
144+
*
145+
* @since 3.8
146+
*/
147+
public void removeLabel(final String errorLabel) {
148+
notNull("errorLabel", errorLabel);
149+
errorLabels.remove(errorLabel);
150+
}
151+
132152
/**
133153
* Gets the set of error labels associated with this exception.
134154
*
@@ -150,4 +170,5 @@ public boolean hasErrorLabel(final String errorLabel) {
150170
notNull("errorLabel", errorLabel);
151171
return errorLabels.contains(errorLabel);
152172
}
173+
153174
}

driver-core/src/main/com/mongodb/operation/CommandOperationHelper.java

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -453,7 +453,7 @@ public R call(final ConnectionSource source, final Connection connection) {
453453
commandResultDecoder, binding.getSessionContext()), connection.getDescription().getServerAddress());
454454
} catch (MongoException e) {
455455
exception = e;
456-
if (!shouldAttemptToRetry(command, e, binding.getSessionContext())) {
456+
if (!shouldAttemptToRetry(command, e)) {
457457
throw exception;
458458
}
459459
} finally {
@@ -544,7 +544,7 @@ public void onResult(final T result, final Throwable originalError) {
544544
}
545545

546546
private void checkRetryableException(final Throwable originalError, final SingleResultCallback<R> releasingCallback) {
547-
if (!shouldAttemptToRetry(command, originalError, binding.getSessionContext())) {
547+
if (!shouldAttemptToRetry(command, originalError)) {
548548
releasingCallback.onResult(null, originalError);
549549
} else {
550550
oldConnection.release();
@@ -700,14 +700,15 @@ public void onResult(final D response, final Throwable t) {
700700
}
701701
}
702702

703-
private static boolean shouldAttemptToRetry(@Nullable final BsonDocument command, final Throwable exception,
704-
final SessionContext sessionContext) {
705-
return shouldAttemptToRetry(command != null && command.containsKey("txnNumber"), exception, sessionContext);
703+
private static boolean shouldAttemptToRetry(@Nullable final BsonDocument command, final Throwable exception) {
704+
return shouldAttemptToRetry(command != null
705+
&& (command.containsKey("txnNumber")
706+
|| command.getFirstKey().equals("commitTransaction") || command.getFirstKey().equals("abortTransaction")),
707+
exception);
706708
}
707709

708-
static boolean shouldAttemptToRetry(final boolean retryWritesEnabled, final Throwable exception,
709-
final SessionContext sessionContext) {
710-
return retryWritesEnabled && isRetryableException(exception) && !sessionContext.hasActiveTransaction();
710+
static boolean shouldAttemptToRetry(final boolean retryWritesEnabled, final Throwable exception) {
711+
return retryWritesEnabled && isRetryableException(exception);
711712
}
712713

713714
private CommandOperationHelper() {

driver-core/src/main/com/mongodb/operation/MixedBulkWriteOperation.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ private BulkWriteResult executeBulkWriteBatch(final WriteBinding binding, final
258258
if (retryWrites && !binding.getSessionContext().hasActiveTransaction()) {
259259
MongoException writeConcernBasedError = ProtocolHelper.createSpecialException(result,
260260
connection.getDescription().getServerAddress(), "errMsg");
261-
if (writeConcernBasedError != null && shouldAttemptToRetry(true, writeConcernBasedError, binding.getSessionContext())) {
261+
if (writeConcernBasedError != null && shouldAttemptToRetry(true, writeConcernBasedError)) {
262262
throw new MongoWriteConcernWithResponseException(writeConcernBasedError, result);
263263
}
264264
}
@@ -275,7 +275,7 @@ private BulkWriteResult executeBulkWriteBatch(final WriteBinding binding, final
275275
if (exception == null) {
276276
return currentBatch.getResult();
277277
} else if (!(exception instanceof MongoWriteConcernWithResponseException)
278-
&& !shouldAttemptToRetry(originalBatch.getRetryWrites(), exception, binding.getSessionContext())) {
278+
&& !shouldAttemptToRetry(originalBatch.getRetryWrites(), exception)) {
279279
throw exception;
280280
} else {
281281
return retryExecuteBatches(binding, currentBatch, exception);
@@ -444,7 +444,7 @@ private SingleResultCallback<BsonDocument> getCommandCallback(final AsyncWriteBi
444444
@Override
445445
public void onResult(final BsonDocument result, final Throwable t) {
446446
if (t != null) {
447-
if (isSecondAttempt || !shouldAttemptToRetry(retryWrites, t, binding.getSessionContext())) {
447+
if (isSecondAttempt || !shouldAttemptToRetry(retryWrites, t)) {
448448
if (t instanceof MongoWriteConcernWithResponseException) {
449449
addBatchResult((BsonDocument) ((MongoWriteConcernWithResponseException) t).getResponse(), binding, connection,
450450
batch, retryWrites, callback);
@@ -458,8 +458,7 @@ public void onResult(final BsonDocument result, final Throwable t) {
458458
if (retryWrites && !isSecondAttempt) {
459459
MongoException writeConcernBasedError = ProtocolHelper.createSpecialException(result,
460460
connection.getDescription().getServerAddress(), "errMsg");
461-
if (writeConcernBasedError != null && shouldAttemptToRetry(true, writeConcernBasedError,
462-
binding.getSessionContext())) {
461+
if (writeConcernBasedError != null && shouldAttemptToRetry(true, writeConcernBasedError)) {
463462
retryExecuteBatchesAsync(binding, batch,
464463
new MongoWriteConcernWithResponseException(writeConcernBasedError, result),
465464
callback.releaseConnectionAndGetWrapped());

driver-core/src/main/com/mongodb/operation/TransactionOperation.java

Lines changed: 18 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -20,22 +20,20 @@
2020
import com.mongodb.async.SingleResultCallback;
2121
import com.mongodb.binding.AsyncWriteBinding;
2222
import com.mongodb.binding.WriteBinding;
23-
import com.mongodb.connection.AsyncConnection;
24-
import com.mongodb.connection.Connection;
25-
import com.mongodb.operation.OperationHelper.AsyncCallableWithConnection;
26-
import com.mongodb.operation.OperationHelper.CallableWithConnection;
23+
import com.mongodb.connection.ConnectionDescription;
24+
import com.mongodb.connection.ServerDescription;
25+
import com.mongodb.internal.validator.NoOpFieldNameValidator;
26+
import com.mongodb.operation.CommandOperationHelper.CommandCreator;
2727
import org.bson.BsonDocument;
2828
import org.bson.BsonInt32;
29+
import org.bson.codecs.BsonDocumentCodec;
2930

3031
import static com.mongodb.assertions.Assertions.isTrue;
3132
import static com.mongodb.assertions.Assertions.notNull;
3233
import static com.mongodb.internal.async.ErrorHandlingResultCallback.errorHandlingCallback;
33-
import static com.mongodb.operation.CommandOperationHelper.executeWrappedCommandProtocol;
34-
import static com.mongodb.operation.CommandOperationHelper.executeWrappedCommandProtocolAsync;
34+
import static com.mongodb.operation.CommandOperationHelper.executeRetryableCommand;
3535
import static com.mongodb.operation.CommandOperationHelper.writeConcernErrorTransformer;
3636
import static com.mongodb.operation.OperationHelper.LOGGER;
37-
import static com.mongodb.operation.OperationHelper.releasingCallback;
38-
import static com.mongodb.operation.OperationHelper.withConnection;
3937

4038
/**
4139
* A base class for transaction-related operations
@@ -66,31 +64,25 @@ public WriteConcern getWriteConcern() {
6664
@Override
6765
public Void execute(final WriteBinding binding) {
6866
isTrue("in transaction", binding.getSessionContext().hasActiveTransaction());
69-
return withConnection(binding, new CallableWithConnection<Void>() {
70-
@Override
71-
public Void call(final Connection connection) {
72-
executeWrappedCommandProtocol(binding, "admin", getCommand(), connection, writeConcernErrorTransformer());
73-
return null;
74-
}
75-
});
67+
return executeRetryableCommand(binding, "admin", null, new NoOpFieldNameValidator(),
68+
new BsonDocumentCodec(), getCommandCreator(), writeConcernErrorTransformer());
7669
}
7770

7871
@Override
7972
public void executeAsync(final AsyncWriteBinding binding, final SingleResultCallback<Void> callback) {
8073
isTrue("in transaction", binding.getSessionContext().hasActiveTransaction());
81-
withConnection(binding, new AsyncCallableWithConnection() {
82-
@Override
83-
public void call(final AsyncConnection connection, final Throwable t) {
84-
SingleResultCallback<Void> errHandlingCallback = errorHandlingCallback(callback, LOGGER);
85-
if (t != null) {
86-
errHandlingCallback.onResult(null, t);
87-
} else {
88-
executeWrappedCommandProtocolAsync(binding, "admin", getCommand(), connection,
89-
writeConcernErrorTransformer(), releasingCallback(errHandlingCallback, connection));
74+
executeRetryableCommand(binding, "admin", null, new NoOpFieldNameValidator(),
75+
new BsonDocumentCodec(), getCommandCreator(), writeConcernErrorTransformer(),
76+
errorHandlingCallback(callback, LOGGER));
77+
}
9078

91-
}
79+
private CommandCreator getCommandCreator() {
80+
return new CommandCreator() {
81+
@Override
82+
public BsonDocument create(final ServerDescription serverDescription, final ConnectionDescription connectionDescription) {
83+
return getCommand();
9284
}
93-
});
85+
};
9486
}
9587

9688
private BsonDocument getCommand() {

driver-core/src/test/functional/com/mongodb/client/test/CollectionHelper.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -368,8 +368,12 @@ public List<BsonDocument> listIndexes(){
368368
}
369369

370370
public void killAllSessions() {
371-
new CommandWriteOperation<BsonDocument>("admin", new BsonDocument("killAllSessions", new BsonArray()),
372-
new BsonDocumentCodec());
371+
try {
372+
new CommandWriteOperation<BsonDocument>("admin", new BsonDocument("killAllSessions", new BsonArray()),
373+
new BsonDocumentCodec()).execute(getBinding());
374+
} catch (MongoCommandException e) {
375+
// ignore exception caused by killing the implicit session that the killAllSessions command itself is running in
376+
}
373377
}
374378

375379
public void runAdminCommand(final BsonDocument command) {

driver-core/src/test/resources/transactions/README.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,8 @@ Each YAML file has the following keys:
109109
- ``arguments``: Optional, the names and values of arguments.
110110

111111
- ``result``: The return value from the operation, if any. If the
112-
operation is expected to return an error, the ``result`` has one of
113-
the following fields:
112+
operation is expected to return an error, the ``result`` has one or more
113+
of the following fields:
114114

115115
- ``errorContains``: A substring of the expected error message.
116116

0 commit comments

Comments
 (0)