Skip to content

Commit 261abb4

Browse files
feat: read_lock_mode support for connections (#4031)
* feat: read_lock_mode support for connections Add read_lock_mode support for the Connection API. * chore: generate libraries at Wed Sep 3 15:24:37 UTC 2025 --------- Co-authored-by: cloud-java-bot <cloud-java-bot@google.com>
1 parent 8bcb09d commit 261abb4

19 files changed

+3022
-319
lines changed

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1033,4 +1033,14 @@
10331033
<className>com/google/cloud/spanner/SpannerOptions$SpannerEnvironment</className>
10341034
<method>boolean isEnableDirectAccess()</method>
10351035
</difference>
1036+
<difference>
1037+
<differenceType>7012</differenceType>
1038+
<className>com/google/cloud/spanner/connection/Connection</className>
1039+
<method>void setReadLockMode(com.google.spanner.v1.TransactionOptions$ReadWrite$ReadLockMode)</method>
1040+
</difference>
1041+
<difference>
1042+
<differenceType>7012</differenceType>
1043+
<className>com/google/cloud/spanner/connection/Connection</className>
1044+
<method>com.google.spanner.v1.TransactionOptions$ReadWrite$ReadLockMode getReadLockMode()</method>
1045+
</difference>
10361046
</differences>

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import com.google.common.base.Strings;
3535
import com.google.spanner.v1.DirectedReadOptions;
3636
import com.google.spanner.v1.TransactionOptions;
37+
import com.google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode;
3738
import java.lang.reflect.Constructor;
3839
import java.lang.reflect.InvocationTargetException;
3940
import java.time.Duration;
@@ -425,6 +426,36 @@ public TransactionOptions.IsolationLevel convert(String value) {
425426
}
426427
}
427428

