Skip to content

Commit 89414d7

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 89414d7

File tree

11 files changed

+304
-22
lines changed

11 files changed

+304
-22
lines changed

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

Lines changed: 25 additions & 0 deletions
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;
@@ -381,6 +382,30 @@ public DirectedReadOptions convert(String value) {
381382
return null;
382383
}
383384
}
385+
386+
/** Converter for converting strings to {@link com.google.spanner.v1.TransactionOptions.IsolationLevel} values. */
387+
static class IsolationLevelConverter
388+
implements ClientSideStatementValueConverter<TransactionOptions.IsolationLevel> {
389+
static final IsolationLevelConverter INSTANCE = new IsolationLevelConverter();
390+
391+
private final CaseInsensitiveEnumMap<TransactionOptions.IsolationLevel> values =
392+
new CaseInsensitiveEnumMap<>(TransactionOptions.IsolationLevel.class);
393+
394+
private IsolationLevelConverter() {}
395+
396+
/** Constructor needed for reflection. */
397+
public IsolationLevelConverter(String allowedValues) {}
398+
399+
@Override
400+
public Class<TransactionOptions.IsolationLevel> getParameterClass() {
401+
return TransactionOptions.IsolationLevel.class;
402+
}
403+
404+
@Override
405+
public TransactionOptions.IsolationLevel convert(String value) {
406+
return values.get(value);
407+
}
408+
}
384409

385410
/** Converter for converting strings to {@link AutocommitDmlMode} values. */
386411
static class AutocommitDmlModeConverter

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
@@ -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;
@@ -218,6 +219,12 @@ public interface Connection extends AutoCloseable {
218219

219220
/** @return <code>true</code> if this connection is in read-only mode */
220221
boolean isReadOnly();
222+
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();
221228

222229
/**
223230
* Sets the duration the connection should wait before automatically aborting the execution of a

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

Lines changed: 20 additions & 0 deletions
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,22 @@ 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(!isBatchActive(), "Cannot default isolation level while in a batch");
645+
ConnectionPreconditions.checkState(
646+
!isTransactionStarted(), "Cannot set default isolation level while a transaction is active");
647+
setConnectionPropertyValue(DEFAULT_ISOLATION_LEVEL, isolationLevel);
648+
clearLastTransactionAndSetDefaultTransactionOptions();
649+
}
650+
651+
@Override
652+
public IsolationLevel getDefaultIsolationLevel() {
653+
ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG);
654+
return getConnectionPropertyValue(DEFAULT_ISOLATION_LEVEL);
655+
}
656+
638657
private void clearLastTransactionAndSetDefaultTransactionOptions() {
639658
setDefaultTransactionOptions();
640659
this.currentUnitOfWork = null;
@@ -2196,6 +2215,7 @@ UnitOfWork createNewUnitOfWork(
21962215
.setUsesEmulator(options.usesEmulator())
21972216
.setUseAutoSavepointsForEmulator(options.useAutoSavepointsForEmulator())
21982217
.setDatabaseClient(dbClient)
2218+
.setIsolationLevel(getConnectionPropertyValue(DEFAULT_ISOLATION_LEVEL))
21992219
.setDelayTransactionStartUntilFirstWrite(
22002220
getConnectionPropertyValue(DELAY_TRANSACTION_START_UNTIL_FIRST_WRITE))
22012221
.setKeepTransactionAlive(getConnectionPropertyValue(KEEP_TRANSACTION_ALIVE))

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

Lines changed: 17 additions & 2 deletions
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,24 @@ 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[]{IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED, IsolationLevel.SERIALIZABLE, IsolationLevel.REPEATABLE_READ},
415+
IsolationLevelConverter.INSTANCE,
416+
Context.USER);
404417
static final ConnectionProperty<AutocommitDmlMode> AUTOCOMMIT_DML_MODE =
405418
create(
406419
"autocommit_dml_mode",
407420
"Determines the transaction type that is used to execute "
408421
+ "DML statements when the connection is in auto-commit mode.",
409422
AutocommitDmlMode.TRANSACTIONAL,
410-
AutocommitDmlMode.values(),
423+
// Add 'null' as a valid value.
424+
Arrays.copyOf(AutocommitDmlMode.values(), AutocommitDmlMode.values().length + 1),
411425
AutocommitDmlModeConverter.INSTANCE,
412426
Context.USER);
413427
static final ConnectionProperty<Boolean> RETRY_ABORTS_INTERNALLY =
@@ -523,7 +537,8 @@ public class ConnectionProperties {
523537
RPC_PRIORITY_NAME,
524538
"Sets the priority for all RPC invocations from this connection (HIGH/MEDIUM/LOW). The default is HIGH.",
525539
DEFAULT_RPC_PRIORITY,
526-
RpcPriority.values(),
540+
// Add 'null' as a valid value.
541+
Arrays.copyOf(RpcPriority.values(), RpcPriority.values().length + 1),
527542
RpcPriorityConverter.INSTANCE,
528543
Context.USER);
529544
static final ConnectionProperty<SavepointSupport> SAVEPOINT_SUPPORT =

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

Lines changed: 19 additions & 0 deletions
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()));
@@ -248,6 +251,22 @@ private <T> void internalSetValue(
248251
newValue.setValue(value, context);
249252
currentProperties.put(property.getKey(), newValue);
250253
}
254+
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()).noneMatch(validValue -> Objects.equals(validValue, value))) {
260+
throw invalidParamValueError(property, value);
261+
}
262+
}
263+
264+
/** Creates an exception for an invalid value for a connection property. */
265+
static <T> SpannerException invalidParamValueError(ConnectionProperty<T> property, T value) {
266+
return SpannerExceptionFactory.newSpannerException(
267+
ErrorCode.INVALID_ARGUMENT,
268+
String.format("invalid value \"%s\" for configuration property \"%s\"", value, property));
269+
}
251270

252271
/** Creates an exception for an unknown connection property. */
253272
static SpannerException unknownParamError(ConnectionProperty<?> property) {

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

Lines changed: 16 additions & 0 deletions
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

@@ -250,6 +253,11 @@ Builder setSavepointSupport(SavepointSupport savepointSupport) {
250253
this.savepointSupport = savepointSupport;
251254
return this;
252255
}
256+
257+
Builder setIsolationLevel(IsolationLevel isolationLevel) {
258+
this.isolationLevel = Preconditions.checkNotNull(isolationLevel);
259+
return this;
260+
}
253261

254262
@Override
255263
ReadWriteTransaction build() {
@@ -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

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

Lines changed: 8 additions & 0 deletions
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,9 @@ private TransactionRunner createWriteTransaction() {
508510
if (connectionState.getValue(MAX_COMMIT_DELAY).getValue() != null) {
509511
numOptions++;
510512
}
513+
if (connectionState.getValue(DEFAULT_ISOLATION_LEVEL).getValue() != IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED) {
514+
numOptions++;
515+
}
511516
if (numOptions == 0) {
512517
return dbClient.readWriteTransaction();
513518
}
@@ -526,6 +531,9 @@ private TransactionRunner createWriteTransaction() {
526531
options[index++] =
527532
Options.maxCommitDelay(connectionState.getValue(MAX_COMMIT_DELAY).getValue());
528533
}
534+
if (connectionState.getValue(DEFAULT_ISOLATION_LEVEL).getValue() != IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED) {
535+
options[index++] = Options.isolationLevel(connectionState.getValue(DEFAULT_ISOLATION_LEVEL).getValue());
536+
}
529537
return dbClient.readWriteTransaction(options);
530538
}
531539

0 commit comments

Comments
 (0)