Skip to content

Commit d1ba012

Browse files
committed
feat: add default_isolation_level connection property
Add a `default_isolation_level` property for the Connection API. This property will be used by the JDBC driver and PGAdapter to set a default isolation level for all read/write transactions that are executed by a connection. Support for setting an isolation level for a single transaction will be added in a follow-up pull request.
1 parent 67188df commit d1ba012

12 files changed

+334
-23
lines changed

Diff for: google-cloud-spanner/clirr-ignored-differences.xml

+12
Original file line numberDiff line numberDiff line change
@@ -850,4 +850,16 @@
850850
<className>com/google/cloud/spanner/connection/Connection</className>
851851
<method>java.lang.String getDefaultSequenceKind()</method>
852852
</difference>
853+
854+
<!-- Default isolation level -->
855+
<difference>
856+
<differenceType>7012</differenceType>
857+
<className>com/google/cloud/spanner/connection/Connection</className>
858+
<method>void setDefaultIsolationLevel(com.google.spanner.v1.TransactionOptions$IsolationLevel)</method>
859+
</difference>
860+
<difference>
861+
<differenceType>7012</differenceType>
862+
<className>com/google/cloud/spanner/connection/Connection</className>
863+
<method>com.google.spanner.v1.TransactionOptions$IsolationLevel getDefaultIsolationLevel()</method>
864+
</difference>
853865
</differences>

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

+28
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import com.google.common.base.Preconditions;
3434
import com.google.common.base.Strings;
3535
import com.google.spanner.v1.DirectedReadOptions;
36+
import com.google.spanner.v1.TransactionOptions;
3637
import java.lang.reflect.Constructor;
3738
import java.lang.reflect.InvocationTargetException;
3839
import java.time.Duration;
@@ -382,6 +383,33 @@ public DirectedReadOptions convert(String value) {
382383
}
383384
}
384385

