Skip to content

Commit 9fd5cb1

Browse files
committed
feat: support read lock mode for R/W transactions
1 parent bf69b1a commit 9fd5cb1

File tree

6 files changed

+406
-57
lines changed

6 files changed

+406
-57
lines changed

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import com.google.cloud.spanner.Statement.StatementFactory;
2525
import com.google.spanner.v1.BatchWriteResponse;
2626
import com.google.spanner.v1.TransactionOptions.IsolationLevel;
27+
import com.google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode;
2728

2829
/**
2930
* Interface for all the APIs that are used to read/write data into a Cloud Spanner database. An
@@ -417,6 +418,7 @@ ServerStream<BatchWriteResponse> batchWriteAtLeastOnce(
417418
* <li>{@link Options#commitStats()}: Request that the server includes commit statistics in the
418419
* {@link CommitResponse}.
419420
* <li>{@link Options#isolationLevel(IsolationLevel)}: The isolation level for the transaction
421+
* <li>{@link Options#readLockMode(ReadLockMode)}: The read lock mode for the transaction
420422
* </ul>
421423
*/
422424
TransactionRunner readWriteTransaction(TransactionOption... options);
@@ -458,6 +460,7 @@ ServerStream<BatchWriteResponse> batchWriteAtLeastOnce(
458460
* <li>{@link Options#commitStats()}: Request that the server includes commit statistics in the
459461
* {@link CommitResponse}.
460462
* <li>{@link Options#isolationLevel(IsolationLevel)}: The isolation level for the transaction
463+
* <li>{@link Options#readLockMode(ReadLockMode)}: The read lock mode for the transaction
461464
* </ul>
462465
*/
463466
TransactionManager transactionManager(TransactionOption... options);
@@ -499,6 +502,7 @@ ServerStream<BatchWriteResponse> batchWriteAtLeastOnce(
499502
* <li>{@link Options#commitStats()}: Request that the server includes commit statistics in the
500503
* {@link CommitResponse}.
501504
* <li>{@link Options#isolationLevel(IsolationLevel)}: The isolation level for the transaction
505+
* <li>{@link Options#readLockMode(ReadLockMode)}: The read lock mode for the transaction
502506
* </ul>
503507
*/
504508
AsyncRunner runAsync(TransactionOption... options);
@@ -554,6 +558,7 @@ ServerStream<BatchWriteResponse> batchWriteAtLeastOnce(
554558
* <li>{@link Options#commitStats()}: Request that the server includes commit statistics in the
555559
* {@link CommitResponse}.
556560
* <li>{@link Options#isolationLevel(IsolationLevel)}: The isolation level for the transaction
561+
* <li>{@link Options#readLockMode(ReadLockMode)}: The read lock mode for the transaction
557562
* </ul>
558563
*/
559564
AsyncTransactionManager transactionManagerAsync(TransactionOption... options);

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

Lines changed: 64 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import com.google.spanner.v1.ReadRequest.OrderBy;
2323
import com.google.spanner.v1.RequestOptions.Priority;
2424
import com.google.spanner.v1.TransactionOptions.IsolationLevel;
25+
import com.google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode;
2526
import java.io.Serializable;
2627
import java.time.Duration;
2728
import java.util.Objects;
@@ -155,9 +156,44 @@ public static TransactionOption commitStats() {
155156
* process in the commit phase (when any needed locks are acquired). The validation process
156157
* succeeds only if there are no conflicting committed transactions (that committed mutations to
157158
* the read data at a commit timestamp after the read timestamp).
159+
*
160+
* @deprecated Use {@link Options#readLockMode(ReadLockMode)} instead.
158161
*/
162+
@Deprecated
159163
public static TransactionOption optimisticLock() {
160-
return OPTIMISTIC_LOCK_OPTION;
164+
return Options.readLockMode(ReadLockMode.OPTIMISTIC);
165+
}
166+
167+
/**
168+
* Returns a {@link TransactionOption} to set the desired {@link ReadLockMode} for a read-write
169+
* transaction.
170+
*
171+
* <p>This option controls the locking behavior for read operations and queries within a
172+
* read-write transaction. It works in conjunction with the transaction's {@link IsolationLevel}.
173+
*
174+
* <ul>
175+
* <li>{@link ReadLockMode#PESSIMISTIC}: Read locks are acquired immediately on read. This mode
176+
* primarily applies to transactions with {@code SERIALIZABLE} isolation.
177+
* <li>{@link ReadLockMode#OPTIMISTIC}: Locks for reads within the transaction are not acquired
178+
* on read. Instead the locks are acquired on a commit to validate that read/queried data
179+
* has not changed since the transaction started. If a conflict is detected, the transaction
180+
* will fail. This mode applies to transactions with {@code SERIALIZABLE} isolation.
181+
* <li>{@link ReadLockMode#READ_LOCK_MODE_UNSPECIFIED}: This is the default if no mode is set.
182+
* The locking behavior depends on the isolation level:
183+
* <ul>
184+
* <li>For {@code REPEATABLE_READ} isolation: Locking semantics default to {@code
185+
* OPTIMISTIC}. However, validation checks at commit are only performed for reads
186+
* within queries using {@code SELECT FOR UPDATE}, statements with {@code
187+
* LOCK_SCANNED_RANGES} hints, or DML statements. <br>
188+
* Note: It is an error to explicitly set {@code ReadLockMode} when the isolation
189+
* level is {@code REPEATABLE_READ}.
190+
* <li>For all other isolation levels: If the read lock mode is not set, it defaults to
191+
* {@code PESSIMISTIC} locking.
192+
* </ul>
193+
* </ul>
194+
*/
195+
public static TransactionOption readLockMode(ReadLockMode readLockMode) {
196+
return new ReadLockModeOption(readLockMode);
161197
}
162198

163199
/**
@@ -367,16 +403,6 @@ void appendToOptions(Options options) {
367403
}
368404
}
369405

370-
/** Option to request Optimistic Concurrency Control for read/write transactions. */
371-
static final class OptimisticLockOption extends InternalOption implements TransactionOption {
372-
@Override
373-
void appendToOptions(Options options) {
374-
options.withOptimisticLock = true;
375-
}
376-
}
377-
378-
static final OptimisticLockOption OPTIMISTIC_LOCK_OPTION = new OptimisticLockOption();
379-
380406
/** Option to request the transaction to be excluded from change streams. */
381407
static final class ExcludeTxnFromChangeStreamsOption extends InternalOption
382408
implements UpdateTransactionOption {
@@ -516,6 +542,20 @@ void appendToOptions(Options options) {
516542
}
517543
}
518544

545+
/** Option to set read lock mode for read/write transactions. */
546+
static final class ReadLockModeOption extends InternalOption implements TransactionOption {
547+
private final ReadLockMode readLockMode;
548+
549+
public ReadLockModeOption(ReadLockMode readLockMode) {
550+
this.readLockMode = readLockMode;
551+
}
552+
553+
@Override
554+
void appendToOptions(Options options) {
555+
options.readLockMode = readLockMode;
556+
}
557+
}
558+
519559
private boolean withCommitStats;
520560

521561
private Duration maxCommitDelay;
@@ -530,7 +570,6 @@ void appendToOptions(Options options) {
530570
private String tag;
531571
private String etag;
532572
private Boolean validateOnly;
533-
private Boolean withOptimisticLock;
534573
private Boolean withExcludeTxnFromChangeStreams;
535574
private Boolean dataBoostEnabled;
536575
private DirectedReadOptions directedReadOptions;
@@ -540,6 +579,7 @@ void appendToOptions(Options options) {
540579
private Boolean lastStatement;
541580
private IsolationLevel isolationLevel;
542581
private XGoogSpannerRequestId reqId;
582+
private ReadLockMode readLockMode;
543583

544584
// Construction is via factory methods below.
545585
private Options() {}
@@ -644,10 +684,6 @@ Boolean validateOnly() {
644684
return validateOnly;
645685
}
646686

647-
Boolean withOptimisticLock() {
648-
return withOptimisticLock;
649-
}
650-
651687
Boolean withExcludeTxnFromChangeStreams() {
652688
return withExcludeTxnFromChangeStreams;
653689
}
@@ -704,6 +740,10 @@ IsolationLevel isolationLevel() {
704740
return isolationLevel;
705741
}
706742

743+
ReadLockMode readLockMode() {
744+
return readLockMode;
745+
}
746+
707747
@Override
708748
public String toString() {
709749
StringBuilder b = new StringBuilder();
@@ -740,9 +780,6 @@ public String toString() {
740780
if (validateOnly != null) {
741781
b.append("validateOnly: ").append(validateOnly).append(' ');
742782
}
743-
if (withOptimisticLock != null) {
744-
b.append("withOptimisticLock: ").append(withOptimisticLock).append(' ');
745-
}
746783
if (withExcludeTxnFromChangeStreams != null) {
747784
b.append("withExcludeTxnFromChangeStreams: ")
748785
.append(withExcludeTxnFromChangeStreams)
@@ -772,6 +809,9 @@ public String toString() {
772809
if (reqId != null) {
773810
b.append("requestId: ").append(reqId.toString());
774811
}
812+
if (readLockMode != null) {
813+
b.append("readLockMode: ").append(readLockMode).append(' ');
814+
}
775815
return b.toString();
776816
}
777817

@@ -807,15 +847,15 @@ public boolean equals(Object o) {
807847
&& Objects.equals(tag(), that.tag())
808848
&& Objects.equals(etag(), that.etag())
809849
&& Objects.equals(validateOnly(), that.validateOnly())
810-
&& Objects.equals(withOptimisticLock(), that.withOptimisticLock())
811850
&& Objects.equals(withExcludeTxnFromChangeStreams(), that.withExcludeTxnFromChangeStreams())
812851
&& Objects.equals(dataBoostEnabled(), that.dataBoostEnabled())
813852
&& Objects.equals(directedReadOptions(), that.directedReadOptions())
814853
&& Objects.equals(orderBy(), that.orderBy())
815854
&& Objects.equals(isLastStatement(), that.isLastStatement())
816855
&& Objects.equals(lockHint(), that.lockHint())
817856
&& Objects.equals(isolationLevel(), that.isolationLevel())
818-
&& Objects.equals(reqId(), that.reqId());
857+
&& Objects.equals(reqId(), that.reqId())
858+
&& Objects.equals(readLockMode(), that.readLockMode());
819859
}
820860

821861
@Override
@@ -857,9 +897,6 @@ public int hashCode() {
857897
if (validateOnly != null) {
858898
result = 31 * result + validateOnly.hashCode();
859899
}
860-
if (withOptimisticLock != null) {
861-
result = 31 * result + withOptimisticLock.hashCode();
862-
}
863900
if (withExcludeTxnFromChangeStreams != null) {
864901
result = 31 * result + withExcludeTxnFromChangeStreams.hashCode();
865902
}
@@ -887,6 +924,9 @@ public int hashCode() {
887924
if (reqId != null) {
888925
result = 31 * result + reqId.hashCode();
889926
}
927+
if (readLockMode != null) {
928+
result = 31 * result + readLockMode.hashCode();
929+
}
890930
return result;
891931
}
892932

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
import com.google.spanner.v1.RequestOptions;
4646
import com.google.spanner.v1.Transaction;
4747
import com.google.spanner.v1.TransactionOptions;
48+
import com.google.spanner.v1.TransactionOptions.ReadWrite;
4849
import java.time.Instant;
4950
import java.util.ArrayList;
5051
import java.util.Collection;
@@ -76,16 +77,16 @@ static TransactionOptions createReadWriteTransactionOptions(
7677
transactionOptions.setExcludeTxnFromChangeStreams(true);
7778
}
7879
TransactionOptions.ReadWrite.Builder readWrite = TransactionOptions.ReadWrite.newBuilder();
79-
if (options.withOptimisticLock() == Boolean.TRUE) {
80-
readWrite.setReadLockMode(TransactionOptions.ReadWrite.ReadLockMode.OPTIMISTIC);
81-
}
8280
if (previousTransactionId != null
8381
&& previousTransactionId != com.google.protobuf.ByteString.EMPTY) {
8482
readWrite.setMultiplexedSessionPreviousTransactionId(previousTransactionId);
8583
}
8684
if (options.isolationLevel() != null) {
8785
transactionOptions.setIsolationLevel(options.isolationLevel());
8886
}
87+
if (options.readLockMode() != null) {
88+
readWrite.setReadLockMode(options.readLockMode());
89+
}
8990
transactionOptions.setReadWrite(readWrite);
9091
return transactionOptions.build();
9192
}
@@ -283,6 +284,10 @@ public CommitResponse writeAtLeastOnceWithOptions(
283284
if (options.isolationLevel() != null) {
284285
transactionOptionsBuilder.setIsolationLevel(options.isolationLevel());
285286
}
287+
if (options.readLockMode() != null) {
288+
transactionOptionsBuilder.setReadWrite(
289+
ReadWrite.newBuilder().setReadLockMode(options.readLockMode()));
290+
}
286291
requestBuilder.setSingleUseTransaction(
287292
defaultTransactionOptions().toBuilder().mergeFrom(transactionOptionsBuilder.build()));
288293

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@
6969
import com.google.spanner.v1.SpannerGrpc;
7070
import com.google.spanner.v1.TransactionOptions;
7171
import com.google.spanner.v1.TransactionOptions.IsolationLevel;
72+
import com.google.spanner.v1.TransactionOptions.ReadWrite;
73+
import com.google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode;
7274
import io.grpc.CallCredentials;
7375
import io.grpc.CompressorRegistry;
7476
import io.grpc.Context;
@@ -1700,6 +1702,7 @@ public Builder setEnableEndToEndTracing(boolean enableEndToEndTracing) {
17001702
* <pre>{@code
17011703
* DefaultReadWriteTransactionOptions options = DefaultReadWriteTransactionOptions.newBuilder()
17021704
* .setIsolationLevel(IsolationLevel.SERIALIZABLE)
1705+
* .setReadLockMode(ReadLockMode.OPTIMISTIC)
17031706
* .build();
17041707
* }</pre>
17051708
*/
@@ -1724,6 +1727,13 @@ public DefaultReadWriteTransactionOptionsBuilder setIsolationLevel(
17241727
return this;
17251728
}
17261729

1730+
public DefaultReadWriteTransactionOptionsBuilder setReadLockMode(
1731+
ReadLockMode readLockMode) {
1732+
transactionOptionsBuilder.setReadWrite(
1733+
ReadWrite.newBuilder().setReadLockMode(readLockMode));
1734+
return this;
1735+
}
1736+
17271737
public DefaultReadWriteTransactionOptions build() {
17281738
return new DefaultReadWriteTransactionOptions(transactionOptionsBuilder.build());
17291739
}

0 commit comments

Comments
 (0)