Skip to content
This repository was archived by the owner on Apr 7, 2026. It is now read-only.

Commit b5393d1

Browse files
committed
feat: make transaction retry configurable
1 parent 9263972 commit b5393d1

6 files changed

Lines changed: 69 additions & 40 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ public CommitResponse writeAtLeastOnceWithOptions(
301301
ISpan span = tracer.spanBuilder(SpannerImpl.COMMIT);
302302

303303
try (IScope s = tracer.withSpan(span)) {
304-
return SpannerRetryHelper.runTxWithRetriesOnAborted(
304+
return spanner.getTransactionRetryHelper().runTxWithRetriesOnAborted(
305305
() -> new CommitResponse(spanner.getRpc().commit(request, getOptions())));
306306
} catch (RuntimeException e) {
307307
span.setStatus(e);

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ private static String nextDatabaseClientId(DatabaseId databaseId) {
116116

117117
private final DatabaseAdminClient dbAdminClient;
118118
private final InstanceAdminClient instanceClient;
119+
private final TransactionRetryHelper transactionRetryHelper;
119120

120121
/**
121122
* Exception class used to track the stack trace at the point when a Spanner instance is closed.
@@ -145,6 +146,7 @@ static final class ClosedException extends RuntimeException {
145146
this.dbAdminClient = new DatabaseAdminClientImpl(options.getProjectId(), gapicRpc);
146147
this.instanceClient =
147148
new InstanceAdminClientImpl(options.getProjectId(), gapicRpc, dbAdminClient);
149+
this.transactionRetryHelper = new TransactionRetryHelper(options.getDefaultTransactionRetrySettings());
148150
logSpannerOptions(options);
149151
}
150152

@@ -200,6 +202,10 @@ SpannerRpc getRpc() {
200202
return gapicRpc;
201203
}
202204

205+
TransactionRetryHelper getTransactionRetryHelper() {
206+
return transactionRetryHelper;
207+
}
208+
203209
/** Returns the default setting for prefetchChunks of this {@link SpannerImpl} instance. */
204210
int getDefaultPrefetchChunks() {
205211
return getOptions().getPrefetchChunks();

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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
import com.google.cloud.spanner.spi.v1.GapicSpannerRpc;
5959
import com.google.cloud.spanner.spi.v1.SpannerRpc;
6060
import com.google.cloud.spanner.v1.SpannerSettings;
61+
import com.google.cloud.spanner.v1.stub.SpannerStub;
6162
import com.google.cloud.spanner.v1.stub.SpannerStubSettings;
6263
import com.google.common.annotations.VisibleForTesting;
6364
import com.google.common.base.MoreObjects;
@@ -70,6 +71,7 @@
7071
import com.google.spanner.v1.ExecuteSqlRequest;
7172
import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions;
7273
import com.google.spanner.v1.RequestOptions;
74+
import com.google.spanner.v1.RollbackRequest;
7375
import com.google.spanner.v1.SpannerGrpc;
7476
import com.google.spanner.v1.TransactionOptions;
7577
import com.google.spanner.v1.TransactionOptions.IsolationLevel;
@@ -204,6 +206,25 @@ public static GcpChannelPoolOptions createDefaultDynamicChannelPoolOptions() {
204206
.build();
205207
}
206208

209+
/**
210+
* Use the same {@link RetrySettings} for retrying an aborted transaction as for retrying a {@link
211+
* RollbackRequest}. The {@link RollbackRequest} automatically uses the default retry settings
212+
* defined for the {@link SpannerStub}. By referencing these settings, the retry settings for
213+
* retrying aborted transactions will also automatically be updated if the default retry settings
214+
* are updated.
215+
*
216+
* <p>A read/write transaction should not time out while retrying. The total timeout of the retry
217+
* settings is therefore set to 24 hours and there is no max attempts value.
218+
*
219+
* <p>These default {@link RetrySettings} are only used if no retry information is returned by the
220+
* {@link AbortedException}.
221+
*/
222+
public static final RetrySettings DEFAULT_TRANSACTION_RETRY_SETTINGS =
223+
SpannerStubSettings.newBuilder().rollbackSettings().getRetrySettings().toBuilder()
224+
.setTotalTimeoutDuration(Duration.ofHours(24L))
225+
.setMaxAttempts(0)
226+
.build();
227+
207228
private final TransportChannelProvider channelProvider;
208229
private final ChannelEndpointCacheFactory channelEndpointCacheFactory;
209230

@@ -264,6 +285,7 @@ public static GcpChannelPoolOptions createDefaultDynamicChannelPoolOptions() {
264285
private final boolean enableEndToEndTracing;
265286
private final String monitoringHost;
266287
private final TransactionOptions defaultTransactionOptions;
288+
private final RetrySettings defaultTransactionRetrySettings;
267289
private final RequestOptions.ClientContext clientContext;
268290

269291
enum TracingFramework {
@@ -934,6 +956,7 @@ protected SpannerOptions(Builder builder) {
934956
enableEndToEndTracing = builder.enableEndToEndTracing;
935957
monitoringHost = builder.monitoringHost;
936958
defaultTransactionOptions = builder.defaultTransactionOptions;
959+
defaultTransactionRetrySettings = builder.defaultTransactionRetrySettings;
937960
clientContext = builder.clientContext;
938961
}
939962

@@ -1196,6 +1219,7 @@ public static class Builder
11961219
private String experimentalHost = null;
11971220
private boolean usePlainText = false;
11981221
private TransactionOptions defaultTransactionOptions = TransactionOptions.getDefaultInstance();
1222+
private RetrySettings defaultTransactionRetrySettings = TransactionRetryHelper.DEFAULT_TRANSACTION_RETRY_SETTINGS;
11991223
private RequestOptions.ClientContext clientContext;
12001224

12011225
private static String createCustomClientLibToken(String token) {
@@ -1302,6 +1326,7 @@ protected Builder() {
13021326
this.enableEndToEndTracing = options.enableEndToEndTracing;
13031327
this.monitoringHost = options.monitoringHost;
13041328
this.defaultTransactionOptions = options.defaultTransactionOptions;
1329+
this.defaultTransactionRetrySettings = options.defaultTransactionRetrySettings;
13051330
this.clientContext = options.clientContext;
13061331
}
13071332

@@ -2055,6 +2080,13 @@ public Builder setDefaultTransactionOptions(
20552080
return this;
20562081
}
20572082

2083+
/** Sets the default {@link RetrySettings} for all read/write transactions that are executed using this client. These settings are used when the client automatically retries an aborted read/write transaction. The default is to retry for up to 24 hours without a limit for the maximum number of attempts. */
2084+
public Builder setDefaultTransactionRetrySettings(RetrySettings retrySettings) {
2085+
Preconditions.checkNotNull(retrySettings, "RetrySettings cannot be null");
2086+
this.defaultTransactionRetrySettings = retrySettings;
2087+
return this;
2088+
}
2089+
20582090
/** Sets the default {@link RequestOptions.ClientContext} for all requests. */
20592091
public Builder setDefaultClientContext(RequestOptions.ClientContext clientContext) {
20602092
this.clientContext = clientContext;
@@ -2481,6 +2513,10 @@ public TransactionOptions getDefaultTransactionOptions() {
24812513
return defaultTransactionOptions;
24822514
}
24832515

2516+
public RetrySettings getDefaultTransactionRetrySettings() {
2517+
return this.defaultTransactionRetrySettings;
2518+
}
2519+
24842520
@BetaApi
24852521
public boolean isUseVirtualThreads() {
24862522
return useVirtualThreads;

google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerRetryHelper.java renamed to google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRetryHelper.java

Lines changed: 10 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -39,50 +39,35 @@
3939
* that uses specific settings to only retry on aborted transactions, without a timeout and without
4040
* a cap on the number of retries.
4141
*/
42-
class SpannerRetryHelper {
42+
class TransactionRetryHelper {
43+
private final RetrySettings retrySettings;
4344

44-
/**
45-
* Use the same {@link RetrySettings} for retrying an aborted transaction as for retrying a {@link
46-
* RollbackRequest}. The {@link RollbackRequest} automatically uses the default retry settings
47-
* defined for the {@link SpannerStub}. By referencing these settings, the retry settings for
48-
* retrying aborted transactions will also automatically be updated if the default retry settings
49-
* are updated.
50-
*
51-
* <p>A read/write transaction should not timeout while retrying. The total timeout of the retry
52-
* settings is therefore set to 24 hours and there is no max attempts value.
53-
*
54-
* <p>These default {@link RetrySettings} are only used if no retry information is returned by the
55-
* {@link AbortedException}.
56-
*/
57-
@VisibleForTesting
58-
static final RetrySettings txRetrySettings =
59-
SpannerStubSettings.newBuilder().rollbackSettings().getRetrySettings().toBuilder()
60-
.setTotalTimeoutDuration(Duration.ofHours(24L))
61-
.setMaxAttempts(0)
62-
.build();
45+
TransactionRetryHelper(RetrySettings retrySettings) {
46+
this.retrySettings = retrySettings;
47+
}
6348

6449
/** Executes the {@link Callable} and retries if it fails with an {@link AbortedException}. */
65-
static <T> T runTxWithRetriesOnAborted(Callable<T> callable) {
50+
<T> T runTxWithRetriesOnAborted(Callable<T> callable) {
6651
return runTxWithRetriesOnAborted(callable, DefaultErrorHandler.INSTANCE);
6752
}
6853

69-
static <T> T runTxWithRetriesOnAborted(Callable<T> callable, ErrorHandler errorHandler) {
54+
<T> T runTxWithRetriesOnAborted(Callable<T> callable, ErrorHandler errorHandler) {
7055
return runTxWithRetriesOnAborted(
71-
callable, errorHandler, txRetrySettings, NanoClock.getDefaultClock());
56+
callable, errorHandler, this.retrySettings, NanoClock.getDefaultClock());
7257
}
7358

7459
/**
7560
* Executes the {@link Callable} and retries if it fails with an {@link AbortedException} using
7661
* the specific {@link RetrySettings}.
7762
*/
7863
@VisibleForTesting
79-
static <T> T runTxWithRetriesOnAborted(
64+
<T> T runTxWithRetriesOnAborted(
8065
Callable<T> callable, RetrySettings retrySettings, ApiClock clock) {
8166
return runTxWithRetriesOnAborted(callable, DefaultErrorHandler.INSTANCE, retrySettings, clock);
8267
}
8368

8469
@VisibleForTesting
85-
static <T> T runTxWithRetriesOnAborted(
70+
<T> T runTxWithRetriesOnAborted(
8671
Callable<T> callable,
8772
ErrorHandler errorHandler,
8873
RetrySettings retrySettings,

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1357,7 +1357,7 @@ private <T> T runInternal(final TransactionCallable<T> txCallable) {
13571357
throw e;
13581358
}
13591359
};
1360-
return SpannerRetryHelper.runTxWithRetriesOnAborted(retryCallable, session.getErrorHandler());
1360+
return session.getSpanner().getTransactionRetryHelper().runTxWithRetriesOnAborted(retryCallable, session.getErrorHandler());
13611361
}
13621362

13631363
@Override

google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerRetryHelperTest.java renamed to google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionRetryHelperTest.java

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
import org.junit.runners.JUnit4;
4444

4545
@RunWith(JUnit4.class)
46-
public class SpannerRetryHelperTest {
46+
public class TransactionRetryHelperTest {
4747
private static class FakeClock implements ApiClock {
4848
private long currentTime;
4949

@@ -58,6 +58,8 @@ public long millisTime() {
5858
}
5959
}
6060

61+
private final TransactionRetryHelper retryHelper = new TransactionRetryHelper(SpannerOptions.DEFAULT_TRANSACTION_RETRY_SETTINGS);
62+
6163
@Test
6264
public void testRetryDoesNotTimeoutAfterTenMinutes() {
6365
final FakeClock clock = new FakeClock();
@@ -72,8 +74,8 @@ public void testRetryDoesNotTimeoutAfterTenMinutes() {
7274
};
7375
assertEquals(
7476
2,
75-
SpannerRetryHelper.runTxWithRetriesOnAborted(
76-
callable, SpannerRetryHelper.txRetrySettings, clock)
77+
retryHelper.runTxWithRetriesOnAborted(
78+
callable, SpannerOptions.DEFAULT_TRANSACTION_RETRY_SETTINGS, clock)
7779
.intValue());
7880
}
7981

@@ -93,8 +95,8 @@ public void testRetryDoesFailAfterMoreThanOneDay() {
9395
assertThrows(
9496
SpannerException.class,
9597
() ->
96-
SpannerRetryHelper.runTxWithRetriesOnAborted(
97-
callable, SpannerRetryHelper.txRetrySettings, clock));
98+
retryHelper.runTxWithRetriesOnAborted(
99+
callable, SpannerOptions.DEFAULT_TRANSACTION_RETRY_SETTINGS, clock));
98100
assertEquals(ErrorCode.ABORTED, e.getErrorCode());
99101
assertEquals(1, attempts.get());
100102
}
@@ -119,7 +121,7 @@ public void testCancelledContext() {
119121
assertThrows(
120122
SpannerException.class,
121123
() ->
122-
withCancellation.run(() -> SpannerRetryHelper.runTxWithRetriesOnAborted(callable)));
124+
withCancellation.run(() -> retryHelper.runTxWithRetriesOnAborted(callable)));
123125
assertEquals(ErrorCode.CANCELLED, e.getErrorCode());
124126
}
125127

@@ -135,14 +137,14 @@ public void testTimedOutContext() {
135137
SpannerException e =
136138
assertThrows(
137139
SpannerException.class,
138-
() -> withDeadline.run(() -> SpannerRetryHelper.runTxWithRetriesOnAborted(callable)));
140+
() -> withDeadline.run(() -> retryHelper.runTxWithRetriesOnAborted(callable)));
139141
assertEquals(ErrorCode.DEADLINE_EXCEEDED, e.getErrorCode());
140142
}
141143

142144
@Test
143145
public void noException() {
144146
Callable<Integer> callable = () -> 1 + 1;
145-
assertThat(SpannerRetryHelper.runTxWithRetriesOnAborted(callable)).isEqualTo(2);
147+
assertThat(retryHelper.runTxWithRetriesOnAborted(callable)).isEqualTo(2);
146148
}
147149

148150
@Test(expected = IllegalStateException.class)
@@ -151,7 +153,7 @@ public void propagateUncheckedException() {
151153
() -> {
152154
throw new IllegalStateException("test");
153155
};
154-
SpannerRetryHelper.runTxWithRetriesOnAborted(callable);
156+
retryHelper.runTxWithRetriesOnAborted(callable);
155157
}
156158

157159
@Test
@@ -164,7 +166,7 @@ public void retryOnAborted() {
164166
}
165167
return 1 + 1;
166168
};
167-
assertThat(SpannerRetryHelper.runTxWithRetriesOnAborted(callable)).isEqualTo(2);
169+
assertThat(retryHelper.runTxWithRetriesOnAborted(callable)).isEqualTo(2);
168170
}
169171

170172
@Test
@@ -177,7 +179,7 @@ public void retryMultipleTimesOnAborted() {
177179
}
178180
return 1 + 1;
179181
};
180-
assertThat(SpannerRetryHelper.runTxWithRetriesOnAborted(callable)).isEqualTo(2);
182+
assertThat(retryHelper.runTxWithRetriesOnAborted(callable)).isEqualTo(2);
181183
}
182184

183185
@Test(expected = IllegalStateException.class)
@@ -190,7 +192,7 @@ public void retryOnAbortedAndThenPropagateUnchecked() {
190192
}
191193
throw new IllegalStateException("test");
192194
};
193-
SpannerRetryHelper.runTxWithRetriesOnAborted(callable);
195+
retryHelper.runTxWithRetriesOnAborted(callable);
194196
}
195197

196198
@Test
@@ -238,7 +240,7 @@ public void testExceptionWithRetryInfo() {
238240
// The following call should take at least 100ms, as that is the retry delay specified in the
239241
// retry info of the exception.
240242
Stopwatch watch = Stopwatch.createStarted();
241-
assertThat(SpannerRetryHelper.runTxWithRetriesOnAborted(callable)).isEqualTo(2);
243+
assertThat(retryHelper.runTxWithRetriesOnAborted(callable)).isEqualTo(2);
242244
long elapsed = watch.elapsed(TimeUnit.MILLISECONDS);
243245
// Allow 1ms difference as that should be the accuracy of the sleep method.
244246
assertThat(elapsed).isAtLeast(RETRY_DELAY_MILLIS - 1);

0 commit comments

Comments
 (0)