Skip to content

Commit 1746ad5

Browse files
committed
JAVA-2815: Add ability to retry commitTransaction
1 parent 66e8e79 commit 1746ad5

File tree

6 files changed

+397
-55
lines changed

6 files changed

+397
-55
lines changed

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

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,15 @@
3131

3232
class ClientSessionImpl extends BaseClientSessionImpl implements ClientSession {
3333

34+
private enum TransactionState {
35+
NONE, IN, DONE, ABORTED
36+
}
37+
3438
private final OperationExecutor executor;
35-
private boolean inTransaction;
39+
private TransactionState transactionState = TransactionState.NONE;
3640
private boolean messageSent;
41+
private boolean commitInProgress;
42+
3743
private TransactionOptions transactionOptions;
3844

3945
ClientSessionImpl(final ServerSessionPool serverSessionPool, final MongoClient mongoClient, final ClientSessionOptions options,
@@ -44,7 +50,7 @@ class ClientSessionImpl extends BaseClientSessionImpl implements ClientSession {
4450

4551
@Override
4652
public boolean hasActiveTransaction() {
47-
return inTransaction;
53+
return transactionState == TransactionState.IN || (transactionState == TransactionState.DONE && commitInProgress);
4854
}
4955

5056
@Override
@@ -56,7 +62,7 @@ public boolean notifyMessageSent() {
5662

5763
@Override
5864
public TransactionOptions getTransactionOptions() {
59-
isTrue("in transaction", inTransaction);
65+
isTrue("in transaction", transactionState == TransactionState.IN || transactionState == TransactionState.DONE);
6066
return transactionOptions;
6167
}
6268

@@ -68,32 +74,42 @@ public void startTransaction() {
6874
@Override
6975
public void startTransaction(final TransactionOptions transactionOptions) {
7076
notNull("transactionOptions", transactionOptions);
71-
if (inTransaction) {
77+
if (transactionState == TransactionState.IN) {
7278
throw new IllegalStateException("Transaction already in progress");
7379
}
74-
inTransaction = true;
80+
if (transactionState == TransactionState.DONE) {
81+
cleanupTransaction(TransactionState.IN);
82+
} else {
83+
transactionState = TransactionState.IN;
84+
}
85+
getServerSession().advanceTransactionNumber();
7586
this.transactionOptions = TransactionOptions.merge(transactionOptions, getOptions().getDefaultTransactionOptions());
7687
}
7788

7889
@Override
7990
public void commitTransaction(final SingleResultCallback<Void> callback) {
80-
if (!canCommitOrAbort()) {
91+
if (transactionState == TransactionState.ABORTED) {
92+
throw new IllegalStateException("Cannot call commitTransaction after calling abortTransaction");
93+
}
94+
if (transactionState == TransactionState.NONE) {
8195
throw new IllegalStateException("There is no transaction started");
8296
}
8397
if (!messageSent) {
84-
cleanupTransaction();
98+
cleanupTransaction(TransactionState.DONE);
8599
callback.onResult(null, null);
86100
} else {
87101
ReadConcern readConcern = transactionOptions.getReadConcern();
88102
if (readConcern == null) {
89103
throw new MongoInternalException("Invariant violated. Transaction options read concern can not be null");
90104
}
105+
commitInProgress = true;
91106
executor.execute(new CommitTransactionOperation(transactionOptions.getWriteConcern()),
92107
readConcern, this,
93108
new SingleResultCallback<Void>() {
94109
@Override
95110
public void onResult(final Void result, final Throwable t) {
96-
cleanupTransaction();
111+
commitInProgress = false;
112+
transactionState = TransactionState.DONE;
97113
callback.onResult(result, t);
98114
}
99115
});
@@ -102,11 +118,17 @@ public void onResult(final Void result, final Throwable t) {
102118

103119
@Override
104120
public void abortTransaction(final SingleResultCallback<Void> callback) {
105-
if (!canCommitOrAbort()) {
121+
if (transactionState == TransactionState.ABORTED) {
122+
throw new IllegalStateException("Cannot call abortTransaction twice");
123+
}
124+
if (transactionState == TransactionState.DONE) {
125+
throw new IllegalStateException("Cannot call abortTransaction after calling commitTransaction");
126+
}
127+
if (transactionState == TransactionState.NONE) {
106128
throw new IllegalStateException("There is no transaction started");
107129
}
108130
if (!messageSent) {
109-
cleanupTransaction();
131+
cleanupTransaction(TransactionState.ABORTED);
110132
callback.onResult(null, null);
111133
} else {
112134
ReadConcern readConcern = transactionOptions.getReadConcern();
@@ -118,21 +140,17 @@ public void abortTransaction(final SingleResultCallback<Void> callback) {
118140
new SingleResultCallback<Void>() {
119141
@Override
120142
public void onResult(final Void result, final Throwable t) {
121-
cleanupTransaction();
143+
cleanupTransaction(TransactionState.ABORTED);
122144
callback.onResult(null, null);
123145
}
124146
});
125147
}
126148
}
127149

128-
private boolean canCommitOrAbort() {
129-
return inTransaction;
130-
}
131-
132150
// TODO: should there be a version of this that takes a callback?
133151
@Override
134152
public void close() {
135-
if (inTransaction) {
153+
if (transactionState == TransactionState.IN) {
136154
abortTransaction(new SingleResultCallback<Void>() {
137155
@Override
138156
public void onResult(final Void result, final Throwable t) {
@@ -144,10 +162,9 @@ public void onResult(final Void result, final Throwable t) {
144162
}
145163
}
146164

147-
private void cleanupTransaction() {
148-
inTransaction = false;
165+
private void cleanupTransaction(final TransactionState nextState) {
149166
messageSent = false;
150167
transactionOptions = null;
151-
getServerSession().advanceTransactionNumber();
168+
transactionState = nextState;
152169
}
153170
}

driver-core/src/main/com/mongodb/internal/session/ServerSessionPool.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ private boolean shouldPrune(final ServerSessionImpl serverSession) {
146146

147147
final class ServerSessionImpl implements ServerSession {
148148
private final BsonDocument identifier;
149-
private long transactionNumber = 1;
149+
private long transactionNumber = 0;
150150
private volatile long lastUsedAtMillis = clock.millis();
151151
private volatile boolean closed;
152152

@@ -175,7 +175,8 @@ public BsonDocument getIdentifier() {
175175

176176
@Override
177177
public long advanceTransactionNumber() {
178-
return transactionNumber++;
178+
transactionNumber++;
179+
return transactionNumber;
179180
}
180181

181182
@Override

driver-core/src/test/resources/transactions/abort.json

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@
245245
"session": "session0"
246246
},
247247
"result": {
248-
"errorContains": "no transaction started"
248+
"errorContains": "cannot call abortTransaction twice"
249249
}
250250
}
251251
],
@@ -317,7 +317,39 @@
317317
}
318318
},
319319
{
320-
"description": "commit after abort",
320+
"description": "abort directly after no-op commit",
321+
"operations": [
322+
{
323+
"name": "startTransaction",
324+
"arguments": {
325+
"session": "session0"
326+
}
327+
},
328+
{
329+
"name": "commitTransaction",
330+
"arguments": {
331+
"session": "session0"
332+
}
333+
},
334+
{
335+
"name": "abortTransaction",
336+
"arguments": {
337+
"session": "session0"
338+
},
339+
"result": {
340+
"errorContains": "Cannot call abortTransaction after calling commitTransaction"
341+
}
342+
}
343+
],
344+
"expectations": [],
345+
"outcome": {
346+
"collection": {
347+
"data": []
348+
}
349+
}
350+
},
351+
{
352+
"description": "abort directly after commit",
321353
"operations": [
322354
{
323355
"name": "startTransaction",
@@ -338,18 +370,18 @@
338370
}
339371
},
340372
{
341-
"name": "abortTransaction",
373+
"name": "commitTransaction",
342374
"arguments": {
343375
"session": "session0"
344376
}
345377
},
346378
{
347-
"name": "commitTransaction",
379+
"name": "abortTransaction",
348380
"arguments": {
349381
"session": "session0"
350382
},
351383
"result": {
352-
"errorContains": "no transaction started"
384+
"errorContains": "Cannot call abortTransaction after calling commitTransaction"
353385
}
354386
}
355387
],
@@ -380,7 +412,7 @@
380412
{
381413
"command_started_event": {
382414
"command": {
383-
"abortTransaction": 1,
415+
"commitTransaction": 1,
384416
"lsid": "session0",
385417
"txnNumber": {
386418
"$numberLong": "1"
@@ -389,11 +421,20 @@
389421
"autocommit": false,
390422
"writeConcern": null
391423
},
392-
"command_name": "abortTransaction",
424+
"command_name": "commitTransaction",
393425
"database_name": "admin"
394426
}
395427
}
396-
]
428+
],
429+
"outcome": {
430+
"collection": {
431+
"data": [
432+
{
433+
"_id": 1
434+
}
435+
]
436+
}
437+
}
397438
},
398439
{
399440
"description": "abort ignores TransactionAborted",

0 commit comments

Comments
 (0)