Skip to content

Commit e62f5ab

Browse files
shobhitsgolavloite
andauthored
feat: support isolation level REPEATABLE_READ for R/W transactions (#3670)
* feat(spanner): Support REPEATABLE_READ for RW transaction * feat: addressed review comments * feat: addressed review comments and added missing javadoc * Update google-cloud-spanner/src/main/java/com/google/cloud/spanner/Options.java Co-authored-by: Knut Olav Løite <[email protected]> * Update google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java Co-authored-by: Knut Olav Løite <[email protected]> * feat: code reformat and minor test cases fix due to renaming of Options.isolationLevel * feat: Options method renamed in javadoc * code formatting fixed --------- Co-authored-by: Knut Olav Løite <[email protected]>
1 parent bd4b1f5 commit e62f5ab

19 files changed

+834
-428
lines changed

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -641,8 +641,8 @@ private ResultSet executeQueryInternal(
641641
* <li>Specific {@link QueryOptions} passed in for this query.
642642
* <li>Any value specified in a valid environment variable when the {@link SpannerOptions}
643643
* instance was created.
644-
* <li>The default {@link SpannerOptions#getDefaultQueryOptions()} specified for the database
645-
* where the query is executed.
644+
* <li>The default {@link SpannerOptions#getDefaultQueryOptions(DatabaseId)} ()} specified for
645+
* the database where the query is executed.
646646
* </ol>
647647
*/
648648
@VisibleForTesting

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

+5
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import com.google.cloud.spanner.Options.TransactionOption;
2323
import com.google.cloud.spanner.Options.UpdateOption;
2424
import com.google.spanner.v1.BatchWriteResponse;
25+
import com.google.spanner.v1.TransactionOptions.IsolationLevel;
2526

2627
/**
2728
* Interface for all the APIs that are used to read/write data into a Cloud Spanner database. An
@@ -414,6 +415,7 @@ ServerStream<BatchWriteResponse> batchWriteAtLeastOnce(
414415
* applied to any other requests on the transaction.
415416
* <li>{@link Options#commitStats()}: Request that the server includes commit statistics in the
416417
* {@link CommitResponse}.
418+
* <li>{@link Options#isolationLevel(IsolationLevel)}: The isolation level for the transaction
417419
* </ul>
418420
*/
419421
TransactionRunner readWriteTransaction(TransactionOption... options);
@@ -454,6 +456,7 @@ ServerStream<BatchWriteResponse> batchWriteAtLeastOnce(
454456
* applied to any other requests on the transaction.
455457
* <li>{@link Options#commitStats()}: Request that the server includes commit statistics in the
456458
* {@link CommitResponse}.
459+
* <li>{@link Options#isolationLevel(IsolationLevel)}: The isolation level for the transaction
457460
* </ul>
458461
*/
459462
TransactionManager transactionManager(TransactionOption... options);
@@ -494,6 +497,7 @@ ServerStream<BatchWriteResponse> batchWriteAtLeastOnce(
494497
* applied to any other requests on the transaction.
495498
* <li>{@link Options#commitStats()}: Request that the server includes commit statistics in the
496499
* {@link CommitResponse}.
500+
* <li>{@link Options#isolationLevel(IsolationLevel)}: The isolation level for the transaction
497501
* </ul>
498502
*/
499503
AsyncRunner runAsync(TransactionOption... options);
@@ -548,6 +552,7 @@ ServerStream<BatchWriteResponse> batchWriteAtLeastOnce(
548552
* applied to any other requests on the transaction.
549553
* <li>{@link Options#commitStats()}: Request that the server includes commit statistics in the
550554
* {@link CommitResponse}.
555+
* <li>{@link Options#isolationLevel(IsolationLevel)}: The isolation level for the transaction
551556
* </ul>
552557
*/
553558
AsyncTransactionManager transactionManagerAsync(TransactionOption... options);

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

+35-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import com.google.spanner.v1.ReadRequest.LockHint;
2222
import com.google.spanner.v1.ReadRequest.OrderBy;
2323
import com.google.spanner.v1.RequestOptions.Priority;
24+
import com.google.spanner.v1.TransactionOptions.IsolationLevel;
2425
import java.io.Serializable;
2526
import java.time.Duration;
2627
import java.util.Objects;
@@ -159,6 +160,13 @@ public static TransactionOption optimisticLock() {
159160
return OPTIMISTIC_LOCK_OPTION;
160161
}
161162

163+
/**
164+
* Specifying this instructs the transaction to request {@link IsolationLevel} from the backend.
165+
*/
166+
public static TransactionOption isolationLevel(IsolationLevel isolationLevel) {
167+
return new IsolationLevelOption(isolationLevel);
168+
}
169+
162170
/**
163171
* Specifying this instructs the transaction to be excluded from being recorded in change streams
164172
* with the DDL option `allow_txn_exclusion=true`. This does not exclude the transaction from
@@ -490,6 +498,20 @@ void appendToOptions(Options options) {
490498
}
491499
}
492500

501+
/** Option to set isolation level for read/write transactions. */
502+
static final class IsolationLevelOption extends InternalOption implements TransactionOption {
503+
private final IsolationLevel isolationLevel;
504+
505+
public IsolationLevelOption(IsolationLevel isolationLevel) {
506+
this.isolationLevel = isolationLevel;
507+
}
508+
509+
@Override
510+
void appendToOptions(Options options) {
511+
options.isolationLevel = isolationLevel;
512+
}
513+
}
514+
493515
private boolean withCommitStats;
494516

495517
private Duration maxCommitDelay;
@@ -512,6 +534,7 @@ void appendToOptions(Options options) {
512534
private RpcOrderBy orderBy;
513535
private RpcLockHint lockHint;
514536
private Boolean lastStatement;
537+
private IsolationLevel isolationLevel;
515538

516539
// Construction is via factory methods below.
517540
private Options() {}
@@ -664,6 +687,10 @@ LockHint lockHint() {
664687
return lockHint == null ? null : lockHint.proto;
665688
}
666689

690+
IsolationLevel isolationLevel() {
691+
return isolationLevel;
692+
}
693+
667694
@Override
668695
public String toString() {
669696
StringBuilder b = new StringBuilder();
@@ -726,6 +753,9 @@ public String toString() {
726753
if (lockHint != null) {
727754
b.append("lockHint: ").append(lockHint).append(' ');
728755
}
756+
if (isolationLevel != null) {
757+
b.append("isolationLevel: ").append(isolationLevel).append(' ');
758+
}
729759
return b.toString();
730760
}
731761

@@ -767,7 +797,8 @@ public boolean equals(Object o) {
767797
&& Objects.equals(directedReadOptions(), that.directedReadOptions())
768798
&& Objects.equals(orderBy(), that.orderBy())
769799
&& Objects.equals(isLastStatement(), that.isLastStatement())
770-
&& Objects.equals(lockHint(), that.lockHint());
800+
&& Objects.equals(lockHint(), that.lockHint())
801+
&& Objects.equals(isolationLevel(), that.isolationLevel());
771802
}
772803

773804
@Override
@@ -833,6 +864,9 @@ public int hashCode() {
833864
if (lockHint != null) {
834865
result = 31 * result + lockHint.hashCode();
835866
}
867+
if (isolationLevel != null) {
868+
result = 31 * result + isolationLevel.hashCode();
869+
}
836870
return result;
837871
}
838872

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

+17-3
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ static TransactionOptions createReadWriteTransactionOptions(
8383
&& previousTransactionId != com.google.protobuf.ByteString.EMPTY) {
8484
readWrite.setMultiplexedSessionPreviousTransactionId(previousTransactionId);
8585
}
86+
if (options.isolationLevel() != null) {
87+
transactionOptions.setIsolationLevel(options.isolationLevel());
88+
}
8689
transactionOptions.setReadWrite(readWrite);
8790
return transactionOptions.build();
8891
}
@@ -193,6 +196,10 @@ void markUsed(Instant instant) {
193196
sessionReference.markUsed(instant);
194197
}
195198

199+
TransactionOptions defaultTransactionOptions() {
200+
return this.spanner.getOptions().getDefaultTransactionOptions();
201+
}
202+
196203
public DatabaseId getDatabaseId() {
197204
return sessionReference.getDatabaseId();
198205
}
@@ -252,7 +259,11 @@ public CommitResponse writeAtLeastOnceWithOptions(
252259
if (options.withExcludeTxnFromChangeStreams() == Boolean.TRUE) {
253260
transactionOptionsBuilder.setExcludeTxnFromChangeStreams(true);
254261
}
255-
requestBuilder.setSingleUseTransaction(transactionOptionsBuilder);
262+
if (options.isolationLevel() != null) {
263+
transactionOptionsBuilder.setIsolationLevel(options.isolationLevel());
264+
}
265+
requestBuilder.setSingleUseTransaction(
266+
defaultTransactionOptions().toBuilder().mergeFrom(transactionOptionsBuilder.build()));
256267

257268
if (options.hasMaxCommitDelay()) {
258269
requestBuilder.setMaxCommitDelay(
@@ -444,7 +455,11 @@ ApiFuture<Transaction> beginTransactionAsync(
444455
BeginTransactionRequest.newBuilder()
445456
.setSession(getName())
446457
.setOptions(
447-
createReadWriteTransactionOptions(transactionOptions, previousTransactionId));
458+
defaultTransactionOptions()
459+
.toBuilder()
460+
.mergeFrom(
461+
createReadWriteTransactionOptions(
462+
transactionOptions, previousTransactionId)));
448463
if (sessionReference.getIsMultiplexed() && mutation != null) {
449464
requestBuilder.setMutationKey(mutation);
450465
}
@@ -489,7 +504,6 @@ TransactionContextImpl newTransaction(Options options, ByteString previousTransa
489504
.setOptions(options)
490505
.setTransactionId(null)
491506
.setPreviousTransactionId(previousTransactionId)
492-
.setOptions(options)
493507
.setTrackTransactionStarter(spanner.getOptions().isTrackTransactionStarter())
494508
.setRpc(spanner.getRpc())
495509
.setDefaultQueryOptions(spanner.getDefaultQueryOptions(getDatabaseId()))

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

+59
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@
6666
import com.google.spanner.v1.ExecuteSqlRequest;
6767
import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions;
6868
import com.google.spanner.v1.SpannerGrpc;
69+
import com.google.spanner.v1.TransactionOptions;
70+
import com.google.spanner.v1.TransactionOptions.IsolationLevel;
6971
import io.grpc.CallCredentials;
7072
import io.grpc.CompressorRegistry;
7173
import io.grpc.Context;
@@ -178,6 +180,7 @@ public class SpannerOptions extends ServiceOptions<Spanner, SpannerOptions> {
178180
private final boolean enableExtendedTracing;
179181
private final boolean enableEndToEndTracing;
180182
private final String monitoringHost;
183+
private final TransactionOptions defaultTransactionOptions;
181184

182185
enum TracingFramework {
183186
OPEN_CENSUS,
@@ -807,6 +810,7 @@ protected SpannerOptions(Builder builder) {
807810
enableBuiltInMetrics = builder.enableBuiltInMetrics;
808811
enableEndToEndTracing = builder.enableEndToEndTracing;
809812
monitoringHost = builder.monitoringHost;
813+
defaultTransactionOptions = builder.defaultTransactionOptions;
810814
}
811815

812816
/**
@@ -988,6 +992,7 @@ public static class Builder
988992
private String monitoringHost = SpannerOptions.environment.getMonitoringHost();
989993
private SslContext mTLSContext = null;
990994
private boolean isExperimentalHost = false;
995+
private TransactionOptions defaultTransactionOptions = TransactionOptions.getDefaultInstance();
991996

992997
private static String createCustomClientLibToken(String token) {
993998
return token + " " + ServiceOptions.getGoogApiClientLibName();
@@ -1056,6 +1061,7 @@ protected Builder() {
10561061
this.enableBuiltInMetrics = options.enableBuiltInMetrics;
10571062
this.enableEndToEndTracing = options.enableEndToEndTracing;
10581063
this.monitoringHost = options.monitoringHost;
1064+
this.defaultTransactionOptions = options.defaultTransactionOptions;
10591065
}
10601066

10611067
@Override
@@ -1645,6 +1651,55 @@ public Builder setEnableEndToEndTracing(boolean enableEndToEndTracing) {
16451651
return this;
16461652
}
16471653

1654+
/**
1655+
* Provides the default read-write transaction options for all databases. These defaults are
1656+
* overridden by any explicit {@link com.google.cloud.spanner.Options.TransactionOption}
1657+
* provided through {@link DatabaseClient}.
1658+
*
1659+
* <p>Example Usage:
1660+
*
1661+
* <pre>{@code
1662+
* DefaultReadWriteTransactionOptions options = DefaultReadWriteTransactionOptions.newBuilder()
1663+
* .setIsolationLevel(IsolationLevel.SERIALIZABLE)
1664+
* .build();
1665+
* }</pre>
1666+
*/
1667+
public static class DefaultReadWriteTransactionOptions {
1668+
private final TransactionOptions defaultTransactionOptions;
1669+
1670+
private DefaultReadWriteTransactionOptions(TransactionOptions defaultTransactionOptions) {
1671+
this.defaultTransactionOptions = defaultTransactionOptions;
1672+
}
1673+
1674+
public static DefaultReadWriteTransactionOptionsBuilder newBuilder() {
1675+
return new DefaultReadWriteTransactionOptionsBuilder();
1676+
}
1677+
1678+
public static class DefaultReadWriteTransactionOptionsBuilder {
1679+
private final TransactionOptions.Builder transactionOptionsBuilder =
1680+
TransactionOptions.newBuilder();
1681+
1682+
public DefaultReadWriteTransactionOptionsBuilder setIsolationLevel(
1683+
IsolationLevel isolationLevel) {
1684+
transactionOptionsBuilder.setIsolationLevel(isolationLevel);
1685+
return this;
1686+
}
1687+
1688+
public DefaultReadWriteTransactionOptions build() {
1689+
return new DefaultReadWriteTransactionOptions(transactionOptionsBuilder.build());
1690+
}
1691+
}
1692+
}
1693+
1694+
/** Sets the {@link DefaultReadWriteTransactionOptions} for read-write transactions. */
1695+
public Builder setDefaultTransactionOptions(
1696+
DefaultReadWriteTransactionOptions defaultReadWriteTransactionOptions) {
1697+
Preconditions.checkNotNull(
1698+
defaultReadWriteTransactionOptions, "DefaultReadWriteTransactionOptions cannot be null");
1699+
this.defaultTransactionOptions = defaultReadWriteTransactionOptions.defaultTransactionOptions;
1700+
return this;
1701+
}
1702+
16481703
@SuppressWarnings("rawtypes")
16491704
@Override
16501705
public SpannerOptions build() {
@@ -1990,6 +2045,10 @@ String getMonitoringHost() {
19902045
return monitoringHost;
19912046
}
19922047

2048+
public TransactionOptions getDefaultTransactionOptions() {
2049+
return defaultTransactionOptions;
2050+
}
2051+
19932052
@BetaApi
19942053
public boolean isUseVirtualThreads() {
19952054
return useVirtualThreads;

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

+6-2
Original file line numberDiff line numberDiff line change
@@ -643,8 +643,12 @@ TransactionSelector getTransactionSelector() {
643643
if (tx == null) {
644644
return TransactionSelector.newBuilder()
645645
.setBegin(
646-
SessionImpl.createReadWriteTransactionOptions(
647-
options, getPreviousTransactionId()))
646+
this.session
647+
.defaultTransactionOptions()
648+
.toBuilder()
649+
.mergeFrom(
650+
SessionImpl.createReadWriteTransactionOptions(
651+
options, getPreviousTransactionId())))
648652
.build();
649653
} else {
650654
// Wait for the transaction to come available. The tx.get() call will fail with an

0 commit comments

Comments
 (0)