Skip to content

Commit 5783116

Browse files
authored
feat: support begin with AbortedException for manager interface (#3835)
Aborted transactions should ideally be retried in the same transaction manager instance because the client library will ensure to populate the "Prev txn attempt's txn id" on retry (for mux sessions) which helps preserve the lock order(priority) of the transaction in scenarios of high contention thats causing txn wounding. This is achieved by using `resetForRetry()` that automatically takes care of preserving the lock order. But if the customer application retries aborted transactions on a new TransactionManager instance with out using the `resetForRetry()`, we lose the lock order of the previous attempt. To address this scenario of preserving the lockorder across transaction manger instance for a retry of the same transaction Current proposal as per client lib team recommendation: 1. Overload the [Begin](https://togithub.com/googleapis/java-spanner/blob/9940b66dabe519495a69dcc0020e2587c16f7285/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionManager.java#L62) API in the TransactionManager to accept an AbortedException argument. 2. Client library will populate the aborted transaction's transaction ID in the [AbortedException](https://togithub.com/googleapis/java-spanner/blob/efb168015aee1c4cba16f666b89be19e6f04fdbe/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbortedException.java#L27) object which is propagated up the customer's application stack. 3. On the customer end, they will just need to pass in this AbortedException object to the "Begin" API when they retry the transaction on the new transaction manager instance. Note: This will be a no-op for regular sessions. b/407037309
1 parent 6e220ff commit 5783116

15 files changed

+405
-4
lines changed

google-cloud-spanner/clirr-ignored-differences.xml

+12
Original file line numberDiff line numberDiff line change
@@ -996,4 +996,16 @@
996996
<className>com/google/cloud/spanner/DatabaseClient</className>
997997
<method>com.google.cloud.spanner.Statement$StatementFactory getStatementFactory()</method>
998998
</difference>
999+
1000+
<!-- Add method begin() with AbortedException in TransactionManager and AsyncTransactionManager -->
1001+
<difference>
1002+
<differenceType>7012</differenceType>
1003+
<className>com/google/cloud/spanner/AsyncTransactionManager</className>
1004+
<method>com.google.cloud.spanner.AsyncTransactionManager$TransactionContextFuture beginAsync(com.google.cloud.spanner.AbortedException)</method>
1005+
</difference>
1006+
<difference>
1007+
<differenceType>7012</differenceType>
1008+
<className>com/google/cloud/spanner/TransactionManager</className>
1009+
<method>com.google.cloud.spanner.TransactionContext begin(com.google.cloud.spanner.AbortedException)</method>
1010+
</difference>
9991011
</differences>

google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbortedException.java

+14
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.google.cloud.spanner;
1818

1919
import com.google.api.gax.rpc.ApiException;
20+
import com.google.protobuf.ByteString;
2021
import javax.annotation.Nullable;
2122

2223
/**
@@ -32,6 +33,8 @@ public class AbortedException extends SpannerException {
3233
*/
3334
private static final boolean IS_RETRYABLE = false;
3435

36+
private ByteString transactionID;
37+
3538
/** Private constructor. Use {@link SpannerExceptionFactory} to create instances. */
3639
AbortedException(
3740
DoNotConstructDirectly token, @Nullable String message, @Nullable Throwable cause) {
@@ -46,6 +49,9 @@ public class AbortedException extends SpannerException {
4649
@Nullable ApiException apiException,
4750
@Nullable XGoogSpannerRequestId reqId) {
4851
super(token, ErrorCode.ABORTED, IS_RETRYABLE, message, cause, apiException, reqId);
52+
if (cause instanceof AbortedException) {
53+
this.transactionID = ((AbortedException) cause).getTransactionID();
54+
}
4955
}
5056

5157
/**
@@ -55,4 +61,12 @@ public class AbortedException extends SpannerException {
5561
public boolean isEmulatorOnlySupportsOneTransactionException() {
5662
return getMessage().endsWith("The emulator only supports one transaction at a time.");
5763
}
64+
65+
void setTransactionID(ByteString transactionID) {
66+
this.transactionID = transactionID;
67+
}
68+
69+
ByteString getTransactionID() {
70+
return this.transactionID;
71+
}
5872
}

google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManager.java

+15
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,21 @@ interface AsyncTransactionFunction<I, O> {
170170
*/
171171
TransactionContextFuture beginAsync();
172172

173+
/**
174+
* Initializes a new read-write transaction that is a retry of a previously aborted transaction.
175+
* This method must be called before performing any operations, and it can only be invoked once
176+
* per transaction lifecycle.
177+
*
178+
* <p>This method should only be used when multiplexed sessions are enabled to create a retry for
179+
* a previously aborted transaction. This method can be used instead of {@link
180+
* #resetForRetryAsync()} to create a retry. Using this method or {@link #resetForRetryAsync()}
181+
* will have the same effect. You must pass in the {@link AbortedException} from the previous
182+
* attempt to preserve the transaction's priority.
183+
*
184+
* <p>For regular sessions, this behaves the same as {@link #beginAsync()}.
185+
*/
186+
TransactionContextFuture beginAsync(AbortedException exception);
187+
173188
/**
174189
* Rolls back the currently active transaction. In most cases there should be no need to call this
175190
* explicitly since {@link #close()} would automatically roll back any active transaction.

google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManagerImpl.java

+16-3
Original file line numberDiff line numberDiff line change
@@ -76,14 +76,27 @@ public ApiFuture<Void> closeAsync() {
7676
@Override
7777
public TransactionContextFutureImpl beginAsync() {
7878
Preconditions.checkState(txn == null, "begin can only be called once");
79-
return new TransactionContextFutureImpl(this, internalBeginAsync(true));
79+
return new TransactionContextFutureImpl(this, internalBeginAsync(true, ByteString.EMPTY));
8080
}
8181

82-
private ApiFuture<TransactionContext> internalBeginAsync(boolean firstAttempt) {
82+
@Override
83+
public TransactionContextFutureImpl beginAsync(AbortedException exception) {
84+
Preconditions.checkState(txn == null, "begin can only be called once");
85+
Preconditions.checkNotNull(exception, "AbortedException from the previous attempt is required");
86+
ByteString abortedTransactionId =
87+
exception.getTransactionID() != null ? exception.getTransactionID() : ByteString.EMPTY;
88+
return new TransactionContextFutureImpl(this, internalBeginAsync(true, abortedTransactionId));
89+
}
90+
91+
private ApiFuture<TransactionContext> internalBeginAsync(
92+
boolean firstAttempt, ByteString abortedTransactionID) {
8393
txnState = TransactionState.STARTED;
8494

8595
// Determine the latest transactionId when using a multiplexed session.
8696
ByteString multiplexedSessionPreviousTransactionId = ByteString.EMPTY;
97+
if (firstAttempt && session.getIsMultiplexed()) {
98+
multiplexedSessionPreviousTransactionId = abortedTransactionID;
99+
}
87100
if (txn != null && session.getIsMultiplexed() && !firstAttempt) {
88101
// Use the current transactionId if available, otherwise fallback to the previous aborted
89102
// transactionId.
@@ -187,7 +200,7 @@ public TransactionContextFuture resetForRetryAsync() {
187200
throw new IllegalStateException(
188201
"resetForRetry can only be called if the previous attempt aborted");
189202
}
190-
return new TransactionContextFutureImpl(this, internalBeginAsync(false));
203+
return new TransactionContextFutureImpl(this, internalBeginAsync(false, ByteString.EMPTY));
191204
}
192205

193206
@Override

google-cloud-spanner/src/main/java/com/google/cloud/spanner/DelayedAsyncTransactionManager.java

+5
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ public TransactionContextFuture beginAsync() {
5050
return getAsyncTransactionManager().beginAsync();
5151
}
5252

53+
@Override
54+
public TransactionContextFuture beginAsync(AbortedException exception) {
55+
return getAsyncTransactionManager().beginAsync(exception);
56+
}
57+
5358
@Override
5459
public ApiFuture<Void> rollbackAsync() {
5560
return getAsyncTransactionManager().rollbackAsync();

google-cloud-spanner/src/main/java/com/google/cloud/spanner/DelayedTransactionManager.java

+5
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ public TransactionContext begin() {
4949
return getTransactionManager().begin();
5050
}
5151

52+
@Override
53+
public TransactionContext begin(AbortedException exception) {
54+
return getTransactionManager().begin(exception);
55+
}
56+
5257
@Override
5358
public void commit() {
5459
getTransactionManager().commit();

google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPool.java

+7
Original file line numberDiff line numberDiff line change
@@ -900,6 +900,13 @@ public TransactionContext begin() {
900900
return internalBegin();
901901
}
902902

903+
@Override
904+
public TransactionContext begin(AbortedException exception) {
905+
// For regular sessions, the input exception is ignored and the behavior is equivalent to
906+
// calling {@link #begin()}.
907+
return begin();
908+
}
909+
903910
private TransactionContext internalBegin() {
904911
TransactionContext res = new SessionPoolTransactionContext(this, delegate.begin());
905912
session.get().markUsed();

google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPoolAsyncTransactionManager.java

+7
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,13 @@ public void onSuccess(TransactionContext result) {
163163
return new TransactionContextFutureImpl(this, delegateTxnFuture);
164164
}
165165

166+
@Override
167+
public TransactionContextFuture beginAsync(AbortedException exception) {
168+
// For regular sessions, the input exception is ignored and the behavior is equivalent to
169+
// calling {@link #beginAsync()}.
170+
return beginAsync();
171+
}
172+
166173
@Override
167174
public void onError(Throwable t) {
168175
if (t instanceof AbortedException) {

google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionManager.java

+15
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,21 @@ enum TransactionState {
6161
*/
6262
TransactionContext begin();
6363

64+
/**
65+
* Initializes a new read-write transaction that is a retry of a previously aborted transaction.
66+
* This method must be called before performing any operations, and it can only be invoked once
67+
* per transaction lifecycle.
68+
*
69+
* <p>This method should only be used when multiplexed sessions are enabled to create a retry for
70+
* a previously aborted transaction. This method can be used instead of {@link #resetForRetry()}
71+
* to create a retry. Using this method or {@link #resetForRetry()} will have the same effect. You
72+
* must pass in the {@link AbortedException} from the previous attempt to preserve the
73+
* transaction's priority.
74+
*
75+
* <p>For regular sessions, this behaves the same as {@link #begin()}.
76+
*/
77+
TransactionContext begin(AbortedException exception);
78+
6479
/**
6580
* Commits the currently active transaction. If the transaction was already aborted, then this
6681
* would throw an {@link AbortedException}.

google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionManagerImpl.java

+14-1
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,21 @@ public void setSpan(ISpan span) {
5353
@Override
5454
public TransactionContext begin() {
5555
Preconditions.checkState(txn == null, "begin can only be called once");
56+
return begin(ByteString.EMPTY);
57+
}
58+
59+
@Override
60+
public TransactionContext begin(AbortedException exception) {
61+
Preconditions.checkState(txn == null, "begin can only be called once");
62+
Preconditions.checkNotNull(exception, "AbortedException from the previous attempt is required");
63+
ByteString previousAbortedTransactionID =
64+
exception.getTransactionID() != null ? exception.getTransactionID() : ByteString.EMPTY;
65+
return begin(previousAbortedTransactionID);
66+
}
67+
68+
TransactionContext begin(ByteString previousTransactionId) {
5669
try (IScope s = tracer.withSpan(span)) {
57-
txn = session.newTransaction(options, /* previousTransactionId = */ ByteString.EMPTY);
70+
txn = session.newTransaction(options, previousTransactionId);
5871
session.setActive(this);
5972
txnState = TransactionState.STARTED;
6073
return txn;

google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java

+5
Original file line numberDiff line numberDiff line change
@@ -793,6 +793,11 @@ public SpannerException onError(SpannerException e, boolean withBeginTransaction
793793
long delay = -1L;
794794
if (exceptionToThrow instanceof AbortedException) {
795795
delay = exceptionToThrow.getRetryDelayInMillis();
796+
((AbortedException) exceptionToThrow)
797+
.setTransactionID(
798+
this.transactionId != null
799+
? this.transactionId
800+
: this.getPreviousTransactionId());
796801
}
797802
if (delay == -1L) {
798803
txnLogger.log(

0 commit comments

Comments
 (0)