Skip to content

Commit f1cbd16

Browse files
feat: delay transaction start option (#2462)
* feat: delay transaction start option Adds an opt-in to delay the actual start of a read/write transaction until the first write operation. This reduces lock contention and can reduce latency for read/write transactions that execute (many) read operations before any write operations, at the expense of a lower transaction isolation level. Typical workloads that benefit from this option are ORMs (e.g. Hibernate). The application must be able to handle the lower isolation level. * fix: clirr check * fix: only create tx manager if needed * test: add integration tests * test: skip concurrency tests on emulator * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com>
1 parent ee6548c commit f1cbd16

23 files changed

+36044
-31572
lines changed

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

+12
Original file line numberDiff line numberDiff line change
@@ -347,4 +347,16 @@
347347
<className>com/google/cloud/spanner/connection/Connection</className>
348348
<method>void rollbackToSavepoint(java.lang.String)</method>
349349
</difference>
350+
351+
<!-- Delay start transaction -->
352+
<difference>
353+
<differenceType>7012</differenceType>
354+
<className>com/google/cloud/spanner/connection/Connection</className>
355+
<method>void setDelayTransactionStartUntilFirstWrite(boolean)</method>
356+
</difference>
357+
<difference>
358+
<differenceType>7012</differenceType>
359+
<className>com/google/cloud/spanner/connection/Connection</className>
360+
<method>boolean isDelayTransactionStartUntilFirstWrite()</method>
361+
</difference>
350362
</differences>

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

+6-4
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,12 @@ public boolean isActive() {
9999
abstract void checkAborted();
100100

101101
/**
102-
* Check that the current transaction actually has a valid underlying transaction. If not, the
103-
* method will throw a {@link SpannerException}.
102+
* Check that the current transaction actually has a valid underlying transaction and creates it
103+
* if necessary. If the transaction does not have a valid underlying transaction and/or is not in
104+
* a state that allows the creation of a transaction, the method will throw a {@link
105+
* SpannerException}.
104106
*/
105-
abstract void checkValidTransaction(CallType callType);
107+
abstract void checkOrCreateValidTransaction(ParsedStatement statement, CallType callType);
106108

107109
/** Returns the {@link ReadContext} that can be used for queries on this transaction. */
108110
abstract ReadContext getReadContext();
@@ -114,7 +116,7 @@ public ApiFuture<ResultSet> executeQueryAsync(
114116
final AnalyzeMode analyzeMode,
115117
final QueryOption... options) {
116118
Preconditions.checkArgument(statement.isQuery(), "Statement is not a query");
117-
checkValidTransaction(callType);
119+
checkOrCreateValidTransaction(statement, callType);
118120
return executeStatementAsync(
119121
callType,
120122
statement,

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

+23
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,29 @@ default RpcPriority getRPCPriority() {
563563
throw new UnsupportedOperationException("Unimplemented");
564564
}
565565

566+
/**
567+
* Sets whether this connection should delay the actual start of a read/write transaction until
568+
* the first write operation is observed on that transaction. All read operations that are
569+
* executed before the first write operation in the transaction will be executed as if the
570+
* connection was in auto-commit mode. This can reduce locking, especially for transactions that
571+
* execute a large number of reads before any writes, at the expense of a lower transaction
572+
* isolation.
573+
*
574+
* <p>NOTE: This will make read/write transactions non-serializable.
575+
*/
576+
default void setDelayTransactionStartUntilFirstWrite(
577+
boolean delayTransactionStartUntilFirstWrite) {
578+
throw new UnsupportedOperationException("Unimplemented");
579+
}
580+
581+
/**
582+
* @return true if this connection delays the actual start of a read/write transaction until the
583+
* first write operation on that transaction.
584+
*/
585+
default boolean isDelayTransactionStartUntilFirstWrite() {
586+
throw new UnsupportedOperationException("Unimplemented");
587+
}
588+
566589
/**
567590
* Commits the current transaction of this connection. All mutations that have been buffered
568591
* during the current transaction will be written to the database.

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

+19
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ static UnitOfWorkType of(TransactionMode transactionMode) {
193193
private boolean autocommit;
194194
private boolean readOnly;
195195
private boolean returnCommitStats;
196+
private boolean delayTransactionStartUntilFirstWrite;
196197

197198
private UnitOfWork currentUnitOfWork = null;
198199
/**
@@ -239,6 +240,7 @@ static UnitOfWorkType of(TransactionMode transactionMode) {
239240
this.queryOptions = this.queryOptions.toBuilder().mergeFrom(options.getQueryOptions()).build();
240241
this.rpcPriority = options.getRPCPriority();
241242
this.returnCommitStats = options.isReturnCommitStats();
243+
this.delayTransactionStartUntilFirstWrite = options.isDelayTransactionStartUntilFirstWrite();
242244
this.ddlClient = createDdlClient();
243245
setDefaultTransactionOptions();
244246
}
@@ -744,6 +746,22 @@ public boolean isReturnCommitStats() {
744746
return this.returnCommitStats;
745747
}
746748

749+
@Override
750+
public void setDelayTransactionStartUntilFirstWrite(
751+
boolean delayTransactionStartUntilFirstWrite) {
752+
ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG);
753+
ConnectionPreconditions.checkState(
754+
!isTransactionStarted(),
755+
"Cannot set DelayTransactionStartUntilFirstWrite while a transaction is active");
756+
this.delayTransactionStartUntilFirstWrite = delayTransactionStartUntilFirstWrite;
757+
}
758+
759+
@Override
760+
public boolean isDelayTransactionStartUntilFirstWrite() {
761+
ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG);
762+
return this.delayTransactionStartUntilFirstWrite;
763+
}
764+
747765
/** Resets this connection to its default transaction options. */
748766
private void setDefaultTransactionOptions() {
749767
if (transactionStack.isEmpty()) {
@@ -1376,6 +1394,7 @@ UnitOfWork createNewUnitOfWork() {
13761394
case READ_WRITE_TRANSACTION:
13771395
return ReadWriteTransaction.newBuilder()
13781396
.setDatabaseClient(dbClient)
1397+
.setDelayTransactionStartUntilFirstWrite(delayTransactionStartUntilFirstWrite)
13791398
.setRetryAbortsInternally(retryAbortsInternally)
13801399
.setSavepointSupport(savepointSupport)
13811400
.setReturnCommitStats(returnCommitStats)

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

+30
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ public String[] getValidValues() {
172172
private static final RpcPriority DEFAULT_RPC_PRIORITY = null;
173173
private static final boolean DEFAULT_RETURN_COMMIT_STATS = false;
174174
private static final boolean DEFAULT_LENIENT = false;
175+
private static final boolean DEFAULT_DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE = false;
175176
private static final boolean DEFAULT_TRACK_SESSION_LEAKS = true;
176177
private static final boolean DEFAULT_TRACK_CONNECTION_LEAKS = true;
177178

@@ -220,6 +221,9 @@ public String[] getValidValues() {
220221
private static final String DIALECT_PROPERTY_NAME = "dialect";
221222
/** Name of the 'databaseRole' connection property. */
222223
public static final String DATABASE_ROLE_PROPERTY_NAME = "databaseRole";
224+
/** Name of the 'delay transaction start until first write' property. */
225+
public static final String DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE_NAME =
226+
"delayTransactionStartUntilFirstWrite";
223227
/** Name of the 'trackStackTraceOfSessionCheckout' connection property. */
224228
public static final String TRACK_SESSION_LEAKS_PROPERTY_NAME = "trackSessionLeaks";
225229
/** Name of the 'trackStackTraceOfConnectionCreation' connection property. */
@@ -294,6 +298,14 @@ public String[] getValidValues() {
294298
ConnectionProperty.createStringProperty(
295299
DATABASE_ROLE_PROPERTY_NAME,
296300
"Sets the database role to use for this connection. The default is privileges assigned to IAM role"),
301+
ConnectionProperty.createBooleanProperty(
302+
DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE_NAME,
303+
"Enabling this option will delay the actual start of a read/write transaction until the first write operation is seen in that transaction. "
304+
+ "All reads that happen before the first write in a transaction will instead be executed as if the connection was in auto-commit mode. "
305+
+ "Enabling this option will make read/write transactions lose their SERIALIZABLE isolation level. Read operations that are executed after "
306+
+ "the first write operation in a read/write transaction will be executed using the read/write transaction. Enabling this mode can reduce locking "
307+
+ "and improve performance for applications that can handle the lower transaction isolation semantics.",
308+
DEFAULT_DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE),
297309
ConnectionProperty.createBooleanProperty(
298310
TRACK_SESSION_LEAKS_PROPERTY_NAME,
299311
"Capture the call stack of the thread that checked out a session of the session pool. This will "
@@ -568,6 +580,7 @@ public static Builder newBuilder() {
568580
private final boolean returnCommitStats;
569581
private final boolean autoConfigEmulator;
570582
private final RpcPriority rpcPriority;
583+
private final boolean delayTransactionStartUntilFirstWrite;
571584
private final boolean trackSessionLeaks;
572585
private final boolean trackConnectionLeaks;
573586

@@ -614,6 +627,7 @@ private ConnectionOptions(Builder builder) {
614627
this.usePlainText = this.autoConfigEmulator || parseUsePlainText(this.uri);
615628
this.host = determineHost(matcher, autoConfigEmulator, usePlainText);
616629
this.rpcPriority = parseRPCPriority(this.uri);
630+
this.delayTransactionStartUntilFirstWrite = parseDelayTransactionStartUntilFirstWrite(this.uri);
617631
this.trackSessionLeaks = parseTrackSessionLeaks(this.uri);
618632
this.trackConnectionLeaks = parseTrackConnectionLeaks(this.uri);
619633

@@ -867,6 +881,14 @@ static boolean parseLenient(String uri) {
867881
return value != null ? Boolean.parseBoolean(value) : DEFAULT_LENIENT;
868882
}
869883

884+
@VisibleForTesting
885+
static boolean parseDelayTransactionStartUntilFirstWrite(String uri) {
886+
String value = parseUriProperty(uri, DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE_NAME);
887+
return value != null
888+
? Boolean.parseBoolean(value)
889+
: DEFAULT_DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE;
890+
}
891+
870892
@VisibleForTesting
871893
static boolean parseTrackSessionLeaks(String uri) {
872894
String value = parseUriProperty(uri, TRACK_SESSION_LEAKS_PROPERTY_NAME);
@@ -1119,6 +1141,14 @@ RpcPriority getRPCPriority() {
11191141
return rpcPriority;
11201142
}
11211143

1144+
/**
1145+
* Whether connections created by this {@link ConnectionOptions} should delay the actual start of
1146+
* a read/write transaction until the first write operation.
1147+
*/
1148+
boolean isDelayTransactionStartUntilFirstWrite() {
1149+
return delayTransactionStartUntilFirstWrite;
1150+
}
1151+
11221152
boolean isTrackConnectionLeaks() {
11231153
return this.trackConnectionLeaks;
11241154
}

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

+5
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,11 @@ interface ConnectionStatementExecutor {
7676

7777
StatementResult statementShowReturnCommitStats();
7878

79+
StatementResult statementSetDelayTransactionStartUntilFirstWrite(
80+
Boolean delayTransactionStartUntilFirstWrite);
81+
82+
StatementResult statementShowDelayTransactionStartUntilFirstWrite();
83+
7984
StatementResult statementSetStatementTag(String tag);
8085

8186
StatementResult statementShowStatementTag();

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

+18
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_AUTOCOMMIT;
2626
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_AUTOCOMMIT_DML_MODE;
2727
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_DEFAULT_TRANSACTION_ISOLATION;
28+
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE;
2829
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_OPTIMIZER_STATISTICS_PACKAGE;
2930
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_OPTIMIZER_VERSION;
3031
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_READONLY;
@@ -41,6 +42,7 @@
4142
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_AUTOCOMMIT_DML_MODE;
4243
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_COMMIT_RESPONSE;
4344
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_COMMIT_TIMESTAMP;
45+
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE;
4446
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_OPTIMIZER_STATISTICS_PACKAGE;
4547
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_OPTIMIZER_VERSION;
4648
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_READONLY;
@@ -313,6 +315,22 @@ public StatementResult statementShowReturnCommitStats() {
313315
SHOW_RETURN_COMMIT_STATS);
314316
}
315317

318+
@Override
319+
public StatementResult statementSetDelayTransactionStartUntilFirstWrite(
320+
Boolean delayTransactionStartUntilFirstWrite) {
321+
getConnection().setDelayTransactionStartUntilFirstWrite(delayTransactionStartUntilFirstWrite);
322+
return noResult(SET_DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE);
323+
}
324+
325+
@Override
326+
public StatementResult statementShowDelayTransactionStartUntilFirstWrite() {
327+
return resultSet(
328+
String.format(
329+
"%sDELAY_TRANSACTION_START_UNTIL_FIRST_WRITE", getNamespace(connection.getDialect())),
330+
getConnection().isDelayTransactionStartUntilFirstWrite(),
331+
SHOW_DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE);
332+
}
333+
316334
@Override
317335
public StatementResult statementSetStatementTag(String tag) {
318336
getConnection().setStatementTag("".equals(tag) ? null : tag);

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public void retry(AbortedException aborted) throws AbortedException {
5757
.invokeInterceptors(
5858
RUN_BATCH_STATEMENT, StatementExecutionStep.RETRY_STATEMENT, transaction);
5959
try {
60-
transaction.getReadContext().batchUpdate(statements);
60+
transaction.getTransactionContext().batchUpdate(statements);
6161
} catch (AbortedException e) {
6262
// Propagate abort to force a new retry.
6363
throw e;

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public void retry(AbortedException aborted) throws AbortedException {
5353
transaction
5454
.getStatementExecutor()
5555
.invokeInterceptors(statement, StatementExecutionStep.RETRY_STATEMENT, transaction);
56-
transaction.getReadContext().executeUpdate(statement.getStatement());
56+
transaction.getTransactionContext().executeUpdate(statement.getStatement());
5757
} catch (AbortedException e) {
5858
// Propagate abort to force a new retry.
5959
throw e;

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ void checkAborted() {
9696
}
9797

9898
@Override
99-
void checkValidTransaction(CallType callType) {
99+
void checkOrCreateValidTransaction(ParsedStatement statement, CallType callType) {
100100
if (transaction == null) {
101101
transaction = dbClient.readOnlyTransaction(readOnlyStaleness);
102102
}

0 commit comments

Comments
 (0)