Skip to content

Commit e06eaf9

Browse files
committed
feat: add transaction_timeout connection property
Add a transaction_timeout property that sets a timeout for an entire read/write transaction. When a transaction is started, a deadline is calculated based on the current setting for transaction_timeout. The deadline is applied to all statements and the commit of the transaction.
1 parent abd4eae commit e06eaf9

17 files changed

+6082
-231
lines changed

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1043,4 +1043,14 @@
10431043
<className>com/google/cloud/spanner/connection/Connection</className>
10441044
<method>com.google.spanner.v1.TransactionOptions$ReadWrite$ReadLockMode getReadLockMode()</method>
10451045
</difference>
1046+
<difference>
1047+
<differenceType>7012</differenceType>
1048+
<className>com/google/cloud/spanner/connection/Connection</className>
1049+
<method>void setTransactionTimeout(java.time.Duration)</method>
1050+
</difference>
1051+
<difference>
1052+
<differenceType>7012</differenceType>
1053+
<className>com/google/cloud/spanner/connection/Connection</className>
1054+
<method>java.time.Duration getTransactionTimeout()</method>
1055+
</difference>
10461056
</differences>

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AbstractBaseUnitOfWork.java

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -358,22 +358,25 @@ <T> ApiFuture<T> executeStatementAsync(
358358
statement, StatementExecutionStep.EXECUTE_STATEMENT, this);
359359
}
360360
Context context = Context.current();
361-
if (statementTimeout.hasTimeout() && !applyStatementTimeoutToMethods.isEmpty()) {
362-
Deadline deadline =
363-
Deadline.after(
364-
statementTimeout.getTimeoutValue(TimeUnit.NANOSECONDS), TimeUnit.NANOSECONDS);
361+
Deadline transactionDeadline = getTransactionDeadline();
362+
Deadline statementDeadline =
363+
statementTimeout.hasTimeout()
364+
? Deadline.after(
365+
statementTimeout.getTimeoutValue(TimeUnit.NANOSECONDS), TimeUnit.NANOSECONDS)
366+
: null;
367+
Deadline effectiveDeadline = min(transactionDeadline, statementDeadline);
368+
if (effectiveDeadline != null && !applyStatementTimeoutToMethods.isEmpty()) {
365369
context =
366370
context.withValue(
367371
SpannerOptions.CALL_CONTEXT_CONFIGURATOR_KEY,
368372
new SpannerOptions.CallContextConfigurator() {
369373
@Override
370374
public <ReqT, RespT> ApiCallContext configure(
371375
ApiCallContext context, ReqT request, MethodDescriptor<ReqT, RespT> method) {
372-
if (statementTimeout.hasTimeout()
373-
&& applyStatementTimeoutToMethods.contains(method)) {
376+
if (applyStatementTimeoutToMethods.contains(method)) {
374377
// Calculate the remaining timeout. This method could be called multiple times
375378
// if the transaction is retried.
376-
long remainingTimeout = deadline.timeRemaining(TimeUnit.NANOSECONDS);
379+
long remainingTimeout = effectiveDeadline.timeRemaining(TimeUnit.NANOSECONDS);
377380
if (remainingTimeout <= 0) {
378381
remainingTimeout = 1;
379382
}
@@ -427,4 +430,23 @@ public void run() {
427430
return future;
428431
}
429432
}
433+
434+
@Nullable
435+
static Deadline min(@Nullable Deadline a, @Nullable Deadline b) {
436+
if (a == null && b == null) {
437+
return null;
438+
}
439+
if (a == null) {
440+
return b;
441+
}
442+
if (b == null) {
443+
return a;
444+
}
445+
return a.minimum(b);
446+
}
447+
448+
@Nullable
449+
Deadline getTransactionDeadline() {
450+
return null;
451+
}
430452
}

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/Connection.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,12 @@ public interface Connection extends AutoCloseable {
239239
/** Returns the read lock mode for read/write transactions for this connection. */
240240
ReadLockMode getReadLockMode();
241241

242+
/** Sets the timeout for read/write transactions. */
243+
void setTransactionTimeout(Duration timeout);
244+
245+
/** Returns the timeout for read/write transactions. */
246+
Duration getTransactionTimeout();
247+
242248
/**
243249
* Sets the duration the connection should wait before automatically aborting the execution of a
244250
* statement. The default is no timeout. Statement timeouts are applied all types of statements,

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionImpl.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
import static com.google.cloud.spanner.connection.ConnectionProperties.RPC_PRIORITY;
4646
import static com.google.cloud.spanner.connection.ConnectionProperties.SAVEPOINT_SUPPORT;
4747
import static com.google.cloud.spanner.connection.ConnectionProperties.TRACING_PREFIX;
48+
import static com.google.cloud.spanner.connection.ConnectionProperties.TRANSACTION_TIMEOUT;
4849

4950
import com.google.api.core.ApiFuture;
5051
import com.google.api.core.ApiFutures;
@@ -94,6 +95,8 @@
9495
import com.google.spanner.v1.ResultSetStats;
9596
import com.google.spanner.v1.TransactionOptions.IsolationLevel;
9697
import com.google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode;
98+
import io.grpc.Deadline;
99+
import io.grpc.Deadline.Ticker;
97100
import io.opentelemetry.api.OpenTelemetry;
98101
import io.opentelemetry.api.common.Attributes;
99102
import io.opentelemetry.api.common.AttributesBuilder;
@@ -254,6 +257,7 @@ static UnitOfWorkType of(TransactionMode transactionMode) {
254257
}
255258
}
256259

260+
private final Ticker ticker;
257261
private StatementExecutor.StatementTimeout statementTimeout =
258262
new StatementExecutor.StatementTimeout();
259263
private boolean closed = false;
@@ -311,6 +315,7 @@ static UnitOfWorkType of(TransactionMode transactionMode) {
311315
? StatementExecutorType.VIRTUAL_THREAD
312316
: StatementExecutorType.PLATFORM_THREAD;
313317
}
318+
this.ticker = options.getTicker();
314319
this.statementExecutor =
315320
new StatementExecutor(statementExecutorType, options.getStatementExecutionInterceptors());
316321
this.spannerPool = SpannerPool.INSTANCE;
@@ -361,6 +366,7 @@ && getDialect() == Dialect.POSTGRESQL
361366
? StatementExecutorType.VIRTUAL_THREAD
362367
: StatementExecutorType.PLATFORM_THREAD,
363368
Collections.emptyList());
369+
this.ticker = options.getTicker();
364370
this.spannerPool = Preconditions.checkNotNull(spannerPool);
365371
this.options = Preconditions.checkNotNull(options);
366372
this.spanner = spannerPool.getSpanner(options, this);
@@ -489,6 +495,7 @@ private void reset(Context context, boolean inTransaction) {
489495
this.connectionState.resetValue(READONLY, context, inTransaction);
490496
this.connectionState.resetValue(DEFAULT_ISOLATION_LEVEL, context, inTransaction);
491497
this.connectionState.resetValue(READ_LOCK_MODE, context, inTransaction);
498+
this.connectionState.resetValue(TRANSACTION_TIMEOUT, context, inTransaction);
492499
this.connectionState.resetValue(READ_ONLY_STALENESS, context, inTransaction);
493500
this.connectionState.resetValue(OPTIMIZER_VERSION, context, inTransaction);
494501
this.connectionState.resetValue(OPTIMIZER_STATISTICS_PACKAGE, context, inTransaction);
@@ -683,6 +690,26 @@ public ReadLockMode getReadLockMode() {
683690
return getConnectionPropertyValue(READ_LOCK_MODE);
684691
}
685692

693+
@Override
694+
public void setTransactionTimeout(Duration timeout) {
695+
ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG);
696+
setConnectionPropertyValue(TRANSACTION_TIMEOUT, timeout);
697+
}
698+
699+
@Override
700+
public Duration getTransactionTimeout() {
701+
ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG);
702+
return getConnectionPropertyValue(TRANSACTION_TIMEOUT);
703+
}
704+
705+
@Nullable
706+
Deadline getTransactionDeadline() {
707+
Duration timeout = getTransactionTimeout();
708+
return timeout == null
709+
? null
710+
: Deadline.after(timeout.toNanos(), TimeUnit.NANOSECONDS, this.ticker);
711+
}
712+
686713
@Override
687714
public void setAutocommitDmlMode(AutocommitDmlMode mode) {
688715
Preconditions.checkNotNull(mode);
@@ -2271,6 +2298,7 @@ UnitOfWork createNewUnitOfWork(
22712298
.setDatabaseClient(dbClient)
22722299
.setIsolationLevel(transactionIsolationLevel)
22732300
.setReadLockMode(getConnectionPropertyValue(READ_LOCK_MODE))
2301+
.setDeadline(getTransactionDeadline())
22742302
.setDelayTransactionStartUntilFirstWrite(
22752303
getConnectionPropertyValue(DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE))
22762304
.setKeepTransactionAlive(getConnectionPropertyValue(KEEP_TRANSACTION_ALIVE))

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@
8282
import com.google.common.base.Strings;
8383
import com.google.common.base.Suppliers;
8484
import com.google.common.collect.ImmutableMap;
85+
import io.grpc.Deadline;
86+
import io.grpc.Deadline.Ticker;
8587
import io.opentelemetry.api.OpenTelemetry;
8688
import java.io.IOException;
8789
import java.net.URL;
@@ -388,6 +390,7 @@ public static class Builder {
388390
Collections.emptyList();
389391
private SpannerOptionsConfigurator configurator;
390392
private OpenTelemetry openTelemetry;
393+
private Ticker ticker = Deadline.getSystemTicker();
391394

392395
private Builder() {}
393396

@@ -559,6 +562,12 @@ Builder setCredentials(Credentials credentials) {
559562
return this;
560563
}
561564

565+
@VisibleForTesting
566+
Builder setTicker(Ticker ticker) {
567+
this.ticker = Preconditions.checkNotNull(ticker);
568+
return this;
569+
}
570+
562571
/**
563572
* Sets the executor type to use for connections. See {@link StatementExecutorType} for more
564573
* information on what the different options mean.
@@ -613,6 +622,7 @@ public static Builder newBuilder() {
613622
private final OpenTelemetry openTelemetry;
614623
private final List<StatementExecutionInterceptor> statementExecutionInterceptors;
615624
private final SpannerOptionsConfigurator configurator;
625+
private final Ticker ticker;
616626

617627
private ConnectionOptions(Builder builder) {
618628
Matcher matcher;
@@ -641,6 +651,7 @@ private ConnectionOptions(Builder builder) {
641651
this.statementExecutionInterceptors =
642652
Collections.unmodifiableList(builder.statementExecutionInterceptors);
643653
this.configurator = builder.configurator;
654+
this.ticker = builder.ticker;
644655

645656
// Create the initial connection state from the parsed properties in the connection URL.
646657
this.initialConnectionState = new ConnectionState(connectionPropertyValues);
@@ -813,6 +824,10 @@ SpannerOptionsConfigurator getConfigurator() {
813824
return configurator;
814825
}
815826

827+
Ticker getTicker() {
828+
return ticker;
829+
}
830+
816831
@VisibleForTesting
817832
CredentialsService getCredentialsService() {
818833
return CredentialsService.INSTANCE;

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionProperties.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,14 @@ public class ConnectionProperties {
494494
.toArray(new ReadLockMode[0]),
495495
ReadLockModeConverter.INSTANCE,
496496
Context.USER);
497+
static final ConnectionProperty<Duration> TRANSACTION_TIMEOUT =
498+
create(
499+
"transaction_timeout",
500+
"Timeout for read/write transactions.",
501+
null,
502+
null,
503+
DurationConverter.INSTANCE,
504+
Context.USER);
497505
static final ConnectionProperty<AutocommitDmlMode> AUTOCOMMIT_DML_MODE =
498506
create(
499507
"autocommit_dml_mode",

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionStatementExecutor.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ interface ConnectionStatementExecutor {
6060

6161
StatementResult statementShowStatementTimeout();
6262

63+
StatementResult statementSetTransactionTimeout(Duration duration);
64+
65+
StatementResult statementShowTransactionTimeout();
66+
6367
StatementResult statementShowReadTimestamp();
6468

6569
StatementResult statementShowCommitTimestamp();

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionStatementExecutorImpl.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_STATEMENT_TIMEOUT;
5454
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_TRANSACTION_MODE;
5555
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_TRANSACTION_TAG;
56+
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_TRANSACTION_TIMEOUT;
5657
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_AUTOCOMMIT;
5758
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_AUTOCOMMIT_DML_MODE;
5859
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_AUTO_BATCH_DML;
@@ -85,6 +86,7 @@
8586
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_STATEMENT_TIMEOUT;
8687
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_TRANSACTION_ISOLATION_LEVEL;
8788
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_TRANSACTION_TAG;
89+
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_TRANSACTION_TIMEOUT;
8890
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.START_BATCH_DDL;
8991
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.START_BATCH_DML;
9092
import static com.google.cloud.spanner.connection.StatementResultImpl.noResult;
@@ -248,6 +250,24 @@ public StatementResult statementShowStatementTimeout() {
248250
SHOW_STATEMENT_TIMEOUT);
249251
}
250252

253+
@Override
254+
public StatementResult statementSetTransactionTimeout(Duration duration) {
255+
if (duration == null || duration.isZero()) {
256+
getConnection().setTransactionTimeout(null);
257+
} else {
258+
getConnection().setTransactionTimeout(duration);
259+
}
260+
return noResult(SET_TRANSACTION_TIMEOUT);
261+
}
262+
263+
@Override
264+
public StatementResult statementShowTransactionTimeout() {
265+
return resultSet(
266+
String.format("%sTRANSACTION_TIMEOUT", getNamespace(connection.getDialect())),
267+
String.valueOf(getConnection().getTransactionTimeout()),
268+
SHOW_TRANSACTION_TIMEOUT);
269+
}
270+
251271
@Override
252272
public StatementResult statementShowReadTimestamp() {
253273
return resultSet(

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReadWriteTransaction.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
import com.google.spanner.v1.SpannerGrpc;
6363
import com.google.spanner.v1.TransactionOptions.IsolationLevel;
6464
import com.google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode;
65+
import io.grpc.Deadline;
6566
import io.opentelemetry.api.common.AttributeKey;
6667
import io.opentelemetry.context.Scope;
6768
import java.time.Duration;
@@ -81,6 +82,7 @@
8182
import java.util.logging.Level;
8283
import java.util.logging.Logger;
8384
import javax.annotation.Nonnull;
85+
import javax.annotation.Nullable;
8486

8587
/**
8688
* Transaction that is used when a {@link Connection} is normal read/write mode (i.e. not autocommit
@@ -157,6 +159,7 @@ class ReadWriteTransaction extends AbstractMultiUseTransaction {
157159
private final SavepointSupport savepointSupport;
158160
@Nonnull private final IsolationLevel isolationLevel;
159161
private final ReadLockMode readLockMode;
162+
private final Deadline deadline;
160163
private int transactionRetryAttempts;
161164
private int successfulRetries;
162165
private volatile ApiFuture<TransactionContext> txContextFuture;
@@ -210,6 +213,7 @@ static class Builder extends AbstractMultiUseTransaction.Builder<Builder, ReadWr
210213
private SavepointSupport savepointSupport;
211214
private IsolationLevel isolationLevel;
212215
private ReadLockMode readLockMode = ReadLockMode.READ_LOCK_MODE_UNSPECIFIED;
216+
private Deadline deadline;
213217

214218
private Builder() {}
215219

@@ -269,6 +273,11 @@ Builder setReadLockMode(ReadLockMode readLockMode) {
269273
return this;
270274
}
271275

276+
Builder setDeadline(Deadline deadline) {
277+
this.deadline = deadline;
278+
return this;
279+
}
280+
272281
@Override
273282
ReadWriteTransaction build() {
274283
Preconditions.checkState(dbClient != null, "No DatabaseClient client specified");
@@ -314,6 +323,7 @@ private ReadWriteTransaction(Builder builder) {
314323
this.savepointSupport = builder.savepointSupport;
315324
this.isolationLevel = Preconditions.checkNotNull(builder.isolationLevel);
316325
this.readLockMode = Preconditions.checkNotNull(builder.readLockMode);
326+
this.deadline = builder.deadline;
317327
this.transactionOptions = extractOptions(builder);
318328
}
319329

@@ -1307,6 +1317,12 @@ String getUnitOfWorkName() {
13071317
return "read/write transaction";
13081318
}
13091319

1320+
@Nullable
1321+
@Override
1322+
Deadline getTransactionDeadline() {
1323+
return this.deadline;
1324+
}
1325+
13101326
static class ReadWriteSavepoint extends Savepoint {
13111327
private final int statementPosition;
13121328
private final int mutationPosition;

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/StatementResult.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ enum ClientSideStatementType {
5858
SET_AUTOCOMMIT_DML_MODE,
5959
SHOW_STATEMENT_TIMEOUT,
6060
SET_STATEMENT_TIMEOUT,
61+
SHOW_TRANSACTION_TIMEOUT,
62+
SET_TRANSACTION_TIMEOUT,
6163
SHOW_READ_TIMESTAMP,
6264
SHOW_COMMIT_TIMESTAMP,
6365
SHOW_COMMIT_RESPONSE,

0 commit comments

Comments
 (0)