Skip to content

feat: support read lock mode for R/W transactions #4010

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import com.google.cloud.spanner.Statement.StatementFactory;
import com.google.spanner.v1.BatchWriteResponse;
import com.google.spanner.v1.TransactionOptions.IsolationLevel;
import com.google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode;

/**
* Interface for all the APIs that are used to read/write data into a Cloud Spanner database. An
Expand Down Expand Up @@ -417,6 +418,7 @@ ServerStream<BatchWriteResponse> batchWriteAtLeastOnce(
* <li>{@link Options#commitStats()}: Request that the server includes commit statistics in the
* {@link CommitResponse}.
* <li>{@link Options#isolationLevel(IsolationLevel)}: The isolation level for the transaction
* <li>{@link Options#readLockMode(ReadLockMode)}: The read lock mode for the transaction
* </ul>
*/
TransactionRunner readWriteTransaction(TransactionOption... options);
Expand Down Expand Up @@ -458,6 +460,7 @@ ServerStream<BatchWriteResponse> batchWriteAtLeastOnce(
* <li>{@link Options#commitStats()}: Request that the server includes commit statistics in the
* {@link CommitResponse}.
* <li>{@link Options#isolationLevel(IsolationLevel)}: The isolation level for the transaction
* <li>{@link Options#readLockMode(ReadLockMode)}: The read lock mode for the transaction
* </ul>
*/
TransactionManager transactionManager(TransactionOption... options);
Expand Down Expand Up @@ -499,6 +502,7 @@ ServerStream<BatchWriteResponse> batchWriteAtLeastOnce(
* <li>{@link Options#commitStats()}: Request that the server includes commit statistics in the
* {@link CommitResponse}.
* <li>{@link Options#isolationLevel(IsolationLevel)}: The isolation level for the transaction
* <li>{@link Options#readLockMode(ReadLockMode)}: The read lock mode for the transaction
* </ul>
*/
AsyncRunner runAsync(TransactionOption... options);
Expand Down Expand Up @@ -554,6 +558,7 @@ ServerStream<BatchWriteResponse> batchWriteAtLeastOnce(
* <li>{@link Options#commitStats()}: Request that the server includes commit statistics in the
* {@link CommitResponse}.
* <li>{@link Options#isolationLevel(IsolationLevel)}: The isolation level for the transaction
* <li>{@link Options#readLockMode(ReadLockMode)}: The read lock mode for the transaction
* </ul>
*/
AsyncTransactionManager transactionManagerAsync(TransactionOption... options);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import com.google.spanner.v1.ReadRequest.OrderBy;
import com.google.spanner.v1.RequestOptions.Priority;
import com.google.spanner.v1.TransactionOptions.IsolationLevel;
import com.google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode;
import java.io.Serializable;
import java.time.Duration;
import java.util.Objects;
Expand Down Expand Up @@ -155,9 +156,44 @@ public static TransactionOption commitStats() {
* process in the commit phase (when any needed locks are acquired). The validation process
* succeeds only if there are no conflicting committed transactions (that committed mutations to
* the read data at a commit timestamp after the read timestamp).
*
* @deprecated Use {@link Options#readLockMode(ReadLockMode)} instead.
*/
@Deprecated
public static TransactionOption optimisticLock() {
return OPTIMISTIC_LOCK_OPTION;
return Options.readLockMode(ReadLockMode.OPTIMISTIC);
}

/**
* Returns a {@link TransactionOption} to set the desired {@link ReadLockMode} for a read-write
* transaction.
*
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we also add some generic advice when to use what? (Otherwise, I think that most users will automatically assume that 'optimistic is always better than pessimistic').

So for example something like this:

Suggested change
*
* Optimistic locking can improve throughput, as transactions wait with taking locks until they try to commit,
* but increases the probability that Spanner needs to abort a transaction due to lock conflicts. Pessimistic
* locking instructs transactions to take locks earlier, which means that concurrent transactions might have
* to wait for other transactions to release locks before proceeding. Pessimistic locking reduces the probability
* that Spanner needs to abort a transaction due to lock conflicts.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I initially added some advice to the document but removed it because I felt it deserved a more detailed explanation in a separate link. I've now added the advice back, and we will update this document once the RR + Pessimistic feature is launched.

* <p>This option controls the locking behavior for read operations and queries within a
* read-write transaction. It works in conjunction with the transaction's {@link IsolationLevel}.
*
* <ul>
* <li>{@link ReadLockMode#PESSIMISTIC}: Read locks are acquired immediately on read. This mode
* primarily applies to transactions with {@code SERIALIZABLE} isolation.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this sentence mean?

This mode primarily applies to transactions with {@code SERIALIZABLE} isolation.

That it has no effect on RR? Or that it has a strong effect on Serializable?

What I'm aiming at is: Can we make this more concrete? As a customer, I would have no idea what this means.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the doc. Currently, we do not allow setting the readLockMode for Repeatable Read(RR) isolation level and error will be returned. And by default, OPTIMISTIC lock mode will be used for RR.

* <li>{@link ReadLockMode#OPTIMISTIC}: Locks for reads within the transaction are not acquired
* on read. Instead the locks are acquired on a commit to validate that read/queried data
* has not changed since the transaction started. If a conflict is detected, the transaction
* will fail. This mode applies to transactions with {@code SERIALIZABLE} isolation.
* <li>{@link ReadLockMode#READ_LOCK_MODE_UNSPECIFIED}: This is the default if no mode is set.
* The locking behavior depends on the isolation level:
* <ul>
* <li>For {@code REPEATABLE_READ} isolation: Locking semantics default to {@code
* OPTIMISTIC}. However, validation checks at commit are only performed for reads
* within queries using {@code SELECT FOR UPDATE}, statements with {@code
* LOCK_SCANNED_RANGES} hints, or DML statements. <br>
* Note: It is an error to explicitly set {@code ReadLockMode} when the isolation
* level is {@code REPEATABLE_READ}.
* <li>For all other isolation levels: If the read lock mode is not set, it defaults to
* {@code PESSIMISTIC} locking.
* </ul>
* </ul>
*/
public static TransactionOption readLockMode(ReadLockMode readLockMode) {
return new ReadLockModeOption(readLockMode);
}

/**
Expand Down Expand Up @@ -367,16 +403,6 @@ void appendToOptions(Options options) {
}
}

/** Option to request Optimistic Concurrency Control for read/write transactions. */
static final class OptimisticLockOption extends InternalOption implements TransactionOption {
@Override
void appendToOptions(Options options) {
options.withOptimisticLock = true;
}
}

static final OptimisticLockOption OPTIMISTIC_LOCK_OPTION = new OptimisticLockOption();

/** Option to request the transaction to be excluded from change streams. */
static final class ExcludeTxnFromChangeStreamsOption extends InternalOption
implements UpdateTransactionOption {
Expand Down Expand Up @@ -516,6 +542,20 @@ void appendToOptions(Options options) {
}
}

/** Option to set read lock mode for read/write transactions. */
static final class ReadLockModeOption extends InternalOption implements TransactionOption {
private final ReadLockMode readLockMode;

public ReadLockModeOption(ReadLockMode readLockMode) {
this.readLockMode = readLockMode;
}

@Override
void appendToOptions(Options options) {
options.readLockMode = readLockMode;
}
}

private boolean withCommitStats;

private Duration maxCommitDelay;
Expand All @@ -530,7 +570,6 @@ void appendToOptions(Options options) {
private String tag;
private String etag;
private Boolean validateOnly;
private Boolean withOptimisticLock;
private Boolean withExcludeTxnFromChangeStreams;
private Boolean dataBoostEnabled;
private DirectedReadOptions directedReadOptions;
Expand All @@ -540,6 +579,7 @@ void appendToOptions(Options options) {
private Boolean lastStatement;
private IsolationLevel isolationLevel;
private XGoogSpannerRequestId reqId;
private ReadLockMode readLockMode;

// Construction is via factory methods below.
private Options() {}
Expand Down Expand Up @@ -644,10 +684,6 @@ Boolean validateOnly() {
return validateOnly;
}

Boolean withOptimisticLock() {
return withOptimisticLock;
}

Boolean withExcludeTxnFromChangeStreams() {
return withExcludeTxnFromChangeStreams;
}
Expand Down Expand Up @@ -704,6 +740,10 @@ IsolationLevel isolationLevel() {
return isolationLevel;
}

ReadLockMode readLockMode() {
return readLockMode;
}

@Override
public String toString() {
StringBuilder b = new StringBuilder();
Expand Down Expand Up @@ -740,9 +780,6 @@ public String toString() {
if (validateOnly != null) {
b.append("validateOnly: ").append(validateOnly).append(' ');
}
if (withOptimisticLock != null) {
b.append("withOptimisticLock: ").append(withOptimisticLock).append(' ');
}
if (withExcludeTxnFromChangeStreams != null) {
b.append("withExcludeTxnFromChangeStreams: ")
.append(withExcludeTxnFromChangeStreams)
Expand Down Expand Up @@ -772,6 +809,9 @@ public String toString() {
if (reqId != null) {
b.append("requestId: ").append(reqId.toString());
}
if (readLockMode != null) {
b.append("readLockMode: ").append(readLockMode).append(' ');
}
return b.toString();
}

Expand Down Expand Up @@ -807,15 +847,15 @@ public boolean equals(Object o) {
&& Objects.equals(tag(), that.tag())
&& Objects.equals(etag(), that.etag())
&& Objects.equals(validateOnly(), that.validateOnly())
&& Objects.equals(withOptimisticLock(), that.withOptimisticLock())
&& Objects.equals(withExcludeTxnFromChangeStreams(), that.withExcludeTxnFromChangeStreams())
&& Objects.equals(dataBoostEnabled(), that.dataBoostEnabled())
&& Objects.equals(directedReadOptions(), that.directedReadOptions())
&& Objects.equals(orderBy(), that.orderBy())
&& Objects.equals(isLastStatement(), that.isLastStatement())
&& Objects.equals(lockHint(), that.lockHint())
&& Objects.equals(isolationLevel(), that.isolationLevel())
&& Objects.equals(reqId(), that.reqId());
&& Objects.equals(reqId(), that.reqId())
&& Objects.equals(readLockMode(), that.readLockMode());
}

@Override
Expand Down Expand Up @@ -857,9 +897,6 @@ public int hashCode() {
if (validateOnly != null) {
result = 31 * result + validateOnly.hashCode();
}
if (withOptimisticLock != null) {
result = 31 * result + withOptimisticLock.hashCode();
}
if (withExcludeTxnFromChangeStreams != null) {
result = 31 * result + withExcludeTxnFromChangeStreams.hashCode();
}
Expand Down Expand Up @@ -887,6 +924,9 @@ public int hashCode() {
if (reqId != null) {
result = 31 * result + reqId.hashCode();
}
if (readLockMode != null) {
result = 31 * result + readLockMode.hashCode();
}
return result;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
import com.google.spanner.v1.RequestOptions;
import com.google.spanner.v1.Transaction;
import com.google.spanner.v1.TransactionOptions;
import com.google.spanner.v1.TransactionOptions.ReadWrite;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
Expand Down Expand Up @@ -76,16 +77,16 @@ static TransactionOptions createReadWriteTransactionOptions(
transactionOptions.setExcludeTxnFromChangeStreams(true);
}
TransactionOptions.ReadWrite.Builder readWrite = TransactionOptions.ReadWrite.newBuilder();
if (options.withOptimisticLock() == Boolean.TRUE) {
readWrite.setReadLockMode(TransactionOptions.ReadWrite.ReadLockMode.OPTIMISTIC);
}
if (previousTransactionId != null
&& previousTransactionId != com.google.protobuf.ByteString.EMPTY) {
readWrite.setMultiplexedSessionPreviousTransactionId(previousTransactionId);
}
if (options.isolationLevel() != null) {
transactionOptions.setIsolationLevel(options.isolationLevel());
}
if (options.readLockMode() != null) {
readWrite.setReadLockMode(options.readLockMode());
}
transactionOptions.setReadWrite(readWrite);
return transactionOptions.build();
}
Expand Down Expand Up @@ -283,6 +284,10 @@ public CommitResponse writeAtLeastOnceWithOptions(
if (options.isolationLevel() != null) {
transactionOptionsBuilder.setIsolationLevel(options.isolationLevel());
}
if (options.readLockMode() != null) {
transactionOptionsBuilder.setReadWrite(
ReadWrite.newBuilder().setReadLockMode(options.readLockMode()));
}
requestBuilder.setSingleUseTransaction(
defaultTransactionOptions().toBuilder().mergeFrom(transactionOptionsBuilder.build()));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@
import com.google.spanner.v1.SpannerGrpc;
import com.google.spanner.v1.TransactionOptions;
import com.google.spanner.v1.TransactionOptions.IsolationLevel;
import com.google.spanner.v1.TransactionOptions.ReadWrite;
import com.google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode;
import io.grpc.CallCredentials;
import io.grpc.CompressorRegistry;
import io.grpc.Context;
Expand Down Expand Up @@ -1700,6 +1702,7 @@ public Builder setEnableEndToEndTracing(boolean enableEndToEndTracing) {
* <pre>{@code
* DefaultReadWriteTransactionOptions options = DefaultReadWriteTransactionOptions.newBuilder()
* .setIsolationLevel(IsolationLevel.SERIALIZABLE)
* .setReadLockMode(ReadLockMode.OPTIMISTIC)
* .build();
* }</pre>
*/
Expand All @@ -1724,6 +1727,13 @@ public DefaultReadWriteTransactionOptionsBuilder setIsolationLevel(
return this;
}

public DefaultReadWriteTransactionOptionsBuilder setReadLockMode(
ReadLockMode readLockMode) {
transactionOptionsBuilder.setReadWrite(
ReadWrite.newBuilder().setReadLockMode(readLockMode));
return this;
}

public DefaultReadWriteTransactionOptions build() {
return new DefaultReadWriteTransactionOptions(transactionOptionsBuilder.build());
}
Expand Down
Loading
Loading