429+
/**
430+
* Converter for converting strings to {@link
431+
* com.google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode} values.
432+
*/
433+
static class ReadLockModeConverter implements ClientSideStatementValueConverter<ReadLockMode> {
434+
static final ReadLockModeConverter INSTANCE = new ReadLockModeConverter();
435+
436+
private final CaseInsensitiveEnumMap<ReadLockMode> values =
437+
new CaseInsensitiveEnumMap<>(ReadLockMode.class);
438+
439+
ReadLockModeConverter() {}
440+
441+
/** Constructor needed for reflection. */
442+
public ReadLockModeConverter(String allowedValues) {}
443+
444+
@Override
445+
public Class<ReadLockMode> getParameterClass() {
446+
return ReadLockMode.class;
447+
}
448+
449+
@Override
450+
public ReadLockMode convert(String value) {
451+
if (value != null && value.equalsIgnoreCase("unspecified")) {
452+
// Allow 'unspecified' to be used in addition to 'read_lock_mode_unspecified'.
453+
value = ReadLockMode.READ_LOCK_MODE_UNSPECIFIED.name();
454+
}
455+
return values.get(value);
456+
}
457+
}
458+
428459
/** Converter for converting strings to {@link AutocommitDmlMode} values. */
429460
static class AutocommitDmlModeConverter
430461
implements ClientSideStatementValueConverter<AutocommitDmlMode> {

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import com.google.spanner.v1.ExecuteBatchDmlRequest;
4444
import com.google.spanner.v1.ResultSetStats;
4545
import com.google.spanner.v1.TransactionOptions.IsolationLevel;
46+
import com.google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode;
4647
import java.time.Duration;
4748
import java.util.Iterator;
4849
import java.util.Set;
@@ -232,6 +233,12 @@ public interface Connection extends AutoCloseable {
232233
/** Returns the default isolation level for read/write transactions for this connection. */
233234
IsolationLevel getDefaultIsolationLevel();
234235

236+
/** Sets the read lock mode for read/write transactions for this connection. */
237+
void setReadLockMode(ReadLockMode readLockMode);
238+
239+
/** Returns the read lock mode for read/write transactions for this connection. */
240+
ReadLockMode getReadLockMode();
241+
235242
/**
236243
* Sets the duration the connection should wait before automatically aborting the execution of a
237244
* 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: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import static com.google.cloud.spanner.connection.ConnectionProperties.OPTIMIZER_STATISTICS_PACKAGE;
3939
import static com.google.cloud.spanner.connection.ConnectionProperties.OPTIMIZER_VERSION;
4040
import static com.google.cloud.spanner.connection.ConnectionProperties.READONLY;
41+
import static com.google.cloud.spanner.connection.ConnectionProperties.READ_LOCK_MODE;
4142
import static com.google.cloud.spanner.connection.ConnectionProperties.READ_ONLY_STALENESS;
4243
import static com.google.cloud.spanner.connection.ConnectionProperties.RETRY_ABORTS_INTERNALLY;
4344
import static com.google.cloud.spanner.connection.ConnectionProperties.RETURN_COMMIT_STATS;
@@ -92,6 +93,7 @@
9293
import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions;
9394
import com.google.spanner.v1.ResultSetStats;
9495
import com.google.spanner.v1.TransactionOptions.IsolationLevel;
96+
import com.google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode;
9597
import io.opentelemetry.api.OpenTelemetry;
9698
import io.opentelemetry.api.common.Attributes;
9799
import io.opentelemetry.api.common.AttributesBuilder;
@@ -486,6 +488,7 @@ private void reset(Context context, boolean inTransaction) {
486488
this.connectionState.resetValue(AUTOCOMMIT, context, inTransaction);
487489
this.connectionState.resetValue(READONLY, context, inTransaction);
488490
this.connectionState.resetValue(DEFAULT_ISOLATION_LEVEL, context, inTransaction);
491+
this.connectionState.resetValue(READ_LOCK_MODE, context, inTransaction);
489492
this.connectionState.resetValue(READ_ONLY_STALENESS, context, inTransaction);
490493
this.connectionState.resetValue(OPTIMIZER_VERSION, context, inTransaction);
491494
this.connectionState.resetValue(OPTIMIZER_STATISTICS_PACKAGE, context, inTransaction);
@@ -668,6 +671,18 @@ private void clearLastTransactionAndSetDefaultTransactionOptions(IsolationLevel
668671
this.currentUnitOfWork = null;
669672
}
670673

674+
@Override
675+
public void setReadLockMode(ReadLockMode readLockMode) {
676+
ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG);
677+
setConnectionPropertyValue(READ_LOCK_MODE, readLockMode);
678+
}
679+
680+
@Override
681+
public ReadLockMode getReadLockMode() {
682+
ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG);
683+
return getConnectionPropertyValue(READ_LOCK_MODE);
684+
}
685+
671686
@Override
672687
public void setAutocommitDmlMode(AutocommitDmlMode mode) {
673688
Preconditions.checkNotNull(mode);
@@ -2255,6 +2270,7 @@ UnitOfWork createNewUnitOfWork(
22552270
.setUseAutoSavepointsForEmulator(options.useAutoSavepointsForEmulator())
22562271
.setDatabaseClient(dbClient)
22572272
.setIsolationLevel(transactionIsolationLevel)
2273+
.setReadLockMode(getConnectionPropertyValue(READ_LOCK_MODE))
22582274
.setDelayTransactionStartUntilFirstWrite(
22592275
getConnectionPropertyValue(DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE))
22602276
.setKeepTransactionAlive(getConnectionPropertyValue(KEEP_TRANSACTION_ALIVE))

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@
115115
import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.IsolationLevelConverter;
116116
import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.LongConverter;
117117
import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.NonNegativeIntegerConverter;
118+
import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.ReadLockModeConverter;
118119
import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.ReadOnlyStalenessConverter;
119120
import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.RpcPriorityConverter;
120121
import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.SavepointSupportConverter;
@@ -125,8 +126,10 @@
125126
import com.google.common.collect.ImmutableMap;
126127
import com.google.spanner.v1.DirectedReadOptions;
127128
import com.google.spanner.v1.TransactionOptions.IsolationLevel;
129+
import com.google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode;
128130
import java.time.Duration;
129131
import java.util.Arrays;
132+
import java.util.stream.Collectors;
130133

131134
/** Utility class that defines all known connection properties. */
132135
public class ConnectionProperties {
@@ -451,6 +454,38 @@ public class ConnectionProperties {
451454
},
452455
IsolationLevelConverter.INSTANCE,
453456
Context.USER);
457+
static final ConnectionProperty<ReadLockMode> READ_LOCK_MODE =
458+
create(
459+
"read_lock_mode",
460+
"This option controls the locking behavior for read operations and queries within a"
461+
+ " read/write transaction. It works in conjunction with the transaction's isolation"
462+
+ " level.\n\n"
463+
+ "PESSIMISTIC: Read locks are acquired immediately on read. This mode only applies"
464+
+ " to SERIALIZABLE isolation. This mode prevents concurrent modifications by locking"
465+
+ " data throughout the transaction. This reduces commit-time aborts due to"
466+
+ " conflicts, but can increase how long transactions wait for locks and the overall"
467+
+ " contention.\n\n"
468+
+ "OPTIMISTIC: Locks for reads within the transaction are not acquired on read."
469+
+ " Instead, the locks are acquired on commit to validate that read/queried data has"
470+
+ " not changed since the transaction started. If a conflict is detected, the"
471+
+ " transaction will fail. This mode only applies to SERIALIZABLE isolation. This"
472+
+ " mode defers locking until commit, which can reduce contention and improve"
473+
+ " throughput. However, be aware that this increases the risk of transaction aborts"
474+
+ " if there's significant write competition on the same data.\n\n"
475+
+ "READ_LOCK_MODE_UNSPECIFIED: This is the default if no mode is set. The locking"
476+
+ " behavior depends on the isolation level:\n\n"
477+
+ "REPEATABLE_READ: Locking semantics default to OPTIMISTIC. However, validation"
478+
+ " checks at commit are only performed for queries using SELECT FOR UPDATE,"
479+
+ " statements with {@code LOCK_SCANNED_RANGES} hints, and DML statements.\n\n"
480+
+ "For all other isolation levels: If the read lock mode is not set, it defaults to"
481+
+ " PESSIMISTIC locking.",
482+
ReadLockMode.READ_LOCK_MODE_UNSPECIFIED,
483+
Arrays.stream(ReadLockMode.values())
484+
.filter(mode -> !mode.equals(ReadLockMode.UNRECOGNIZED))
485+
.collect(Collectors.toList())
486+
.toArray(new ReadLockMode[0]),
487+
ReadLockModeConverter.INSTANCE,
488+
Context.USER);
454489
static final ConnectionProperty<AutocommitDmlMode> AUTOCOMMIT_DML_MODE =
455490
create(
456491
"autocommit_dml_mode",

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import com.google.cloud.spanner.connection.PgTransactionMode.IsolationLevel;
2424
import com.google.spanner.v1.DirectedReadOptions;
2525
import com.google.spanner.v1.TransactionOptions;
26+
import com.google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode;
2627
import java.time.Duration;
2728

2829
/**
@@ -190,4 +191,8 @@ StatementResult statementSetPgSessionCharacteristicsTransactionMode(
190191
StatementResult statementSetAutoBatchDmlUpdateCountVerification(Boolean verification);
191192

192193
StatementResult statementShowAutoBatchDmlUpdateCountVerification();
194+
195+
StatementResult statementSetReadLockMode(ReadLockMode readLockMode);
196+
197+
StatementResult statementShowReadLockMode();
193198
}

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_PROTO_DESCRIPTORS;
4444
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_PROTO_DESCRIPTORS_FILE_PATH;
4545
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_READONLY;
46+
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_READ_LOCK_MODE;
4647
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_READ_ONLY_STALENESS;
4748
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_RETRY_ABORTS_INTERNALLY;
4849
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SET_RETURN_COMMIT_STATS;
@@ -73,6 +74,7 @@
7374
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_PROTO_DESCRIPTORS;
7475
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_PROTO_DESCRIPTORS_FILE_PATH;
7576
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_READONLY;
77+
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_READ_LOCK_MODE;
7678
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_READ_ONLY_STALENESS;
7779
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_READ_TIMESTAMP;
7880
import static com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType.SHOW_RETRY_ABORTS_INTERNALLY;
@@ -113,6 +115,7 @@
113115
import com.google.spanner.v1.QueryPlan;
114116
import com.google.spanner.v1.RequestOptions;
115117
import com.google.spanner.v1.TransactionOptions;
118+
import com.google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode;
116119
import java.time.Duration;
117120
import java.util.ArrayList;
118121
import java.util.Collections;
@@ -607,6 +610,20 @@ public StatementResult statementShowSavepointSupport() {
607610
SHOW_SAVEPOINT_SUPPORT);
608611
}
609612

613+
@Override
614+
public StatementResult statementSetReadLockMode(ReadLockMode readLockMode) {
615+
getConnection().setReadLockMode(readLockMode);
616+
return noResult(SET_READ_LOCK_MODE);
617+
}
618+
619+
@Override
620+
public StatementResult statementShowReadLockMode() {
621+
return resultSet(
622+
String.format("%sREAD_LOCK_MODE", getNamespace(connection.getDialect())),
623+
getConnection().getReadLockMode(),
624+
SHOW_READ_LOCK_MODE);
625+
}
626+
610627
@Override
611628
public StatementResult statementShowTransactionIsolationLevel() {
612629
return resultSet("transaction_isolation", "serializable", SHOW_TRANSACTION_ISOLATION_LEVEL);

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
import com.google.common.util.concurrent.MoreExecutors;
6262
import com.google.spanner.v1.SpannerGrpc;
6363
import com.google.spanner.v1.TransactionOptions.IsolationLevel;
64+
import com.google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode;
6465
import io.opentelemetry.api.common.AttributeKey;
6566
import io.opentelemetry.context.Scope;
6667
import java.time.Duration;
@@ -155,6 +156,7 @@ class ReadWriteTransaction extends AbstractMultiUseTransaction {
155156
private final ReentrantLock keepAliveLock;
156157
private final SavepointSupport savepointSupport;
157158
@Nonnull private final IsolationLevel isolationLevel;
159+
private final ReadLockMode readLockMode;
158160
private int transactionRetryAttempts;
159161
private int successfulRetries;
160162
private volatile ApiFuture<TransactionContext> txContextFuture;
@@ -207,6 +209,7 @@ static class Builder extends AbstractMultiUseTransaction.Builder<Builder, ReadWr
207209
private Duration maxCommitDelay;
208210
private SavepointSupport savepointSupport;
209211
private IsolationLevel isolationLevel;
212+
private ReadLockMode readLockMode = ReadLockMode.READ_LOCK_MODE_UNSPECIFIED;
210213

211214
private Builder() {}
212215

@@ -261,6 +264,11 @@ Builder setIsolationLevel(IsolationLevel isolationLevel) {
261264
return this;
262265
}
263266

267+
Builder setReadLockMode(ReadLockMode readLockMode) {
268+
this.readLockMode = Preconditions.checkNotNull(readLockMode);
269+
return this;
270+
}
271+
264272
@Override
265273
ReadWriteTransaction build() {
266274
Preconditions.checkState(dbClient != null, "No DatabaseClient client specified");
@@ -305,6 +313,7 @@ private ReadWriteTransaction(Builder builder) {
305313
this.retryAbortsInternally = builder.retryAbortsInternally;
306314
this.savepointSupport = builder.savepointSupport;
307315
this.isolationLevel = Preconditions.checkNotNull(builder.isolationLevel);
316+
this.readLockMode = Preconditions.checkNotNull(builder.readLockMode);
308317
this.transactionOptions = extractOptions(builder);
309318
}
310319

@@ -328,6 +337,9 @@ private TransactionOption[] extractOptions(Builder builder) {
328337
if (this.isolationLevel != IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED) {
329338
numOptions++;
330339
}
340+
if (this.readLockMode != ReadLockMode.READ_LOCK_MODE_UNSPECIFIED) {
341+
numOptions++;
342+
}
331343
TransactionOption[] options = new TransactionOption[numOptions];
332344
int index = 0;
333345
if (builder.returnCommitStats) {
@@ -348,6 +360,9 @@ private TransactionOption[] extractOptions(Builder builder) {
348360
if (this.isolationLevel != IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED) {
349361
options[index++] = Options.isolationLevel(this.isolationLevel);
350362
}
363+
if (this.readLockMode != ReadLockMode.READ_LOCK_MODE_UNSPECIFIED) {
364+
options[index++] = Options.readLockMode(this.readLockMode);
365+
}
351366
return options;
352367
}
353368

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import static com.google.cloud.spanner.connection.ConnectionProperties.DEFAULT_SEQUENCE_KIND;
2424
import static com.google.cloud.spanner.connection.ConnectionProperties.MAX_COMMIT_DELAY;
2525
import static com.google.cloud.spanner.connection.ConnectionProperties.READONLY;
26+
import static com.google.cloud.spanner.connection.ConnectionProperties.READ_LOCK_MODE;
2627
import static com.google.cloud.spanner.connection.ConnectionProperties.READ_ONLY_STALENESS;
2728
import static com.google.cloud.spanner.connection.ConnectionProperties.RETURN_COMMIT_STATS;
2829
import static com.google.cloud.spanner.connection.DdlClient.isCreateDatabaseStatement;
@@ -62,6 +63,7 @@
6263
import com.google.spanner.admin.database.v1.DatabaseAdminGrpc;
6364
import com.google.spanner.v1.SpannerGrpc;
6465
import com.google.spanner.v1.TransactionOptions.IsolationLevel;
66+
import com.google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode;
6567
import io.opentelemetry.context.Scope;
6668
import java.util.Arrays;
6769
import java.util.UUID;
@@ -514,6 +516,10 @@ private TransactionRunner createWriteTransaction() {
514516
!= IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED) {
515517
numOptions++;
516518
}
519+
if (connectionState.getValue(READ_LOCK_MODE).getValue()
520+
!= ReadLockMode.READ_LOCK_MODE_UNSPECIFIED) {
521+
numOptions++;
522+
}
517523
if (numOptions == 0) {
518524
return dbClient.readWriteTransaction();
519525
}
@@ -537,6 +543,10 @@ private TransactionRunner createWriteTransaction() {
537543
options[index++] =
538544
Options.isolationLevel(connectionState.getValue(DEFAULT_ISOLATION_LEVEL).getValue());
539545
}
546+
if (connectionState.getValue(READ_LOCK_MODE).getValue()
547+
!= ReadLockMode.READ_LOCK_MODE_UNSPECIFIED) {
548+
options[index++] = Options.readLockMode(connectionState.getValue(READ_LOCK_MODE).getValue());
549+
}
540550
return dbClient.readWriteTransaction(options);
541551
}
542552

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
@@ -120,6 +120,8 @@ enum ClientSideStatementType {
120120
SHOW_AUTO_BATCH_DML_UPDATE_COUNT,
121121
SET_AUTO_BATCH_DML_UPDATE_COUNT_VERIFICATION,
122122
SHOW_AUTO_BATCH_DML_UPDATE_COUNT_VERIFICATION,
123+
SHOW_READ_LOCK_MODE,
124+
SET_READ_LOCK_MODE,
123125
}
124126

125127
/**

0 commit comments

Comments
 (0)