386+
/**
387+
* Converter for converting strings to {@link
388+
* com.google.spanner.v1.TransactionOptions.IsolationLevel} values.
389+
*/
390+
static class IsolationLevelConverter
391+
implements ClientSideStatementValueConverter<TransactionOptions.IsolationLevel> {
392+
static final IsolationLevelConverter INSTANCE = new IsolationLevelConverter();
393+
394+
private final CaseInsensitiveEnumMap<TransactionOptions.IsolationLevel> values =
395+
new CaseInsensitiveEnumMap<>(TransactionOptions.IsolationLevel.class);
396+
397+
private IsolationLevelConverter() {}
398+
399+
/** Constructor needed for reflection. */
400+
public IsolationLevelConverter(String allowedValues) {}
401+
402+
@Override
403+
public Class<TransactionOptions.IsolationLevel> getParameterClass() {
404+
return TransactionOptions.IsolationLevel.class;
405+
}
406+
407+
@Override
408+
public TransactionOptions.IsolationLevel convert(String value) {
409+
return values.get(value);
410+
}
411+
}
412+
385413
/** Converter for converting strings to {@link AutocommitDmlMode} values. */
386414
static class AutocommitDmlModeConverter
387415
implements ClientSideStatementValueConverter<AutocommitDmlMode> {

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

+7
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import com.google.spanner.v1.DirectedReadOptions;
4343
import com.google.spanner.v1.ExecuteBatchDmlRequest;
4444
import com.google.spanner.v1.ResultSetStats;
45+
import com.google.spanner.v1.TransactionOptions.IsolationLevel;
4546
import java.time.Duration;
4647
import java.util.Iterator;
4748
import java.util.Set;
@@ -219,6 +220,12 @@ public interface Connection extends AutoCloseable {
219220
/** @return <code>true</code> if this connection is in read-only mode */
220221
boolean isReadOnly();
221222

223+
/** Sets the default isolation level for read/write transactions for this connection. */
224+
void setDefaultIsolationLevel(IsolationLevel isolationLevel);
225+
226+
/** Returns the default isolation level for read/write transactions for this connection. */
227+
IsolationLevel getDefaultIsolationLevel();
228+
222229
/**
223230
* Sets the duration the connection should wait before automatically aborting the execution of a
224231
* statement. The default is no timeout. Statement timeouts are applied all types of statements,

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

+22
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import static com.google.cloud.spanner.connection.ConnectionProperties.AUTO_PARTITION_MODE;
2828
import static com.google.cloud.spanner.connection.ConnectionProperties.DATA_BOOST_ENABLED;
2929
import static com.google.cloud.spanner.connection.ConnectionProperties.DDL_IN_TRANSACTION_MODE;
30+
import static com.google.cloud.spanner.connection.ConnectionProperties.DEFAULT_ISOLATION_LEVEL;
3031
import static com.google.cloud.spanner.connection.ConnectionProperties.DEFAULT_SEQUENCE_KIND;
3132
import static com.google.cloud.spanner.connection.ConnectionProperties.DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE;
3233
import static com.google.cloud.spanner.connection.ConnectionProperties.DIRECTED_READ;
@@ -90,6 +91,7 @@
9091
import com.google.spanner.v1.DirectedReadOptions;
9192
import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions;
9293
import com.google.spanner.v1.ResultSetStats;
94+
import com.google.spanner.v1.TransactionOptions.IsolationLevel;
9395
import io.opentelemetry.api.OpenTelemetry;
9496
import io.opentelemetry.api.common.Attributes;
9597
import io.opentelemetry.api.common.AttributesBuilder;
@@ -478,6 +480,7 @@ private void reset(Context context, boolean inTransaction) {
478480
this.connectionState.resetValue(RETRY_ABORTS_INTERNALLY, context, inTransaction);
479481
this.connectionState.resetValue(AUTOCOMMIT, context, inTransaction);
480482
this.connectionState.resetValue(READONLY, context, inTransaction);
483+
this.connectionState.resetValue(DEFAULT_ISOLATION_LEVEL, context, inTransaction);
481484
this.connectionState.resetValue(READ_ONLY_STALENESS, context, inTransaction);
482485
this.connectionState.resetValue(OPTIMIZER_VERSION, context, inTransaction);
483486
this.connectionState.resetValue(OPTIMIZER_STATISTICS_PACKAGE, context, inTransaction);
@@ -635,6 +638,24 @@ public boolean isReadOnly() {
635638
return getConnectionPropertyValue(READONLY);
636639
}
637640

641+
@Override
642+
public void setDefaultIsolationLevel(IsolationLevel isolationLevel) {
643+
ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG);
644+
ConnectionPreconditions.checkState(
645+
!isBatchActive(), "Cannot default isolation level while in a batch");
646+
ConnectionPreconditions.checkState(
647+
!isTransactionStarted(),
648+
"Cannot set default isolation level while a transaction is active");
649+
setConnectionPropertyValue(DEFAULT_ISOLATION_LEVEL, isolationLevel);
650+
clearLastTransactionAndSetDefaultTransactionOptions();
651+
}
652+
653+
@Override
654+
public IsolationLevel getDefaultIsolationLevel() {
655+
ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG);
656+
return getConnectionPropertyValue(DEFAULT_ISOLATION_LEVEL);
657+
}
658+
638659
private void clearLastTransactionAndSetDefaultTransactionOptions() {
639660
setDefaultTransactionOptions();
640661
this.currentUnitOfWork = null;
@@ -2196,6 +2217,7 @@ UnitOfWork createNewUnitOfWork(
21962217
.setUsesEmulator(options.usesEmulator())
21972218
.setUseAutoSavepointsForEmulator(options.useAutoSavepointsForEmulator())
21982219
.setDatabaseClient(dbClient)
2220+
.setIsolationLevel(getConnectionPropertyValue(DEFAULT_ISOLATION_LEVEL))
21992221
.setDelayTransactionStartUntilFirstWrite(
22002222
getConnectionPropertyValue(DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE))
22012223
.setKeepTransactionAlive(getConnectionPropertyValue(KEEP_TRANSACTION_ALIVE))

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

+21-2
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@
112112
import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.DdlInTransactionModeConverter;
113113
import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.DialectConverter;
114114
import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.DurationConverter;
115+
import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.IsolationLevelConverter;
115116
import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.LongConverter;
116117
import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.NonNegativeIntegerConverter;
117118
import com.google.cloud.spanner.connection.ClientSideStatementValueConverters.ReadOnlyStalenessConverter;
@@ -123,7 +124,9 @@
123124
import com.google.common.collect.ImmutableList;
124125
import com.google.common.collect.ImmutableMap;
125126
import com.google.spanner.v1.DirectedReadOptions;
127+
import com.google.spanner.v1.TransactionOptions.IsolationLevel;
126128
import java.time.Duration;
129+
import java.util.Arrays;
127130

128131
/**
129132
* Utility class that defines all known connection properties. This class will eventually replace
@@ -401,13 +404,28 @@ public class ConnectionProperties {
401404
BOOLEANS,
402405
BooleanConverter.INSTANCE,
403406
Context.USER);
407+
static final ConnectionProperty<IsolationLevel> DEFAULT_ISOLATION_LEVEL =
408+
create(
409+
"default_isolation_level",
410+
"The transaction isolation level that is used by default for read/write transactions. "
411+
+ "The default is isolation_level_unspecified, which means that the connection will use the "
412+
+ "default isolation level of the database that it is connected to.",
413+
IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED,
414+
new IsolationLevel[] {
415+
IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED,
416+
IsolationLevel.SERIALIZABLE,
417+
IsolationLevel.REPEATABLE_READ
418+
},
419+
IsolationLevelConverter.INSTANCE,
420+
Context.USER);
404421
static final ConnectionProperty<AutocommitDmlMode> AUTOCOMMIT_DML_MODE =
405422
create(
406423
"autocommit_dml_mode",
407424
"Determines the transaction type that is used to execute "
408425
+ "DML statements when the connection is in auto-commit mode.",
409426
AutocommitDmlMode.TRANSACTIONAL,
410-
AutocommitDmlMode.values(),
427+
// Add 'null' as a valid value.
428+
Arrays.copyOf(AutocommitDmlMode.values(), AutocommitDmlMode.values().length + 1),
411429
AutocommitDmlModeConverter.INSTANCE,
412430
Context.USER);
413431
static final ConnectionProperty<Boolean> RETRY_ABORTS_INTERNALLY =
@@ -523,7 +541,8 @@ public class ConnectionProperties {
523541
RPC_PRIORITY_NAME,
524542
"Sets the priority for all RPC invocations from this connection (HIGH/MEDIUM/LOW). The default is HIGH.",
525543
DEFAULT_RPC_PRIORITY,
526-
RpcPriority.values(),
544+
// Add 'null' as a valid value.
545+
Arrays.copyOf(RpcPriority.values(), RpcPriority.values().length + 1),
527546
RpcPriorityConverter.INSTANCE,
528547
Context.USER);
529548
static final ConnectionProperty<SavepointSupport> SAVEPOINT_SUPPORT =

Diff for: google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionState.java

+20
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,12 @@
2727
import com.google.cloud.spanner.connection.ConnectionProperty.Context;
2828
import com.google.common.annotations.VisibleForTesting;
2929
import com.google.common.base.Suppliers;
30+
import java.util.Arrays;
3031
import java.util.Collections;
3132
import java.util.HashMap;
3233
import java.util.Map;
3334
import java.util.Map.Entry;
35+
import java.util.Objects;
3436
import java.util.function.Supplier;
3537
import javax.annotation.Nullable;
3638

@@ -233,6 +235,7 @@ private <T> void internalSetValue(
233235
T value,
234236
Map<String, ConnectionPropertyValue<?>> currentProperties,
235237
Context context) {
238+
checkValidValue(property, value);
236239
ConnectionPropertyValue<T> newValue = cast(currentProperties.get(property.getKey()));
237240
if (newValue == null) {
238241
ConnectionPropertyValue<T> existingValue = cast(properties.get(property.getKey()));
@@ -249,6 +252,23 @@ private <T> void internalSetValue(
249252
currentProperties.put(property.getKey(), newValue);
250253
}
251254

255+
static <T> void checkValidValue(ConnectionProperty<T> property, T value) {
256+
if (property.getValidValues() == null || property.getValidValues().length == 0) {
257+
return;
258+
}
259+
if (Arrays.stream(property.getValidValues())
260+
.noneMatch(validValue -> Objects.equals(validValue, value))) {
261+
throw invalidParamValueError(property, value);
262+
}
263+
}
264+
265+
/** Creates an exception for an invalid value for a connection property. */
266+
static <T> SpannerException invalidParamValueError(ConnectionProperty<T> property, T value) {
267+
return SpannerExceptionFactory.newSpannerException(
268+
ErrorCode.INVALID_ARGUMENT,
269+
String.format("invalid value \"%s\" for configuration property \"%s\"", value, property));
270+
}
271+
252272
/** Creates an exception for an unknown connection property. */
253273
static SpannerException unknownParamError(ConnectionProperty<?> property) {
254274
return SpannerExceptionFactory.newSpannerException(

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

+16
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
import com.google.common.collect.Iterables;
6161
import com.google.common.util.concurrent.MoreExecutors;
6262
import com.google.spanner.v1.SpannerGrpc;
63+
import com.google.spanner.v1.TransactionOptions.IsolationLevel;
6364
import io.opentelemetry.api.common.AttributeKey;
6465
import io.opentelemetry.context.Scope;
6566
import java.time.Duration;
@@ -151,6 +152,7 @@ class ReadWriteTransaction extends AbstractMultiUseTransaction {
151152
private final long keepAliveIntervalMillis;
152153
private final ReentrantLock keepAliveLock;
153154
private final SavepointSupport savepointSupport;
155+
@Nonnull private final IsolationLevel isolationLevel;
154156
private int transactionRetryAttempts;
155157
private int successfulRetries;
156158
private volatile ApiFuture<TransactionContext> txContextFuture;
@@ -202,6 +204,7 @@ static class Builder extends AbstractMultiUseTransaction.Builder<Builder, ReadWr
202204
private boolean returnCommitStats;
203205
private Duration maxCommitDelay;
204206
private SavepointSupport savepointSupport;
207+
private IsolationLevel isolationLevel;
205208

206209
private Builder() {}
207210

@@ -251,6 +254,11 @@ Builder setSavepointSupport(SavepointSupport savepointSupport) {
251254
return this;
252255
}
253256

257+
Builder setIsolationLevel(IsolationLevel isolationLevel) {
258+
this.isolationLevel = Preconditions.checkNotNull(isolationLevel);
259+
return this;
260+
}
261+
254262
@Override
255263
ReadWriteTransaction build() {
256264
Preconditions.checkState(dbClient != null, "No DatabaseClient client specified");
@@ -259,6 +267,7 @@ ReadWriteTransaction build() {
259267
Preconditions.checkState(
260268
hasTransactionRetryListeners(), "TransactionRetryListeners are not specified");
261269
Preconditions.checkState(savepointSupport != null, "SavepointSupport is not specified");
270+
Preconditions.checkState(isolationLevel != null, "IsolationLevel is not specified");
262271
return new ReadWriteTransaction(this);
263272
}
264273
}
@@ -293,6 +302,7 @@ private ReadWriteTransaction(Builder builder) {
293302
this.keepAliveLock = this.keepTransactionAlive ? new ReentrantLock() : null;
294303
this.retryAbortsInternally = builder.retryAbortsInternally;
295304
this.savepointSupport = builder.savepointSupport;
305+
this.isolationLevel = Preconditions.checkNotNull(builder.isolationLevel);
296306
this.transactionOptions = extractOptions(builder);
297307
}
298308

@@ -313,6 +323,9 @@ private TransactionOption[] extractOptions(Builder builder) {
313323
if (this.rpcPriority != null) {
314324
numOptions++;
315325
}
326+
if (this.isolationLevel != IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED) {
327+
numOptions++;
328+
}
316329
TransactionOption[] options = new TransactionOption[numOptions];
317330
int index = 0;
318331
if (builder.returnCommitStats) {
@@ -330,6 +343,9 @@ private TransactionOption[] extractOptions(Builder builder) {
330343
if (this.rpcPriority != null) {
331344
options[index++] = Options.priority(this.rpcPriority);
332345
}
346+
if (this.isolationLevel != IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED) {
347+
options[index++] = Options.isolationLevel(this.isolationLevel);
348+
}
333349
return options;
334350
}
335351

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

+11
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import static com.google.cloud.spanner.connection.AbstractStatementParser.COMMIT_STATEMENT;
2020
import static com.google.cloud.spanner.connection.AbstractStatementParser.RUN_BATCH_STATEMENT;
2121
import static com.google.cloud.spanner.connection.ConnectionProperties.AUTOCOMMIT_DML_MODE;
22+
import static com.google.cloud.spanner.connection.ConnectionProperties.DEFAULT_ISOLATION_LEVEL;
2223
import static com.google.cloud.spanner.connection.ConnectionProperties.DEFAULT_SEQUENCE_KIND;
2324
import static com.google.cloud.spanner.connection.ConnectionProperties.MAX_COMMIT_DELAY;
2425
import static com.google.cloud.spanner.connection.ConnectionProperties.READONLY;
@@ -60,6 +61,7 @@
6061
import com.google.common.util.concurrent.MoreExecutors;
6162
import com.google.spanner.admin.database.v1.DatabaseAdminGrpc;
6263
import com.google.spanner.v1.SpannerGrpc;
64+
import com.google.spanner.v1.TransactionOptions.IsolationLevel;
6365
import io.opentelemetry.context.Scope;
6466
import java.util.Arrays;
6567
import java.util.UUID;
@@ -508,6 +510,10 @@ private TransactionRunner createWriteTransaction() {
508510
if (connectionState.getValue(MAX_COMMIT_DELAY).getValue() != null) {
509511
numOptions++;
510512
}
513+
if (connectionState.getValue(DEFAULT_ISOLATION_LEVEL).getValue()
514+
!= IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED) {
515+
numOptions++;
516+
}
511517
if (numOptions == 0) {
512518
return dbClient.readWriteTransaction();
513519
}
@@ -526,6 +532,11 @@ private TransactionRunner createWriteTransaction() {
526532
options[index++] =
527533
Options.maxCommitDelay(connectionState.getValue(MAX_COMMIT_DELAY).getValue());
528534
}
535+
if (connectionState.getValue(DEFAULT_ISOLATION_LEVEL).getValue()
536+
!= IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED) {
537+
options[index++] =
538+
Options.isolationLevel(connectionState.getValue(DEFAULT_ISOLATION_LEVEL).getValue());
539+
}
529540
return dbClient.readWriteTransaction(options);
530541
}
531542

0 commit comments

Comments
 (0)