From 23494f56bcecdf2b9a088c5761addc19a3dc6654 Mon Sep 17 00:00:00 2001 From: Shobhit Gupta Date: Mon, 3 Mar 2025 08:44:14 +0530 Subject: [PATCH 1/8] feat(spanner): Support REPEATABLE_READ for RW transaction --- .../google/cloud/spanner/DatabaseClient.java | 24 + .../com/google/cloud/spanner/Options.java | 78 ++- .../com/google/cloud/spanner/SessionImpl.java | 34 +- .../google/cloud/spanner/SpannerOptions.java | 58 ++ .../cloud/spanner/DatabaseClientImplTest.java | 19 + ...eClientImplWithTransactionOptionsTest.java | 497 ++++++++++++++++++ .../com/google/cloud/spanner/OptionsTest.java | 75 ++- .../cloud/spanner/SpannerOptionsTest.java | 32 ++ 8 files changed, 809 insertions(+), 8 deletions(-) create mode 100644 google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplWithTransactionOptionsTest.java diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClient.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClient.java index 06237131458..7e303b83f11 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClient.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClient.java @@ -111,6 +111,10 @@ default String getDatabaseRole() { * applied to any other requests on the transaction. *
  • {@link Options#commitStats()}: Request that the server includes commit statistics in the * {@link CommitResponse}. + *
  • {@link Options#repeatableReadIsolationLevel()}: Request Repeatable Read Isolation Level + * from the backend. + *
  • {@link Options#serializableIsolationLevel()}: Request Serializable Isolation Level from + * the backend. * * * @return a response with the timestamp at which the write was committed @@ -186,6 +190,10 @@ CommitResponse writeWithOptions(Iterable mutations, TransactionOption. * applied to any other requests on the transaction. *
  • {@link Options#commitStats()}: Request that the server includes commit statistics in the * {@link CommitResponse}. + *
  • {@link Options#repeatableReadIsolationLevel()}: Request Repeatable Read Isolation Level + * from the backend. + *
  • {@link Options#serializableIsolationLevel()}: Request Serializable Isolation Level from + * the backend. * * * @return a response with the timestamp at which the write was committed @@ -414,6 +422,10 @@ ServerStream batchWriteAtLeastOnce( * applied to any other requests on the transaction. *
  • {@link Options#commitStats()}: Request that the server includes commit statistics in the * {@link CommitResponse}. + *
  • {@link Options#repeatableReadIsolationLevel()}: Request Repeatable Read Isolation Level + * from the backend. + *
  • {@link Options#serializableIsolationLevel()}: Request Serializable Isolation Level from + * the backend. * */ TransactionRunner readWriteTransaction(TransactionOption... options); @@ -454,6 +466,10 @@ ServerStream batchWriteAtLeastOnce( * applied to any other requests on the transaction. *
  • {@link Options#commitStats()}: Request that the server includes commit statistics in the * {@link CommitResponse}. + *
  • {@link Options#repeatableReadIsolationLevel()}: Request Repeatable Read Isolation Level + * from the backend. + *
  • {@link Options#serializableIsolationLevel()}: Request Serializable Isolation Level from + * the backend. * */ TransactionManager transactionManager(TransactionOption... options); @@ -494,6 +510,10 @@ ServerStream batchWriteAtLeastOnce( * applied to any other requests on the transaction. *
  • {@link Options#commitStats()}: Request that the server includes commit statistics in the * {@link CommitResponse}. + *
  • {@link Options#repeatableReadIsolationLevel()}: Request Repeatable Read Isolation Level + * from the backend. + *
  • {@link Options#serializableIsolationLevel()}: Request Serializable Isolation Level from + * the backend. * */ AsyncRunner runAsync(TransactionOption... options); @@ -548,6 +568,10 @@ ServerStream batchWriteAtLeastOnce( * applied to any other requests on the transaction. *
  • {@link Options#commitStats()}: Request that the server includes commit statistics in the * {@link CommitResponse}. + *
  • {@link Options#repeatableReadIsolationLevel()}: Request Repeatable Read Isolation Level + * from the backend. + *
  • {@link Options#serializableIsolationLevel()}: Request Serializable Isolation Level from + * the backend. * */ AsyncTransactionManager transactionManagerAsync(TransactionOption... options); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Options.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Options.java index c8c588f813a..ad3e3347d6b 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Options.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Options.java @@ -21,9 +21,13 @@ import com.google.spanner.v1.ReadRequest.LockHint; import com.google.spanner.v1.ReadRequest.OrderBy; import com.google.spanner.v1.RequestOptions.Priority; +import com.google.spanner.v1.TransactionOptions.IsolationLevel; import java.io.Serializable; import java.time.Duration; +import java.util.Arrays; import java.util.Objects; +import java.util.function.Predicate; +import java.util.stream.Stream; /** Specifies options for various spanner operations */ public final class Options implements Serializable { @@ -131,7 +135,29 @@ public interface UpdateAdminApiOption extends AdminApiOption {} public interface QueryOption {} /** Marker interface to mark options applicable to write operations */ - public interface TransactionOption {} + public interface TransactionOption { + Predicate isValidIsolationLevelOption = + txnOption -> + txnOption instanceof RepeatableReadOption || txnOption instanceof SerializableOption; + + /** + * Combines two arrays of TransactionOption, with primaryOptions taking precedence in case of + * conflicts. Note that {@link + * com.google.cloud.spanner.SpannerOptions.Builder.TransactionOptions} supports only the {@link + * IsolationLevel} TransactionOption, meaning spannerOptions will contain a maximum of one + * TransactionOption. + */ + static TransactionOption[] combine( + TransactionOption[] primaryOptions, TransactionOption[] spannerOptions) { + if (spannerOptions == null + || Arrays.stream(primaryOptions).anyMatch(isValidIsolationLevelOption)) { + return primaryOptions; + } else { + return Stream.concat(Arrays.stream(primaryOptions), Arrays.stream(spannerOptions)) + .toArray(TransactionOption[]::new); + } + } + } /** Marker interface to mark options applicable to update operation. */ public interface UpdateOption {} @@ -159,6 +185,22 @@ public static TransactionOption optimisticLock() { return OPTIMISTIC_LOCK_OPTION; } + /** + * Specifying this instructs the transaction to request Repeatable Read Isolation Level from the + * backend. + */ + public static TransactionOption repeatableReadIsolationLevel() { + return REPEATABLE_READ_OPTION; + } + + /** + * Specifying this instructs the transaction to request Serializable Isolation Level from the + * backend. + */ + public static TransactionOption serializableIsolationLevel() { + return SERIALIZABLE_OPTION; + } + /** * Specifying this instructs the transaction to be excluded from being recorded in change streams * with the DDL option `allow_txn_exclusion=true`. This does not exclude the transaction from @@ -490,6 +532,26 @@ void appendToOptions(Options options) { } } + /** Option to request REPEATABLE READ isolation level for read/write transactions. */ + static final class RepeatableReadOption extends InternalOption implements TransactionOption { + @Override + void appendToOptions(Options options) { + options.isolationLevel = IsolationLevel.REPEATABLE_READ; + } + } + + static final RepeatableReadOption REPEATABLE_READ_OPTION = new RepeatableReadOption(); + + /** Option to request SERIALIZABLE isolation level for read/write transactions. */ + static final class SerializableOption extends InternalOption implements TransactionOption { + @Override + void appendToOptions(Options options) { + options.isolationLevel = IsolationLevel.SERIALIZABLE; + } + } + + static final SerializableOption SERIALIZABLE_OPTION = new SerializableOption(); + private boolean withCommitStats; private Duration maxCommitDelay; @@ -512,6 +574,7 @@ void appendToOptions(Options options) { private RpcOrderBy orderBy; private RpcLockHint lockHint; private Boolean lastStatement; + private IsolationLevel isolationLevel; // Construction is via factory methods below. private Options() {} @@ -664,6 +727,10 @@ LockHint lockHint() { return lockHint == null ? null : lockHint.proto; } + IsolationLevel isolationLevel() { + return isolationLevel; + } + @Override public String toString() { StringBuilder b = new StringBuilder(); @@ -726,6 +793,9 @@ public String toString() { if (lockHint != null) { b.append("lockHint: ").append(lockHint).append(' '); } + if (isolationLevel != null) { + b.append("isolationLevel: ").append(isolationLevel).append(' '); + } return b.toString(); } @@ -767,7 +837,8 @@ public boolean equals(Object o) { && Objects.equals(directedReadOptions(), that.directedReadOptions()) && Objects.equals(orderBy(), that.orderBy()) && Objects.equals(isLastStatement(), that.isLastStatement()) - && Objects.equals(lockHint(), that.lockHint()); + && Objects.equals(lockHint(), that.lockHint()) + && Objects.equals(isolationLevel(), that.isolationLevel()); } @Override @@ -833,6 +904,9 @@ public int hashCode() { if (lockHint != null) { result = 31 * result + lockHint.hashCode(); } + if (isolationLevel != null) { + result = 31 * result + isolationLevel.hashCode(); + } return result; } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionImpl.java index 454709275f8..1a4e2b00c6d 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionImpl.java @@ -83,6 +83,9 @@ static TransactionOptions createReadWriteTransactionOptions( && previousTransactionId != com.google.protobuf.ByteString.EMPTY) { readWrite.setMultiplexedSessionPreviousTransactionId(previousTransactionId); } + if (options.isolationLevel() != null) { + transactionOptions.setIsolationLevel(options.isolationLevel()); + } transactionOptions.setReadWrite(readWrite); return transactionOptions.build(); } @@ -239,7 +242,10 @@ public CommitResponse writeAtLeastOnceWithOptions( setActive(null); List mutationsProto = new ArrayList<>(); Mutation.toProtoAndReturnRandomMutation(mutations, mutationsProto); - Options options = Options.fromTransactionOptions(transactionOptions); + Options options = + Options.fromTransactionOptions( + TransactionOption.combine( + transactionOptions, this.spanner.getOptions().getTransactionOptions())); final CommitRequest.Builder requestBuilder = CommitRequest.newBuilder() .setSession(getName()) @@ -252,6 +258,9 @@ public CommitResponse writeAtLeastOnceWithOptions( if (options.withExcludeTxnFromChangeStreams() == Boolean.TRUE) { transactionOptionsBuilder.setExcludeTxnFromChangeStreams(true); } + if (options.isolationLevel() != null) { + transactionOptionsBuilder.setIsolationLevel(options.isolationLevel()); + } requestBuilder.setSingleUseTransaction(transactionOptionsBuilder); if (options.hasMaxCommitDelay()) { @@ -396,22 +405,37 @@ public ReadOnlyTransaction readOnlyTransaction(TimestampBound bound) { @Override public TransactionRunner readWriteTransaction(TransactionOption... options) { - return setActive(new TransactionRunnerImpl(this, options)); + return setActive( + new TransactionRunnerImpl( + this, + TransactionOption.combine(options, this.spanner.getOptions().getTransactionOptions()))); } @Override public AsyncRunner runAsync(TransactionOption... options) { - return new AsyncRunnerImpl(setActive(new TransactionRunnerImpl(this, options))); + return new AsyncRunnerImpl( + setActive( + new TransactionRunnerImpl( + this, + TransactionOption.combine( + options, this.spanner.getOptions().getTransactionOptions())))); } @Override public TransactionManager transactionManager(TransactionOption... options) { - return new TransactionManagerImpl(this, currentSpan, tracer, options); + return new TransactionManagerImpl( + this, + currentSpan, + tracer, + TransactionOption.combine(options, this.spanner.getOptions().getTransactionOptions())); } @Override public AsyncTransactionManagerImpl transactionManagerAsync(TransactionOption... options) { - return new AsyncTransactionManagerImpl(this, currentSpan, options); + return new AsyncTransactionManagerImpl( + this, + currentSpan, + TransactionOption.combine(options, this.spanner.getOptions().getTransactionOptions())); } @Override diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java index 412bbbb151c..3eda32ff69c 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java @@ -18,6 +18,7 @@ import static com.google.api.gax.util.TimeConversionUtils.toJavaTimeDuration; import static com.google.api.gax.util.TimeConversionUtils.toThreetenDuration; +import static com.google.cloud.spanner.Options.TransactionOption.isValidIsolationLevelOption; import com.google.api.core.ApiFunction; import com.google.api.core.BetaApi; @@ -31,8 +32,10 @@ import com.google.api.gax.retrying.RetrySettings; import com.google.api.gax.rpc.ApiCallContext; import com.google.api.gax.rpc.TransportChannelProvider; +import com.google.api.gax.tracing.ApiTracer; import com.google.api.gax.tracing.ApiTracerFactory; import com.google.api.gax.tracing.BaseApiTracerFactory; +import com.google.api.gax.tracing.MetricsTracer; import com.google.api.gax.tracing.OpencensusTracerFactory; import com.google.auth.oauth2.AccessToken; import com.google.auth.oauth2.GoogleCredentials; @@ -45,6 +48,7 @@ import com.google.cloud.grpc.GrpcTransportOptions; import com.google.cloud.spanner.Options.DirectedReadOption; import com.google.cloud.spanner.Options.QueryOption; +import com.google.cloud.spanner.Options.TransactionOption; import com.google.cloud.spanner.Options.UpdateOption; import com.google.cloud.spanner.admin.database.v1.DatabaseAdminSettings; import com.google.cloud.spanner.admin.database.v1.stub.DatabaseAdminStubSettings; @@ -178,6 +182,7 @@ public class SpannerOptions extends ServiceOptions { private final boolean enableExtendedTracing; private final boolean enableEndToEndTracing; private final String monitoringHost; + private final TransactionOption[] transactionOptions; enum TracingFramework { OPEN_CENSUS, @@ -807,6 +812,7 @@ protected SpannerOptions(Builder builder) { enableBuiltInMetrics = builder.enableBuiltInMetrics; enableEndToEndTracing = builder.enableEndToEndTracing; monitoringHost = builder.monitoringHost; + transactionOptions = builder.transactionOptions; } /** @@ -988,6 +994,7 @@ public static class Builder private String monitoringHost = SpannerOptions.environment.getMonitoringHost(); private SslContext mTLSContext = null; private boolean isExperimentalHost = false; + private TransactionOption[] transactionOptions; private static String createCustomClientLibToken(String token) { return token + " " + ServiceOptions.getGoogApiClientLibName(); @@ -1056,6 +1063,7 @@ protected Builder() { this.enableBuiltInMetrics = options.enableBuiltInMetrics; this.enableEndToEndTracing = options.enableEndToEndTracing; this.monitoringHost = options.monitoringHost; + this.transactionOptions = options.transactionOptions; } @Override @@ -1645,6 +1653,52 @@ public Builder setEnableEndToEndTracing(boolean enableEndToEndTracing) { return this; } + public static class TransactionOptions { + private final List transactionOptions = new ArrayList<>(); + + private TransactionOptions() {} + + TransactionOption[] getTransactionOptions() { + return transactionOptions.toArray(new TransactionOption[0]); + } + + public static class TransactionOptionsBuilder { + + private final List transactionOptions = new ArrayList<>(); + private static final String INVALID_ISOLATION_LEVEL_MESSAGE = + "Either repeatable read or serializable isolation level is allowed"; + + public static TransactionOptionsBuilder newBuilder() { + return new TransactionOptionsBuilder(); + } + + public TransactionOptionsBuilder setIsolationLevel(TransactionOption transactionOption) { + if (!isValidIsolationLevelOption.test(transactionOption)) { + throw SpannerExceptionFactory.newSpannerException( + ErrorCode.INVALID_ARGUMENT, INVALID_ISOLATION_LEVEL_MESSAGE); + } + transactionOptions.removeIf(isValidIsolationLevelOption); + transactionOptions.add(transactionOption); + return this; + } + + public TransactionOptions build() { + TransactionOptions options = new TransactionOptions(); + options.transactionOptions.addAll(this.transactionOptions); + return options; + } + } + } + + /** + * Sets the default transaction options. Only Isolation level option is supported via + * TransactionOptions. + */ + public Builder setDefaultTransactionOptions(TransactionOptions transactionOptions) { + this.transactionOptions = transactionOptions.getTransactionOptions(); + return this; + } + @SuppressWarnings("rawtypes") @Override public SpannerOptions build() { @@ -1990,6 +2044,10 @@ String getMonitoringHost() { return monitoringHost; } + TransactionOption[] getTransactionOptions() { + return transactionOptions; + } + @BetaApi public boolean isUseVirtualThreads() { return useVirtualThreads; diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java index d502e9b0d5d..3918781076e 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java @@ -98,6 +98,7 @@ import com.google.spanner.v1.ResultSetStats; import com.google.spanner.v1.StructType; import com.google.spanner.v1.StructType.Field; +import com.google.spanner.v1.TransactionOptions.IsolationLevel; import com.google.spanner.v1.Type; import com.google.spanner.v1.TypeAnnotationCode; import com.google.spanner.v1.TypeCode; @@ -1928,6 +1929,9 @@ public void testReadWriteExecuteReadWithTag() { .isEqualTo("app=spanner,env=test,action=read"); assertThat(request.getRequestOptions().getTransactionTag()) .isEqualTo("app=spanner,env=test,action=txn"); + assertEquals( + IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED, + request.getTransaction().getBegin().getIsolationLevel()); } @Test @@ -1950,6 +1954,9 @@ public void testExecuteUpdateWithTag() { assertNotNull(request.getTransaction().getBegin()); assertTrue(request.getTransaction().getBegin().hasReadWrite()); assertFalse(request.getTransaction().getBegin().getExcludeTxnFromChangeStreams()); + assertEquals( + IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED, + request.getTransaction().getBegin().getIsolationLevel()); } @Test @@ -1976,6 +1983,9 @@ public void testBatchUpdateWithTag() { assertNotNull(request.getTransaction().getBegin()); assertTrue(request.getTransaction().getBegin().hasReadWrite()); assertFalse(request.getTransaction().getBegin().getExcludeTxnFromChangeStreams()); + assertEquals( + IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED, + request.getTransaction().getBegin().getIsolationLevel()); } @Test @@ -2049,6 +2059,9 @@ public void testTransactionManagerCommitWithTag() { assertNotNull(beginTransaction.getOptions()); assertTrue(beginTransaction.getOptions().hasReadWrite()); assertFalse(beginTransaction.getOptions().getExcludeTxnFromChangeStreams()); + assertEquals( + IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED, + beginTransaction.getOptions().getIsolationLevel()); List requests = mockSpanner.getRequestsOfType(CommitRequest.class); assertThat(requests).hasSize(1); @@ -2079,6 +2092,9 @@ public void testAsyncRunnerCommitWithTag() { assertNotNull(beginTransaction.getOptions()); assertTrue(beginTransaction.getOptions().hasReadWrite()); assertFalse(beginTransaction.getOptions().getExcludeTxnFromChangeStreams()); + assertEquals( + IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED, + beginTransaction.getOptions().getIsolationLevel()); List requests = mockSpanner.getRequestsOfType(CommitRequest.class); assertThat(requests).hasSize(1); @@ -2114,6 +2130,9 @@ public void testAsyncTransactionManagerCommitWithTag() { assertNotNull(beginTransaction.getOptions()); assertTrue(beginTransaction.getOptions().hasReadWrite()); assertFalse(beginTransaction.getOptions().getExcludeTxnFromChangeStreams()); + assertEquals( + IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED, + beginTransaction.getOptions().getIsolationLevel()); List requests = mockSpanner.getRequestsOfType(CommitRequest.class); assertThat(requests).hasSize(1); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplWithTransactionOptionsTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplWithTransactionOptionsTest.java new file mode 100644 index 00000000000..174d80dfc73 --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplWithTransactionOptionsTest.java @@ -0,0 +1,497 @@ +package com.google.cloud.spanner; + +import static com.google.cloud.spanner.SpannerApiFutures.get; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; + +import com.google.api.core.ApiFutures; +import com.google.api.gax.grpc.testing.LocalChannelProvider; +import com.google.cloud.NoCredentials; +import com.google.cloud.Timestamp; +import com.google.cloud.spanner.AsyncTransactionManager.TransactionContextFuture; +import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult; +import com.google.cloud.spanner.Options.RpcPriority; +import com.google.cloud.spanner.SpannerOptions.Builder.TransactionOptions.TransactionOptionsBuilder; +import com.google.protobuf.AbstractMessage; +import com.google.protobuf.ListValue; +import com.google.spanner.v1.BeginTransactionRequest; +import com.google.spanner.v1.CommitRequest; +import com.google.spanner.v1.ExecuteBatchDmlRequest; +import com.google.spanner.v1.ExecuteSqlRequest; +import com.google.spanner.v1.ReadRequest; +import com.google.spanner.v1.ResultSetMetadata; +import com.google.spanner.v1.StructType; +import com.google.spanner.v1.StructType.Field; +import com.google.spanner.v1.TransactionOptions.IsolationLevel; +import com.google.spanner.v1.TypeCode; +import io.grpc.Server; +import io.grpc.Status; +import io.grpc.inprocess.InProcessServerBuilder; +import java.io.IOException; +import java.util.Collections; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class DatabaseClientImplWithTransactionOptionsTest { + + private static MockSpannerServiceImpl mockSpanner; + private static Server server; + private static ExecutorService executor; + private static LocalChannelProvider channelProvider; + private static final Statement UPDATE_STATEMENT = + Statement.of("UPDATE FOO SET BAR=1 WHERE BAZ=2"); + private static final Statement INVALID_UPDATE_STATEMENT = + Statement.of("UPDATE NON_EXISTENT_TABLE SET BAR=1 WHERE BAZ=2"); + private static final Statement INVALID_SELECT_STATEMENT = + Statement.of("SELECT * FROM NON_EXISTENT_TABLE"); + private static final long UPDATE_COUNT = 1L; + private static final Statement SELECT1 = Statement.of("SELECT 1 AS COL1"); + private static final ResultSetMetadata SELECT1_METADATA = + ResultSetMetadata.newBuilder() + .setRowType( + StructType.newBuilder() + .addFields( + Field.newBuilder() + .setName("COL1") + .setType( + com.google.spanner.v1.Type.newBuilder() + .setCode(TypeCode.INT64) + .build()) + .build()) + .build()) + .build(); + private static final com.google.spanner.v1.ResultSet SELECT1_RESULTSET = + com.google.spanner.v1.ResultSet.newBuilder() + .addRows( + ListValue.newBuilder() + .addValues(com.google.protobuf.Value.newBuilder().setStringValue("1").build()) + .build()) + .setMetadata(SELECT1_METADATA) + .build(); + private Spanner spanner; + private Spanner spannerWithRepeatableReadOption; + private Spanner spannerWithSerializableOption; + private DatabaseClient client; + private DatabaseClient clientWithRepeatableReadOption; + private DatabaseClient clientWithSerializableOption; + + @BeforeClass + public static void startStaticServer() throws IOException { + mockSpanner = new MockSpannerServiceImpl(); + mockSpanner.setAbortProbability(0.0D); // We don't want any unpredictable aborted transactions. + mockSpanner.putStatementResult(StatementResult.update(UPDATE_STATEMENT, UPDATE_COUNT)); + mockSpanner.putStatementResult(StatementResult.query(SELECT1, SELECT1_RESULTSET)); + mockSpanner.putStatementResult( + StatementResult.exception( + INVALID_UPDATE_STATEMENT, + Status.INVALID_ARGUMENT.withDescription("invalid statement").asRuntimeException())); + mockSpanner.putStatementResult( + StatementResult.exception( + INVALID_SELECT_STATEMENT, + Status.INVALID_ARGUMENT.withDescription("invalid statement").asRuntimeException())); + mockSpanner.putStatementResult( + StatementResult.read( + "FOO", KeySet.all(), Collections.singletonList("ID"), SELECT1_RESULTSET)); + + String uniqueName = InProcessServerBuilder.generateName(); + executor = Executors.newSingleThreadExecutor(); + server = + InProcessServerBuilder.forName(uniqueName) + // We need to use a real executor for timeouts to occur. + .scheduledExecutorService(new ScheduledThreadPoolExecutor(1)) + .addService(mockSpanner) + .build() + .start(); + channelProvider = LocalChannelProvider.create(uniqueName); + } + + @AfterClass + public static void stopServer() throws InterruptedException { + server.shutdown(); + server.awaitTermination(); + } + + @Before + public void setUp() { + mockSpanner.reset(); + mockSpanner.removeAllExecutionTimes(); + SpannerOptions.Builder spannerOptionsBuilder = + SpannerOptions.newBuilder() + .setProjectId("[PROJECT]") + .setChannelProvider(channelProvider) + .setCredentials(NoCredentials.getInstance()); + spanner = spannerOptionsBuilder.build().getService(); + spannerWithRepeatableReadOption = + spannerOptionsBuilder + .setDefaultTransactionOptions( + TransactionOptionsBuilder.newBuilder() + .setIsolationLevel(Options.repeatableReadIsolationLevel()) + .build()) + .build() + .getService(); + spannerWithSerializableOption = + spannerOptionsBuilder + .setDefaultTransactionOptions( + TransactionOptionsBuilder.newBuilder() + .setIsolationLevel(Options.serializableIsolationLevel()) + .build()) + .build() + .getService(); + client = spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + clientWithRepeatableReadOption = + spannerWithRepeatableReadOption.getDatabaseClient( + DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + clientWithSerializableOption = + spannerWithSerializableOption.getDatabaseClient( + DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + } + + @After + public void tearDown() { + spanner.close(); + spannerWithRepeatableReadOption.close(); + spannerWithSerializableOption.close(); + } + + @Test + public void testWrite_WithNoIsolationLevel() { + Timestamp timestamp = + client.write( + Collections.singletonList( + Mutation.newInsertBuilder("FOO").set("ID").to(1L).set("NAME").to("Bar").build())); + assertNotNull(timestamp); + validateIsolationLevel(IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED); + } + + @Test + public void testWrite_WithRRSpannerOptions() { + Timestamp timestamp = + clientWithRepeatableReadOption.write( + Collections.singletonList( + Mutation.newInsertBuilder("FOO").set("ID").to(1L).set("NAME").to("Bar").build())); + assertNotNull(timestamp); + validateIsolationLevel(IsolationLevel.REPEATABLE_READ); + } + + @Test + public void testWriteWithOptions_WithRRSpannerOptions() { + clientWithRepeatableReadOption.writeWithOptions( + Collections.singletonList( + Mutation.newInsertBuilder("FOO").set("ID").to(1L).set("NAME").to("Bar").build()), + Options.priority(RpcPriority.HIGH)); + validateIsolationLevel(IsolationLevel.REPEATABLE_READ); + } + + @Test + public void testWriteWithOptions_WithSerializableTxnOption() { + clientWithRepeatableReadOption.writeWithOptions( + Collections.singletonList( + Mutation.newInsertBuilder("FOO").set("ID").to(1L).set("NAME").to("Bar").build()), + Options.serializableIsolationLevel()); + validateIsolationLevel(IsolationLevel.SERIALIZABLE); + } + + @Test + public void testWriteAtLeastOnce_WithSerializableSpannerOptions() { + Timestamp timestamp = + clientWithSerializableOption.writeAtLeastOnce( + Collections.singletonList( + Mutation.newInsertBuilder("FOO").set("ID").to(1L).set("NAME").to("Bar").build())); + assertNotNull(timestamp); + validateIsolationLevel(IsolationLevel.SERIALIZABLE); + } + + @Test + public void testWriteAtLeastOnceWithOptions_WithRRTxnOption() { + clientWithSerializableOption.writeAtLeastOnceWithOptions( + Collections.singletonList( + Mutation.newInsertBuilder("FOO").set("ID").to(1L).set("NAME").to("Bar").build()), + Options.repeatableReadIsolationLevel()); + validateIsolationLevel(IsolationLevel.REPEATABLE_READ); + } + + @Test + public void testReadWriteTxn_WithRRSpannerOption_batchUpdate() { + TransactionRunner runner = clientWithRepeatableReadOption.readWriteTransaction(); + runner.run(transaction -> transaction.batchUpdate(Collections.singletonList(UPDATE_STATEMENT))); + validateIsolationLevel(IsolationLevel.REPEATABLE_READ); + } + + @Test + public void testReadWriteTxn_WithSerializableTxnOption_batchUpdate() { + TransactionRunner runner = + clientWithRepeatableReadOption.readWriteTransaction(Options.serializableIsolationLevel()); + runner.run(transaction -> transaction.batchUpdate(Collections.singletonList(UPDATE_STATEMENT))); + validateIsolationLevel(IsolationLevel.SERIALIZABLE); + } + + @Test + public void testPartitionedDML_WithRRSpannerOption() { + clientWithRepeatableReadOption.executePartitionedUpdate(UPDATE_STATEMENT); + validateIsolationLevel(IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED); + } + + @Test + public void testCommit_WithSerializableTxnOption() { + TransactionRunner runner = client.readWriteTransaction(Options.serializableIsolationLevel()); + runner.run( + transaction -> { + transaction.buffer(Mutation.delete("TEST", KeySet.all())); + return null; + }); + + validateIsolationLevel(IsolationLevel.SERIALIZABLE); + } + + @Test + public void testTransactionManagerCommit_WithRRTxnOption() { + try (TransactionManager manager = + clientWithSerializableOption.transactionManager(Options.repeatableReadIsolationLevel())) { + TransactionContext transaction = manager.begin(); + transaction.buffer(Mutation.delete("TEST", KeySet.all())); + manager.commit(); + } + + validateIsolationLevel(IsolationLevel.REPEATABLE_READ); + } + + @Test + public void testAsyncRunnerCommit_WithRRSpannerOption() { + AsyncRunner runner = clientWithRepeatableReadOption.runAsync(); + get( + runner.runAsync( + txn -> { + txn.buffer(Mutation.delete("TEST", KeySet.all())); + return ApiFutures.immediateFuture(null); + }, + executor)); + + validateIsolationLevel(IsolationLevel.REPEATABLE_READ); + } + + @Test + public void testAsyncTransactionManagerCommit_WithSerializableTxnOption() { + try (AsyncTransactionManager manager = + clientWithRepeatableReadOption.transactionManagerAsync( + Options.serializableIsolationLevel())) { + TransactionContextFuture transaction = manager.beginAsync(); + get( + transaction + .then( + (txn, input) -> { + txn.buffer(Mutation.delete("TEST", KeySet.all())); + return ApiFutures.immediateFuture(null); + }, + executor) + .commitAsync()); + } + + validateIsolationLevel(IsolationLevel.SERIALIZABLE); + } + + @Test + public void testReadWriteTxn_WithNoOptions() { + client + .readWriteTransaction() + .run( + transaction -> { + try (ResultSet rs = transaction.executeQuery(SELECT1)) { + while (rs.next()) { + assertEquals(rs.getLong(0), 1); + } + } + return null; + }); + validateIsolationLevel(IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED); + } + + @Test + public void executeSqlWithRWTransactionOptions_RepeatableRead() { + client + .readWriteTransaction(Options.repeatableReadIsolationLevel()) + .run( + transaction -> { + try (ResultSet rs = transaction.executeQuery(SELECT1)) { + while (rs.next()) { + assertEquals(rs.getLong(0), 1); + } + } + return null; + }); + validateIsolationLevel(IsolationLevel.REPEATABLE_READ); + } + + @Test + public void + executeSqlWithDefaultSpannerOptions_SerializableAndRWTransactionOptions_RepeatableRead() { + clientWithSerializableOption + .readWriteTransaction(Options.repeatableReadIsolationLevel()) + .run( + transaction -> { + try (ResultSet rs = transaction.executeQuery(SELECT1)) { + while (rs.next()) { + assertEquals(rs.getLong(0), 1); + } + } + return null; + }); + validateIsolationLevel(IsolationLevel.REPEATABLE_READ); + } + + @Test + public void + executeSqlWithDefaultSpannerOptions_RepeatableReadAndRWTransactionOptions_Serializable() { + clientWithRepeatableReadOption + .readWriteTransaction(Options.serializableIsolationLevel()) + .run( + transaction -> { + try (ResultSet rs = transaction.executeQuery(SELECT1)) { + while (rs.next()) { + assertEquals(rs.getLong(0), 1); + } + } + return null; + }); + validateIsolationLevel(IsolationLevel.SERIALIZABLE); + } + + @Test + public void executeSqlWithDefaultSpannerOptions_RepeatableReadAndNoRWTransactionOptions() { + clientWithRepeatableReadOption + .readWriteTransaction() + .run( + transaction -> { + try (ResultSet rs = transaction.executeQuery(SELECT1)) { + while (rs.next()) { + assertEquals(rs.getLong(0), 1); + } + } + return null; + }); + validateIsolationLevel(IsolationLevel.REPEATABLE_READ); + } + + @Test + public void executeSqlWithRWTransactionOptions_Serializable() { + client + .readWriteTransaction(Options.serializableIsolationLevel()) + .run( + transaction -> { + try (ResultSet rs = transaction.executeQuery(SELECT1)) { + while (rs.next()) { + assertEquals(rs.getLong(0), 1); + } + } + return null; + }); + validateIsolationLevel(IsolationLevel.SERIALIZABLE); + } + + @Test + public void readWithRWTransactionOptions_RepeatableRead() { + client + .readWriteTransaction(Options.repeatableReadIsolationLevel()) + .run( + transaction -> { + try (ResultSet rs = + transaction.read("FOO", KeySet.all(), Collections.singletonList("ID"))) { + while (rs.next()) { + assertEquals(rs.getLong(0), 1); + } + } + return null; + }); + validateIsolationLevel(IsolationLevel.REPEATABLE_READ); + } + + @Test + public void readWithRWTransactionOptions_Serializable() { + client + .readWriteTransaction(Options.serializableIsolationLevel()) + .run( + transaction -> { + try (ResultSet rs = + transaction.read("FOO", KeySet.all(), Collections.singletonList("ID"))) { + while (rs.next()) { + assertEquals(rs.getLong(0), 1); + } + } + return null; + }); + validateIsolationLevel(IsolationLevel.SERIALIZABLE); + } + + @Test + public void beginTransactionWithRWTransactionOptions_RepeatableRead() { + client + .readWriteTransaction(Options.repeatableReadIsolationLevel()) + .run( + transaction -> { + try (ResultSet rs = transaction.executeQuery(INVALID_SELECT_STATEMENT)) { + SpannerException e = assertThrows(SpannerException.class, () -> rs.next()); + assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode()); + } + return transaction.executeUpdate(UPDATE_STATEMENT); + }); + validateIsolationLevel(IsolationLevel.REPEATABLE_READ); + } + + @Test + public void beginTransactionWithRWTransactionOptions_Serializable() { + client + .readWriteTransaction(Options.serializableIsolationLevel()) + .run( + transaction -> { + try (ResultSet rs = transaction.executeQuery(INVALID_SELECT_STATEMENT)) { + SpannerException e = assertThrows(SpannerException.class, () -> rs.next()); + assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode()); + } + return transaction.executeUpdate(UPDATE_STATEMENT); + }); + validateIsolationLevel(IsolationLevel.SERIALIZABLE); + } + + private void validateIsolationLevel(IsolationLevel isolationLevel) { + boolean foundMatchingRequest = false; + for (AbstractMessage request : mockSpanner.getRequests()) { + if (request instanceof ExecuteSqlRequest) { + foundMatchingRequest = true; + assertEquals( + ((ExecuteSqlRequest) request).getTransaction().getBegin().getIsolationLevel(), + isolationLevel); + } else if (request instanceof BeginTransactionRequest) { + foundMatchingRequest = true; + assertEquals( + ((BeginTransactionRequest) request).getOptions().getIsolationLevel(), isolationLevel); + } else if (request instanceof ReadRequest) { + foundMatchingRequest = true; + assertEquals( + ((ReadRequest) request).getTransaction().getBegin().getIsolationLevel(), + isolationLevel); + } else if (request instanceof CommitRequest) { + foundMatchingRequest = true; + assertEquals( + ((CommitRequest) request).getSingleUseTransaction().getIsolationLevel(), + isolationLevel); + } else if (request instanceof ExecuteBatchDmlRequest) { + foundMatchingRequest = true; + assertEquals( + ((ExecuteBatchDmlRequest) request).getTransaction().getBegin().getIsolationLevel(), + isolationLevel); + } + if (foundMatchingRequest) { + break; + } + } + } +} diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/OptionsTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/OptionsTest.java index 17c25558f3b..52c771e330e 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/OptionsTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/OptionsTest.java @@ -17,6 +17,7 @@ package com.google.cloud.spanner; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; @@ -28,12 +29,14 @@ import com.google.cloud.spanner.Options.RpcLockHint; import com.google.cloud.spanner.Options.RpcOrderBy; import com.google.cloud.spanner.Options.RpcPriority; +import com.google.cloud.spanner.Options.TransactionOption; import com.google.spanner.v1.DirectedReadOptions; import com.google.spanner.v1.DirectedReadOptions.IncludeReplicas; import com.google.spanner.v1.DirectedReadOptions.ReplicaSelection; import com.google.spanner.v1.ReadRequest.LockHint; import com.google.spanner.v1.ReadRequest.OrderBy; import com.google.spanner.v1.RequestOptions.Priority; +import com.google.spanner.v1.TransactionOptions.IsolationLevel; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -117,7 +120,7 @@ public void allOptionsAbsent() { assertThat(options.equals(options)).isTrue(); assertThat(options.equals(null)).isFalse(); assertThat(options.equals(this)).isFalse(); - + assertNull(options.isolationLevel()); assertThat(options.hashCode()).isEqualTo(31); } @@ -375,6 +378,14 @@ public void testTransactionOptionsPriority() { assertEquals("priority: " + priority + " ", options.toString()); } + @Test + public void testTransactionOptionsIsolationLevel() { + Options options = Options.fromTransactionOptions(Options.repeatableReadIsolationLevel()); + assertEquals(options.isolationLevel(), IsolationLevel.REPEATABLE_READ); + assertEquals( + "isolationLevel: " + IsolationLevel.REPEATABLE_READ.name() + " ", options.toString()); + } + @Test public void testReadOptionsOrderBy() { RpcOrderBy orderBy = RpcOrderBy.NO_ORDER; @@ -772,6 +783,26 @@ public void transactionOptionsExcludeTxnFromChangeStreams() { assertThat(option3.toString()).doesNotContain("withExcludeTxnFromChangeStreams: true"); } + @Test + public void transactionOptionsIsolationLevel() { + Options option1 = Options.fromTransactionOptions(Options.repeatableReadIsolationLevel()); + Options option2 = Options.fromTransactionOptions(Options.repeatableReadIsolationLevel()); + Options option3 = Options.fromTransactionOptions(); + + assertEquals(option1, option2); + assertEquals(option1.hashCode(), option2.hashCode()); + assertNotEquals(option1, option3); + assertNotEquals(option1.hashCode(), option3.hashCode()); + + assertEquals(option1.isolationLevel(), IsolationLevel.REPEATABLE_READ); + assertThat(option1.toString()) + .contains("isolationLevel: " + IsolationLevel.REPEATABLE_READ.name()); + + assertNull(option3.isolationLevel()); + assertThat(option3.toString()) + .doesNotContain("isolationLevel: " + IsolationLevel.REPEATABLE_READ.name()); + } + @Test public void updateOptionsExcludeTxnFromChangeStreams() { Options option1 = Options.fromUpdateOptions(Options.excludeTxnFromChangeStreams()); @@ -807,4 +838,46 @@ public void testLastStatement() { assertNull(option3.isLastStatement()); assertThat(option3.toString()).doesNotContain("lastStatement: true"); } + + @Test + public void testTransactionOptionCombineMutuallyExclusiveOptions() { + TransactionOption priorityOption = Options.priority(RpcPriority.HIGH); + TransactionOption[] primaryOptions = {Options.commitStats(), priorityOption}; + TransactionOption[] spannerOptions = {Options.repeatableReadIsolationLevel()}; + assertArrayEquals( + TransactionOption.combine(primaryOptions, spannerOptions), + new TransactionOption[] { + Options.commitStats(), priorityOption, Options.repeatableReadIsolationLevel() + }); + } + + @Test + public void testTransactionOptionCombine_PrimaryOptionWithIsolationLevel() { + TransactionOption[] primaryOptions = { + Options.commitStats(), Options.serializableIsolationLevel() + }; + TransactionOption[] spannerOptions = {Options.repeatableReadIsolationLevel()}; + assertArrayEquals( + TransactionOption.combine(primaryOptions, spannerOptions), + new TransactionOption[] {Options.commitStats(), Options.serializableIsolationLevel()}); + } + + @Test + public void testTransactionOptionCombine_WithNoSpannerOptions() { + TransactionOption[] primaryOptions = { + Options.commitStats(), Options.serializableIsolationLevel() + }; + assertArrayEquals( + TransactionOption.combine(primaryOptions, null), + new TransactionOption[] {Options.commitStats(), Options.serializableIsolationLevel()}); + } + + @Test + public void testOptions_WithMultipleDifferentIsolationLevels() { + TransactionOption[] transactionOptions = { + Options.repeatableReadIsolationLevel(), Options.serializableIsolationLevel() + }; + Options options = Options.fromTransactionOptions(transactionOptions); + assertEquals(options.isolationLevel(), IsolationLevel.SERIALIZABLE); + } } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerOptionsTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerOptionsTest.java index 285097d4af7..6e8eab467cd 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerOptionsTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerOptionsTest.java @@ -37,6 +37,9 @@ import com.google.cloud.NoCredentials; import com.google.cloud.ServiceOptions; import com.google.cloud.TransportOptions; +import com.google.cloud.spanner.Options.TransactionOption; +import com.google.cloud.spanner.SpannerOptions.Builder.TransactionOptions; +import com.google.cloud.spanner.SpannerOptions.Builder.TransactionOptions.TransactionOptionsBuilder; import com.google.cloud.spanner.SpannerOptions.FixedCloseableExecutorProvider; import com.google.cloud.spanner.SpannerOptions.SpannerCallContextTimeoutConfigurator; import com.google.cloud.spanner.admin.database.v1.stub.DatabaseAdminStubSettings; @@ -767,6 +770,35 @@ public void testMonitoringHost() { .isEqualTo(metricsEndpoint); } + @Test + public void testTransactionOptions() { + TransactionOptions transactionOptions = + TransactionOptionsBuilder.newBuilder() + .setIsolationLevel(Options.serializableIsolationLevel()) + .build(); + assertNull(SpannerOptions.newBuilder().setProjectId("p").build().getTransactionOptions()); + assertThat( + SpannerOptions.newBuilder() + .setProjectId("p") + .setDefaultTransactionOptions(transactionOptions) + .build() + .getTransactionOptions()) + .isEqualTo(new TransactionOption[] {Options.serializableIsolationLevel()}); + } + + @Test + public void testTransactionOptionsWithError() { + assertNull(SpannerOptions.newBuilder().setProjectId("p").build().getTransactionOptions()); + SpannerException e = + assertThrows( + SpannerException.class, + () -> + TransactionOptionsBuilder.newBuilder() + .setIsolationLevel(Options.commitStats()) + .build()); + assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode()); + } + @Test public void testSetDirectedReadOptions() { final DirectedReadOptions directedReadOptions = From cd8ee256bb9a42843eebcf3cdf4720c53dc5aa2b Mon Sep 17 00:00:00 2001 From: Shobhit Gupta Date: Tue, 11 Mar 2025 19:03:07 +0530 Subject: [PATCH 2/8] feat: addressed review comments --- .../cloud/spanner/AbstractReadContext.java | 4 +- .../google/cloud/spanner/DatabaseClient.java | 33 +- .../com/google/cloud/spanner/Options.java | 62 +-- .../com/google/cloud/spanner/SessionImpl.java | 65 ++- .../google/cloud/spanner/SpannerOptions.java | 49 +-- .../cloud/spanner/TransactionRunnerImpl.java | 8 +- .../cloud/spanner/DatabaseClientImplTest.java | 222 ++-------- ...eClientImplWithTransactionOptionsTest.java | 389 ++++++------------ .../spanner/InlineBeginTransactionTest.java | 141 +------ .../cloud/spanner/MockSpannerTestActions.java | 158 +++++++ .../cloud/spanner/MockSpannerTestUtil.java | 3 +- ...edSessionDatabaseClientMockServerTest.java | 56 +-- .../com/google/cloud/spanner/OptionsTest.java | 64 ++- ...adWriteTransactionWithInlineBeginTest.java | 68 +-- .../google/cloud/spanner/SessionPoolTest.java | 3 + .../cloud/spanner/SpannerOptionsTest.java | 24 +- 16 files changed, 470 insertions(+), 879 deletions(-) create mode 100644 google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerTestActions.java diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractReadContext.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractReadContext.java index 145ad67f827..67b0638f5d9 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractReadContext.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractReadContext.java @@ -641,8 +641,8 @@ private ResultSet executeQueryInternal( *
  • Specific {@link QueryOptions} passed in for this query. *
  • Any value specified in a valid environment variable when the {@link SpannerOptions} * instance was created. - *
  • The default {@link SpannerOptions#getDefaultQueryOptions()} specified for the database - * where the query is executed. + *
  • The default {@link SpannerOptions#getDefaultQueryOptions(DatabaseId)} ()} specified for + * the database where the query is executed. * */ @VisibleForTesting diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClient.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClient.java index 7e303b83f11..df6708ebcd2 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClient.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClient.java @@ -22,6 +22,7 @@ import com.google.cloud.spanner.Options.TransactionOption; import com.google.cloud.spanner.Options.UpdateOption; import com.google.spanner.v1.BatchWriteResponse; +import com.google.spanner.v1.TransactionOptions.IsolationLevel; /** * Interface for all the APIs that are used to read/write data into a Cloud Spanner database. An @@ -111,10 +112,6 @@ default String getDatabaseRole() { * applied to any other requests on the transaction. *
  • {@link Options#commitStats()}: Request that the server includes commit statistics in the * {@link CommitResponse}. - *
  • {@link Options#repeatableReadIsolationLevel()}: Request Repeatable Read Isolation Level - * from the backend. - *
  • {@link Options#serializableIsolationLevel()}: Request Serializable Isolation Level from - * the backend. * * * @return a response with the timestamp at which the write was committed @@ -190,10 +187,6 @@ CommitResponse writeWithOptions(Iterable mutations, TransactionOption. * applied to any other requests on the transaction. *
  • {@link Options#commitStats()}: Request that the server includes commit statistics in the * {@link CommitResponse}. - *
  • {@link Options#repeatableReadIsolationLevel()}: Request Repeatable Read Isolation Level - * from the backend. - *
  • {@link Options#serializableIsolationLevel()}: Request Serializable Isolation Level from - * the backend. * * * @return a response with the timestamp at which the write was committed @@ -422,10 +415,8 @@ ServerStream batchWriteAtLeastOnce( * applied to any other requests on the transaction. *
  • {@link Options#commitStats()}: Request that the server includes commit statistics in the * {@link CommitResponse}. - *
  • {@link Options#repeatableReadIsolationLevel()}: Request Repeatable Read Isolation Level - * from the backend. - *
  • {@link Options#serializableIsolationLevel()}: Request Serializable Isolation Level from - * the backend. + *
  • {@link Options#isolationLevelOption(IsolationLevel)}: The isolation level for the + * transaction * */ TransactionRunner readWriteTransaction(TransactionOption... options); @@ -466,10 +457,8 @@ ServerStream batchWriteAtLeastOnce( * applied to any other requests on the transaction. *
  • {@link Options#commitStats()}: Request that the server includes commit statistics in the * {@link CommitResponse}. - *
  • {@link Options#repeatableReadIsolationLevel()}: Request Repeatable Read Isolation Level - * from the backend. - *
  • {@link Options#serializableIsolationLevel()}: Request Serializable Isolation Level from - * the backend. + *
  • {@link Options#isolationLevelOption(IsolationLevel)}: The isolation level for the + * transaction * */ TransactionManager transactionManager(TransactionOption... options); @@ -510,10 +499,8 @@ ServerStream batchWriteAtLeastOnce( * applied to any other requests on the transaction. *
  • {@link Options#commitStats()}: Request that the server includes commit statistics in the * {@link CommitResponse}. - *
  • {@link Options#repeatableReadIsolationLevel()}: Request Repeatable Read Isolation Level - * from the backend. - *
  • {@link Options#serializableIsolationLevel()}: Request Serializable Isolation Level from - * the backend. + *
  • {@link Options#isolationLevelOption(IsolationLevel)}: The isolation level for the + * transaction * */ AsyncRunner runAsync(TransactionOption... options); @@ -568,10 +555,8 @@ ServerStream batchWriteAtLeastOnce( * applied to any other requests on the transaction. *
  • {@link Options#commitStats()}: Request that the server includes commit statistics in the * {@link CommitResponse}. - *
  • {@link Options#repeatableReadIsolationLevel()}: Request Repeatable Read Isolation Level - * from the backend. - *
  • {@link Options#serializableIsolationLevel()}: Request Serializable Isolation Level from - * the backend. + *
  • {@link Options#isolationLevelOption(IsolationLevel)}: The isolation level for the + * transaction * */ AsyncTransactionManager transactionManagerAsync(TransactionOption... options); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Options.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Options.java index ad3e3347d6b..2b2f64a6aae 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Options.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Options.java @@ -24,10 +24,7 @@ import com.google.spanner.v1.TransactionOptions.IsolationLevel; import java.io.Serializable; import java.time.Duration; -import java.util.Arrays; import java.util.Objects; -import java.util.function.Predicate; -import java.util.stream.Stream; /** Specifies options for various spanner operations */ public final class Options implements Serializable { @@ -135,29 +132,7 @@ public interface UpdateAdminApiOption extends AdminApiOption {} public interface QueryOption {} /** Marker interface to mark options applicable to write operations */ - public interface TransactionOption { - Predicate isValidIsolationLevelOption = - txnOption -> - txnOption instanceof RepeatableReadOption || txnOption instanceof SerializableOption; - - /** - * Combines two arrays of TransactionOption, with primaryOptions taking precedence in case of - * conflicts. Note that {@link - * com.google.cloud.spanner.SpannerOptions.Builder.TransactionOptions} supports only the {@link - * IsolationLevel} TransactionOption, meaning spannerOptions will contain a maximum of one - * TransactionOption. - */ - static TransactionOption[] combine( - TransactionOption[] primaryOptions, TransactionOption[] spannerOptions) { - if (spannerOptions == null - || Arrays.stream(primaryOptions).anyMatch(isValidIsolationLevelOption)) { - return primaryOptions; - } else { - return Stream.concat(Arrays.stream(primaryOptions), Arrays.stream(spannerOptions)) - .toArray(TransactionOption[]::new); - } - } - } + public interface TransactionOption {} /** Marker interface to mark options applicable to update operation. */ public interface UpdateOption {} @@ -186,19 +161,10 @@ public static TransactionOption optimisticLock() { } /** - * Specifying this instructs the transaction to request Repeatable Read Isolation Level from the - * backend. - */ - public static TransactionOption repeatableReadIsolationLevel() { - return REPEATABLE_READ_OPTION; - } - - /** - * Specifying this instructs the transaction to request Serializable Isolation Level from the - * backend. + * Specifying this instructs the transaction to request {@link IsolationLevel} from the backend. */ - public static TransactionOption serializableIsolationLevel() { - return SERIALIZABLE_OPTION; + public static IsolationLevelOption isolationLevelOption(IsolationLevel isolationLevel) { + return new IsolationLevelOption(isolationLevel); } /** @@ -532,26 +498,20 @@ void appendToOptions(Options options) { } } - /** Option to request REPEATABLE READ isolation level for read/write transactions. */ - static final class RepeatableReadOption extends InternalOption implements TransactionOption { - @Override - void appendToOptions(Options options) { - options.isolationLevel = IsolationLevel.REPEATABLE_READ; - } - } + /** Option to set isolation level for read/write transactions. */ + static final class IsolationLevelOption extends InternalOption implements TransactionOption { + private final IsolationLevel isolationLevel; - static final RepeatableReadOption REPEATABLE_READ_OPTION = new RepeatableReadOption(); + public IsolationLevelOption(IsolationLevel isolationLevel) { + this.isolationLevel = isolationLevel; + } - /** Option to request SERIALIZABLE isolation level for read/write transactions. */ - static final class SerializableOption extends InternalOption implements TransactionOption { @Override void appendToOptions(Options options) { - options.isolationLevel = IsolationLevel.SERIALIZABLE; + options.isolationLevel = isolationLevel; } } - static final SerializableOption SERIALIZABLE_OPTION = new SerializableOption(); - private boolean withCommitStats; private Duration maxCommitDelay; diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionImpl.java index 1a4e2b00c6d..fa1bbf9fea3 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionImpl.java @@ -30,6 +30,7 @@ import com.google.cloud.spanner.Options.TransactionOption; import com.google.cloud.spanner.Options.UpdateOption; import com.google.cloud.spanner.SessionClient.SessionOption; +import com.google.cloud.spanner.SpannerOptions.Builder.TransactionOptions; import com.google.cloud.spanner.TransactionRunnerImpl.TransactionContextImpl; import com.google.cloud.spanner.spi.v1.SpannerRpc; import com.google.common.base.Ticker; @@ -44,7 +45,6 @@ import com.google.spanner.v1.CommitRequest; import com.google.spanner.v1.RequestOptions; import com.google.spanner.v1.Transaction; -import com.google.spanner.v1.TransactionOptions; import java.time.Instant; import java.util.ArrayList; import java.util.Collection; @@ -69,15 +69,18 @@ static void throwIfTransactionsPending() { } } - static TransactionOptions createReadWriteTransactionOptions( + static com.google.spanner.v1.TransactionOptions createReadWriteTransactionOptions( Options options, ByteString previousTransactionId) { - TransactionOptions.Builder transactionOptions = TransactionOptions.newBuilder(); + com.google.spanner.v1.TransactionOptions.Builder transactionOptions = + com.google.spanner.v1.TransactionOptions.newBuilder(); if (options.withExcludeTxnFromChangeStreams() == Boolean.TRUE) { transactionOptions.setExcludeTxnFromChangeStreams(true); } - TransactionOptions.ReadWrite.Builder readWrite = TransactionOptions.ReadWrite.newBuilder(); + com.google.spanner.v1.TransactionOptions.ReadWrite.Builder readWrite = + com.google.spanner.v1.TransactionOptions.ReadWrite.newBuilder(); if (options.withOptimisticLock() == Boolean.TRUE) { - readWrite.setReadLockMode(TransactionOptions.ReadWrite.ReadLockMode.OPTIMISTIC); + readWrite.setReadLockMode( + com.google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode.OPTIMISTIC); } if (previousTransactionId != null && previousTransactionId != com.google.protobuf.ByteString.EMPTY) { @@ -196,6 +199,14 @@ void markUsed(Instant instant) { sessionReference.markUsed(instant); } + com.google.spanner.v1.TransactionOptions defaultTransactionOptions() { + TransactionOptions transactionOptions = + this.spanner.getOptions().getDefaultTransactionOptions(); + return transactionOptions != null + ? transactionOptions.getTransactionOptions() + : com.google.spanner.v1.TransactionOptions.getDefaultInstance(); + } + public DatabaseId getDatabaseId() { return sessionReference.getDatabaseId(); } @@ -242,26 +253,24 @@ public CommitResponse writeAtLeastOnceWithOptions( setActive(null); List mutationsProto = new ArrayList<>(); Mutation.toProtoAndReturnRandomMutation(mutations, mutationsProto); - Options options = - Options.fromTransactionOptions( - TransactionOption.combine( - transactionOptions, this.spanner.getOptions().getTransactionOptions())); + Options options = Options.fromTransactionOptions(transactionOptions); final CommitRequest.Builder requestBuilder = CommitRequest.newBuilder() .setSession(getName()) .setReturnCommitStats(options.withCommitStats()) .addAllMutations(mutationsProto); - TransactionOptions.Builder transactionOptionsBuilder = - TransactionOptions.newBuilder() - .setReadWrite(TransactionOptions.ReadWrite.getDefaultInstance()); + com.google.spanner.v1.TransactionOptions.Builder transactionOptionsBuilder = + com.google.spanner.v1.TransactionOptions.newBuilder() + .setReadWrite(com.google.spanner.v1.TransactionOptions.ReadWrite.getDefaultInstance()); if (options.withExcludeTxnFromChangeStreams() == Boolean.TRUE) { transactionOptionsBuilder.setExcludeTxnFromChangeStreams(true); } if (options.isolationLevel() != null) { transactionOptionsBuilder.setIsolationLevel(options.isolationLevel()); } - requestBuilder.setSingleUseTransaction(transactionOptionsBuilder); + requestBuilder.setSingleUseTransaction( + this.defaultTransactionOptions().toBuilder().mergeFrom(transactionOptionsBuilder.build())); if (options.hasMaxCommitDelay()) { requestBuilder.setMaxCommitDelay( @@ -405,37 +414,22 @@ public ReadOnlyTransaction readOnlyTransaction(TimestampBound bound) { @Override public TransactionRunner readWriteTransaction(TransactionOption... options) { - return setActive( - new TransactionRunnerImpl( - this, - TransactionOption.combine(options, this.spanner.getOptions().getTransactionOptions()))); + return setActive(new TransactionRunnerImpl(this, options)); } @Override public AsyncRunner runAsync(TransactionOption... options) { - return new AsyncRunnerImpl( - setActive( - new TransactionRunnerImpl( - this, - TransactionOption.combine( - options, this.spanner.getOptions().getTransactionOptions())))); + return new AsyncRunnerImpl(setActive(new TransactionRunnerImpl(this, options))); } @Override public TransactionManager transactionManager(TransactionOption... options) { - return new TransactionManagerImpl( - this, - currentSpan, - tracer, - TransactionOption.combine(options, this.spanner.getOptions().getTransactionOptions())); + return new TransactionManagerImpl(this, currentSpan, tracer, options); } @Override public AsyncTransactionManagerImpl transactionManagerAsync(TransactionOption... options) { - return new AsyncTransactionManagerImpl( - this, - currentSpan, - TransactionOption.combine(options, this.spanner.getOptions().getTransactionOptions())); + return new AsyncTransactionManagerImpl(this, currentSpan, options); } @Override @@ -468,7 +462,11 @@ ApiFuture beginTransactionAsync( BeginTransactionRequest.newBuilder() .setSession(getName()) .setOptions( - createReadWriteTransactionOptions(transactionOptions, previousTransactionId)); + defaultTransactionOptions() + .toBuilder() + .mergeFrom( + createReadWriteTransactionOptions( + transactionOptions, previousTransactionId))); if (sessionReference.getIsMultiplexed() && mutation != null) { requestBuilder.setMutationKey(mutation); } @@ -513,7 +511,6 @@ TransactionContextImpl newTransaction(Options options, ByteString previousTransa .setOptions(options) .setTransactionId(null) .setPreviousTransactionId(previousTransactionId) - .setOptions(options) .setTrackTransactionStarter(spanner.getOptions().isTrackTransactionStarter()) .setRpc(spanner.getRpc()) .setDefaultQueryOptions(spanner.getDefaultQueryOptions(getDatabaseId())) diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java index 3eda32ff69c..6d5c2460e2d 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java @@ -18,7 +18,6 @@ import static com.google.api.gax.util.TimeConversionUtils.toJavaTimeDuration; import static com.google.api.gax.util.TimeConversionUtils.toThreetenDuration; -import static com.google.cloud.spanner.Options.TransactionOption.isValidIsolationLevelOption; import com.google.api.core.ApiFunction; import com.google.api.core.BetaApi; @@ -32,10 +31,8 @@ import com.google.api.gax.retrying.RetrySettings; import com.google.api.gax.rpc.ApiCallContext; import com.google.api.gax.rpc.TransportChannelProvider; -import com.google.api.gax.tracing.ApiTracer; import com.google.api.gax.tracing.ApiTracerFactory; import com.google.api.gax.tracing.BaseApiTracerFactory; -import com.google.api.gax.tracing.MetricsTracer; import com.google.api.gax.tracing.OpencensusTracerFactory; import com.google.auth.oauth2.AccessToken; import com.google.auth.oauth2.GoogleCredentials; @@ -47,9 +44,10 @@ import com.google.cloud.grpc.GcpManagedChannelOptions; import com.google.cloud.grpc.GrpcTransportOptions; import com.google.cloud.spanner.Options.DirectedReadOption; +import com.google.cloud.spanner.Options.IsolationLevelOption; import com.google.cloud.spanner.Options.QueryOption; -import com.google.cloud.spanner.Options.TransactionOption; import com.google.cloud.spanner.Options.UpdateOption; +import com.google.cloud.spanner.SpannerOptions.Builder.TransactionOptions; import com.google.cloud.spanner.admin.database.v1.DatabaseAdminSettings; import com.google.cloud.spanner.admin.database.v1.stub.DatabaseAdminStubSettings; import com.google.cloud.spanner.admin.instance.v1.InstanceAdminSettings; @@ -182,7 +180,7 @@ public class SpannerOptions extends ServiceOptions { private final boolean enableExtendedTracing; private final boolean enableEndToEndTracing; private final String monitoringHost; - private final TransactionOption[] transactionOptions; + private final TransactionOptions transactionOptions; enum TracingFramework { OPEN_CENSUS, @@ -994,7 +992,7 @@ public static class Builder private String monitoringHost = SpannerOptions.environment.getMonitoringHost(); private SslContext mTLSContext = null; private boolean isExperimentalHost = false; - private TransactionOption[] transactionOptions; + private TransactionOptions transactionOptions; private static String createCustomClientLibToken(String token) { return token + " " + ServiceOptions.getGoogApiClientLibName(); @@ -1654,48 +1652,41 @@ public Builder setEnableEndToEndTracing(boolean enableEndToEndTracing) { } public static class TransactionOptions { - private final List transactionOptions = new ArrayList<>(); + private com.google.spanner.v1.TransactionOptions transactionOptions; private TransactionOptions() {} - TransactionOption[] getTransactionOptions() { - return transactionOptions.toArray(new TransactionOption[0]); + com.google.spanner.v1.TransactionOptions getTransactionOptions() { + return transactionOptions; } public static class TransactionOptionsBuilder { - - private final List transactionOptions = new ArrayList<>(); - private static final String INVALID_ISOLATION_LEVEL_MESSAGE = - "Either repeatable read or serializable isolation level is allowed"; + private IsolationLevelOption isolationLevelOption; public static TransactionOptionsBuilder newBuilder() { return new TransactionOptionsBuilder(); } - public TransactionOptionsBuilder setIsolationLevel(TransactionOption transactionOption) { - if (!isValidIsolationLevelOption.test(transactionOption)) { - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.INVALID_ARGUMENT, INVALID_ISOLATION_LEVEL_MESSAGE); - } - transactionOptions.removeIf(isValidIsolationLevelOption); - transactionOptions.add(transactionOption); + public TransactionOptionsBuilder setIsolationLevel(IsolationLevelOption option) { + this.isolationLevelOption = option; return this; } public TransactionOptions build() { - TransactionOptions options = new TransactionOptions(); - options.transactionOptions.addAll(this.transactionOptions); - return options; + TransactionOptions transactionOptions = new TransactionOptions(); + transactionOptions.transactionOptions = + com.google.spanner.v1.TransactionOptions.newBuilder() + .setIsolationLevel( + Options.fromTransactionOptions(isolationLevelOption).isolationLevel()) + .build(); + return transactionOptions; } } } - /** - * Sets the default transaction options. Only Isolation level option is supported via - * TransactionOptions. - */ + /** Sets the default transaction options. */ public Builder setDefaultTransactionOptions(TransactionOptions transactionOptions) { - this.transactionOptions = transactionOptions.getTransactionOptions(); + this.transactionOptions = transactionOptions; return this; } @@ -2044,7 +2035,7 @@ String getMonitoringHost() { return monitoringHost; } - TransactionOption[] getTransactionOptions() { + public TransactionOptions getDefaultTransactionOptions() { return transactionOptions; } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java index fad4ce564ab..038fb4b52eb 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java @@ -643,8 +643,12 @@ TransactionSelector getTransactionSelector() { if (tx == null) { return TransactionSelector.newBuilder() .setBegin( - SessionImpl.createReadWriteTransactionOptions( - options, getPreviousTransactionId())) + this.session + .defaultTransactionOptions() + .toBuilder() + .mergeFrom( + SessionImpl.createReadWriteTransactionOptions( + options, getPreviousTransactionId()))) .build(); } else { // Wait for the transaction to come available. The tx.get() call will fail with an diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java index 3918781076e..0fb4af2e8c7 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java @@ -49,7 +49,6 @@ import com.google.cloud.NoCredentials; import com.google.cloud.Timestamp; import com.google.cloud.spanner.AsyncResultSet.CallbackResponse; -import com.google.cloud.spanner.AsyncTransactionManager.TransactionContextFuture; import com.google.cloud.spanner.MockSpannerServiceImpl.SimulatedExecutionTime; import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult; import com.google.cloud.spanner.Options.RpcLockHint; @@ -1346,10 +1345,7 @@ public void testPoolMaintainer_whenPDMLFollowedByInactiveTransaction_removeSessi public void testWrite() { DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); - Timestamp timestamp = - client.write( - Collections.singletonList( - Mutation.newInsertBuilder("FOO").set("ID").to(1L).set("NAME").to("Bar").build())); + Timestamp timestamp = MockSpannerTestActions.writeInsertMutation(client); assertNotNull(timestamp); List beginTransactions = @@ -1376,10 +1372,7 @@ public void testWriteAborted() { mockSpanner.setCommitExecutionTime( SimulatedExecutionTime.ofException( mockSpanner.createAbortedException(ByteString.copyFromUtf8("test")))); - Timestamp timestamp = - client.write( - Collections.singletonList( - Mutation.newInsertBuilder("FOO").set("ID").to(1L).set("NAME").to("Bar").build())); + Timestamp timestamp = MockSpannerTestActions.writeInsertMutation(client); assertNotNull(timestamp); List commitRequests = mockSpanner.getRequestsOfType(CommitRequest.class); @@ -1395,10 +1388,7 @@ public void testWriteAtLeastOnceAborted() { mockSpanner.setCommitExecutionTime( SimulatedExecutionTime.ofException( mockSpanner.createAbortedException(ByteString.copyFromUtf8("test")))); - Timestamp timestamp = - client.writeAtLeastOnce( - Collections.singletonList( - Mutation.newInsertBuilder("FOO").set("ID").to(1L).set("NAME").to("Bar").build())); + Timestamp timestamp = MockSpannerTestActions.writeAtLeastOnceInsertMutation(client); assertNotNull(timestamp); List commitRequests = mockSpanner.getRequestsOfType(CommitRequest.class); @@ -1409,10 +1399,8 @@ public void testWriteAtLeastOnceAborted() { public void testWriteWithOptions() { DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); - client.writeWithOptions( - Collections.singletonList( - Mutation.newInsertBuilder("FOO").set("ID").to(1L).set("NAME").to("Bar").build()), - Options.priority(RpcPriority.HIGH)); + MockSpannerTestActions.writeInsertMutationWithOptions( + client, Options.priority(RpcPriority.HIGH)); List beginTransactions = mockSpanner.getRequestsOfType(BeginTransactionRequest.class); @@ -1447,10 +1435,8 @@ public void testWriteWithCommitStats() { public void testWriteWithExcludeTxnFromChangeStreams() { DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); - client.writeWithOptions( - Collections.singletonList( - Mutation.newInsertBuilder("FOO").set("ID").to(1L).set("NAME").to("Bar").build()), - Options.excludeTxnFromChangeStreams()); + MockSpannerTestActions.writeInsertMutationWithOptions( + client, Options.excludeTxnFromChangeStreams()); List beginTransactions = mockSpanner.getRequestsOfType(BeginTransactionRequest.class); @@ -1465,10 +1451,7 @@ public void testWriteWithExcludeTxnFromChangeStreams() { public void testWriteAtLeastOnce() { DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); - Timestamp timestamp = - client.writeAtLeastOnce( - Collections.singletonList( - Mutation.newInsertBuilder("FOO").set("ID").to(1L).set("NAME").to("Bar").build())); + Timestamp timestamp = MockSpannerTestActions.writeAtLeastOnceInsertMutation(client); assertNotNull(timestamp); List commitRequests = mockSpanner.getRequestsOfType(CommitRequest.class); @@ -1508,10 +1491,8 @@ public void testWriteAtLeastOnceWithCommitStats() { public void testWriteAtLeastOnceWithOptions() { DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); - client.writeAtLeastOnceWithOptions( - Collections.singletonList( - Mutation.newInsertBuilder("FOO").set("ID").to(1L).set("NAME").to("Bar").build()), - Options.priority(RpcPriority.LOW)); + MockSpannerTestActions.writeAtLeastOnceWithOptionsInsertMutation( + client, Options.priority(RpcPriority.LOW)); List commitRequests = mockSpanner.getRequestsOfType(CommitRequest.class); assertThat(commitRequests).hasSize(1); @@ -1527,10 +1508,8 @@ public void testWriteAtLeastOnceWithOptions() { public void testWriteAtLeastOnceWithTagOptions() { DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); - client.writeAtLeastOnceWithOptions( - Collections.singletonList( - Mutation.newInsertBuilder("FOO").set("ID").to(1L).set("NAME").to("Bar").build()), - Options.tag("app=spanner,env=test")); + MockSpannerTestActions.writeAtLeastOnceWithOptionsInsertMutation( + client, Options.tag("app=spanner,env=test")); List commitRequests = mockSpanner.getRequestsOfType(CommitRequest.class); assertThat(commitRequests).hasSize(1); @@ -1547,10 +1526,8 @@ public void testWriteAtLeastOnceWithTagOptions() { public void testWriteAtLeastOnceWithExcludeTxnFromChangeStreams() { DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); - client.writeAtLeastOnceWithOptions( - Collections.singletonList( - Mutation.newInsertBuilder("FOO").set("ID").to(1L).set("NAME").to("Bar").build()), - Options.excludeTxnFromChangeStreams()); + MockSpannerTestActions.writeAtLeastOnceWithOptionsInsertMutation( + client, Options.excludeTxnFromChangeStreams()); List commitRequests = mockSpanner.getRequestsOfType(CommitRequest.class); assertThat(commitRequests).hasSize(1); @@ -2016,13 +1993,8 @@ public void testPartitionedDMLWithTag() { public void testCommitWithTag() { DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); - TransactionRunner runner = - client.readWriteTransaction(Options.tag("app=spanner,env=test,action=commit")); - runner.run( - transaction -> { - transaction.buffer(Mutation.delete("TEST", KeySet.all())); - return null; - }); + MockSpannerTestActions.commitDeleteTransaction( + client, Options.tag("app=spanner,env=test,action=commit")); List beginTransactions = mockSpanner.getRequestsOfType(BeginTransactionRequest.class); @@ -2045,12 +2017,8 @@ public void testCommitWithTag() { public void testTransactionManagerCommitWithTag() { DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); - try (TransactionManager manager = - client.transactionManager(Options.tag("app=spanner,env=test,action=manager"))) { - TransactionContext transaction = manager.begin(); - transaction.buffer(Mutation.delete("TEST", KeySet.all())); - manager.commit(); - } + MockSpannerTestActions.transactionManagerCommit( + client, Options.tag("app=spanner,env=test,action=manager")); List beginTransactions = mockSpanner.getRequestsOfType(BeginTransactionRequest.class); @@ -2076,14 +2044,8 @@ public void testTransactionManagerCommitWithTag() { public void testAsyncRunnerCommitWithTag() { DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); - AsyncRunner runner = client.runAsync(Options.tag("app=spanner,env=test,action=runner")); - get( - runner.runAsync( - txn -> { - txn.buffer(Mutation.delete("TEST", KeySet.all())); - return ApiFutures.immediateFuture(null); - }, - executor)); + MockSpannerTestActions.asyncRunnerCommit( + client, executor, Options.tag("app=spanner,env=test,action=runner")); List beginTransactions = mockSpanner.getRequestsOfType(BeginTransactionRequest.class); @@ -2109,19 +2071,8 @@ public void testAsyncRunnerCommitWithTag() { public void testAsyncTransactionManagerCommitWithTag() { DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); - try (AsyncTransactionManager manager = - client.transactionManagerAsync(Options.tag("app=spanner,env=test,action=manager"))) { - TransactionContextFuture transaction = manager.beginAsync(); - get( - transaction - .then( - (txn, input) -> { - txn.buffer(Mutation.delete("TEST", KeySet.all())); - return ApiFutures.immediateFuture(null); - }, - executor) - .commitAsync()); - } + MockSpannerTestActions.transactionManagerAsyncCommit( + client, executor, Options.tag("app=spanner,env=test,action=manager")); List beginTransactions = mockSpanner.getRequestsOfType(BeginTransactionRequest.class); @@ -2162,8 +2113,8 @@ public void testReadWriteTxnWithExcludeTxnFromChangeStreams_executeUpdate() { public void testReadWriteTxnWithExcludeTxnFromChangeStreams_batchUpdate() { DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); - TransactionRunner runner = client.readWriteTransaction(Options.excludeTxnFromChangeStreams()); - runner.run(transaction -> transaction.batchUpdate(Collections.singletonList(UPDATE_STATEMENT))); + MockSpannerTestActions.executeBatchUpdateTransaction( + client, Options.excludeTxnFromChangeStreams()); List requests = mockSpanner.getRequestsOfType(ExecuteBatchDmlRequest.class); @@ -2193,12 +2144,7 @@ public void testPartitionedDMLWithExcludeTxnFromChangeStreams() { public void testCommitWithExcludeTxnFromChangeStreams() { DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); - TransactionRunner runner = client.readWriteTransaction(Options.excludeTxnFromChangeStreams()); - runner.run( - transaction -> { - transaction.buffer(Mutation.delete("TEST", KeySet.all())); - return null; - }); + MockSpannerTestActions.commitDeleteTransaction(client, Options.excludeTxnFromChangeStreams()); List beginTransactions = mockSpanner.getRequestsOfType(BeginTransactionRequest.class); @@ -2213,12 +2159,7 @@ public void testCommitWithExcludeTxnFromChangeStreams() { public void testTransactionManagerCommitWithExcludeTxnFromChangeStreams() { DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); - try (TransactionManager manager = - client.transactionManager(Options.excludeTxnFromChangeStreams())) { - TransactionContext transaction = manager.begin(); - transaction.buffer(Mutation.delete("TEST", KeySet.all())); - manager.commit(); - } + MockSpannerTestActions.transactionManagerCommit(client, Options.excludeTxnFromChangeStreams()); List beginTransactions = mockSpanner.getRequestsOfType(BeginTransactionRequest.class); @@ -2233,14 +2174,8 @@ public void testTransactionManagerCommitWithExcludeTxnFromChangeStreams() { public void testAsyncRunnerCommitWithExcludeTxnFromChangeStreams() { DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); - AsyncRunner runner = client.runAsync(Options.excludeTxnFromChangeStreams()); - get( - runner.runAsync( - txn -> { - txn.buffer(Mutation.delete("TEST", KeySet.all())); - return ApiFutures.immediateFuture(null); - }, - executor)); + MockSpannerTestActions.asyncRunnerCommit( + client, executor, Options.excludeTxnFromChangeStreams()); List beginTransactions = mockSpanner.getRequestsOfType(BeginTransactionRequest.class); @@ -2255,19 +2190,8 @@ public void testAsyncRunnerCommitWithExcludeTxnFromChangeStreams() { public void testAsyncTransactionManagerCommitWithExcludeTxnFromChangeStreams() { DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); - try (AsyncTransactionManager manager = - client.transactionManagerAsync(Options.excludeTxnFromChangeStreams())) { - TransactionContextFuture transaction = manager.beginAsync(); - get( - transaction - .then( - (txn, input) -> { - txn.buffer(Mutation.delete("TEST", KeySet.all())); - return ApiFutures.immediateFuture(null); - }, - executor) - .commitAsync()); - } + MockSpannerTestActions.transactionManagerAsyncCommit( + client, executor, Options.excludeTxnFromChangeStreams()); List beginTransactions = mockSpanner.getRequestsOfType(BeginTransactionRequest.class); @@ -4196,12 +4120,7 @@ public void testPartitionedDMLWithPriority() { public void testCommitWithPriority() { DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); - TransactionRunner runner = client.readWriteTransaction(Options.priority(RpcPriority.HIGH)); - runner.run( - transaction -> { - transaction.buffer(Mutation.delete("TEST", KeySet.all())); - return null; - }); + MockSpannerTestActions.commitDeleteTransaction(client, Options.priority(RpcPriority.HIGH)); List requests = mockSpanner.getRequestsOfType(CommitRequest.class); assertThat(requests).hasSize(1); @@ -4214,12 +4133,7 @@ public void testCommitWithPriority() { public void testTransactionManagerCommitWithPriority() { DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); - try (TransactionManager manager = - client.transactionManager(Options.priority(RpcPriority.HIGH))) { - TransactionContext transaction = manager.begin(); - transaction.buffer(Mutation.delete("TEST", KeySet.all())); - manager.commit(); - } + MockSpannerTestActions.transactionManagerCommit(client, Options.priority(RpcPriority.HIGH)); List requests = mockSpanner.getRequestsOfType(CommitRequest.class); assertThat(requests).hasSize(1); @@ -4232,14 +4146,7 @@ public void testTransactionManagerCommitWithPriority() { public void testAsyncRunnerCommitWithPriority() { DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); - AsyncRunner runner = client.runAsync(Options.priority(RpcPriority.HIGH)); - get( - runner.runAsync( - txn -> { - txn.buffer(Mutation.delete("TEST", KeySet.all())); - return ApiFutures.immediateFuture(null); - }, - executor)); + MockSpannerTestActions.asyncRunnerCommit(client, executor, Options.priority(RpcPriority.HIGH)); List requests = mockSpanner.getRequestsOfType(CommitRequest.class); assertThat(requests).hasSize(1); @@ -4252,19 +4159,8 @@ public void testAsyncRunnerCommitWithPriority() { public void testAsyncTransactionManagerCommitWithPriority() { DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); - try (AsyncTransactionManager manager = - client.transactionManagerAsync(Options.priority(RpcPriority.HIGH))) { - TransactionContextFuture transaction = manager.beginAsync(); - get( - transaction - .then( - (txn, input) -> { - txn.buffer(Mutation.delete("TEST", KeySet.all())); - return ApiFutures.immediateFuture(null); - }, - executor) - .commitAsync()); - } + MockSpannerTestActions.transactionManagerAsyncCommit( + client, executor, Options.priority(RpcPriority.HIGH)); List requests = mockSpanner.getRequestsOfType(CommitRequest.class); assertThat(requests).hasSize(1); @@ -4277,12 +4173,7 @@ public void testAsyncTransactionManagerCommitWithPriority() { public void testCommitWithoutMaxCommitDelay() { DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); - TransactionRunner runner = client.readWriteTransaction(); - runner.run( - transaction -> { - transaction.buffer(Mutation.delete("TEST", KeySet.all())); - return null; - }); + MockSpannerTestActions.commitDeleteTransaction(client); List requests = mockSpanner.getRequestsOfType(CommitRequest.class); assertThat(requests).hasSize(1); @@ -4294,13 +4185,8 @@ public void testCommitWithoutMaxCommitDelay() { public void testCommitWithMaxCommitDelay() { DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); - TransactionRunner runner = - client.readWriteTransaction(Options.maxCommitDelay(java.time.Duration.ofMillis(100))); - runner.run( - transaction -> { - transaction.buffer(Mutation.delete("TEST", KeySet.all())); - return null; - }); + MockSpannerTestActions.commitDeleteTransaction( + client, Options.maxCommitDelay(java.time.Duration.ofMillis(100))); List requests = mockSpanner.getRequestsOfType(CommitRequest.class); assertThat(requests).hasSize(1); @@ -4315,11 +4201,8 @@ public void testCommitWithMaxCommitDelay() { public void testTransactionManagerCommitWithMaxCommitDelay() { DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); - TransactionManager manager = - client.transactionManager(Options.maxCommitDelay(java.time.Duration.ofMillis(100))); - TransactionContext transaction = manager.begin(); - transaction.buffer(Mutation.delete("TEST", KeySet.all())); - manager.commit(); + MockSpannerTestActions.transactionManagerCommit( + client, Options.maxCommitDelay(java.time.Duration.ofMillis(100))); List requests = mockSpanner.getRequestsOfType(CommitRequest.class); assertThat(requests).hasSize(1); @@ -4334,14 +4217,8 @@ public void testTransactionManagerCommitWithMaxCommitDelay() { public void testAsyncRunnerCommitWithMaxCommitDelay() { DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); - AsyncRunner runner = client.runAsync(Options.maxCommitDelay(java.time.Duration.ofMillis(100))); - get( - runner.runAsync( - txn -> { - txn.buffer(Mutation.delete("TEST", KeySet.all())); - return ApiFutures.immediateFuture(null); - }, - executor)); + MockSpannerTestActions.asyncRunnerCommit( + client, executor, Options.maxCommitDelay(java.time.Duration.ofMillis(100))); List requests = mockSpanner.getRequestsOfType(CommitRequest.class); assertThat(requests).hasSize(1); @@ -4356,19 +4233,8 @@ public void testAsyncRunnerCommitWithMaxCommitDelay() { public void testAsyncTransactionManagerCommitWithMaxCommitDelay() { DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); - try (AsyncTransactionManager manager = - client.transactionManagerAsync(Options.maxCommitDelay(java.time.Duration.ofMillis(100)))) { - TransactionContextFuture transaction = manager.beginAsync(); - get( - transaction - .then( - (txn, input) -> { - txn.buffer(Mutation.delete("TEST", KeySet.all())); - return ApiFutures.immediateFuture(null); - }, - executor) - .commitAsync()); - } + MockSpannerTestActions.transactionManagerAsyncCommit( + client, executor, Options.maxCommitDelay(java.time.Duration.ofMillis(100))); List requests = mockSpanner.getRequestsOfType(CommitRequest.class); assertThat(requests).hasSize(1); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplWithTransactionOptionsTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplWithTransactionOptionsTest.java index 174d80dfc73..54809ee7098 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplWithTransactionOptionsTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplWithTransactionOptionsTest.java @@ -1,30 +1,42 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.google.cloud.spanner; -import static com.google.cloud.spanner.SpannerApiFutures.get; +import static com.google.cloud.spanner.MockSpannerTestUtil.INVALID_SELECT_STATEMENT; +import static com.google.cloud.spanner.MockSpannerTestUtil.SELECT1; +import static com.google.cloud.spanner.MockSpannerTestUtil.SELECT1_RESULTSET; +import static com.google.cloud.spanner.MockSpannerTestUtil.UPDATE_COUNT; +import static com.google.cloud.spanner.MockSpannerTestUtil.UPDATE_STATEMENT; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; -import com.google.api.core.ApiFutures; import com.google.api.gax.grpc.testing.LocalChannelProvider; import com.google.cloud.NoCredentials; -import com.google.cloud.Timestamp; -import com.google.cloud.spanner.AsyncTransactionManager.TransactionContextFuture; import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult; +import com.google.cloud.spanner.Options.IsolationLevelOption; import com.google.cloud.spanner.Options.RpcPriority; import com.google.cloud.spanner.SpannerOptions.Builder.TransactionOptions.TransactionOptionsBuilder; import com.google.protobuf.AbstractMessage; -import com.google.protobuf.ListValue; import com.google.spanner.v1.BeginTransactionRequest; import com.google.spanner.v1.CommitRequest; import com.google.spanner.v1.ExecuteBatchDmlRequest; import com.google.spanner.v1.ExecuteSqlRequest; import com.google.spanner.v1.ReadRequest; -import com.google.spanner.v1.ResultSetMetadata; -import com.google.spanner.v1.StructType; -import com.google.spanner.v1.StructType.Field; import com.google.spanner.v1.TransactionOptions.IsolationLevel; -import com.google.spanner.v1.TypeCode; import io.grpc.Server; import io.grpc.Status; import io.grpc.inprocess.InProcessServerBuilder; @@ -33,6 +45,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.function.Consumer; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; @@ -43,44 +56,15 @@ @RunWith(JUnit4.class) public class DatabaseClientImplWithTransactionOptionsTest { - + private static final IsolationLevelOption SERIALIZABLE_ISOLATION_OPTION = + Options.isolationLevelOption(IsolationLevel.SERIALIZABLE); + private static final IsolationLevelOption RR_ISOLATION_OPTION = + Options.isolationLevelOption(IsolationLevel.REPEATABLE_READ); private static MockSpannerServiceImpl mockSpanner; private static Server server; private static ExecutorService executor; private static LocalChannelProvider channelProvider; - private static final Statement UPDATE_STATEMENT = - Statement.of("UPDATE FOO SET BAR=1 WHERE BAZ=2"); - private static final Statement INVALID_UPDATE_STATEMENT = - Statement.of("UPDATE NON_EXISTENT_TABLE SET BAR=1 WHERE BAZ=2"); - private static final Statement INVALID_SELECT_STATEMENT = - Statement.of("SELECT * FROM NON_EXISTENT_TABLE"); - private static final long UPDATE_COUNT = 1L; - private static final Statement SELECT1 = Statement.of("SELECT 1 AS COL1"); - private static final ResultSetMetadata SELECT1_METADATA = - ResultSetMetadata.newBuilder() - .setRowType( - StructType.newBuilder() - .addFields( - Field.newBuilder() - .setName("COL1") - .setType( - com.google.spanner.v1.Type.newBuilder() - .setCode(TypeCode.INT64) - .build()) - .build()) - .build()) - .build(); - private static final com.google.spanner.v1.ResultSet SELECT1_RESULTSET = - com.google.spanner.v1.ResultSet.newBuilder() - .addRows( - ListValue.newBuilder() - .addValues(com.google.protobuf.Value.newBuilder().setStringValue("1").build()) - .build()) - .setMetadata(SELECT1_METADATA) - .build(); private Spanner spanner; - private Spanner spannerWithRepeatableReadOption; - private Spanner spannerWithSerializableOption; private DatabaseClient client; private DatabaseClient clientWithRepeatableReadOption; private DatabaseClient clientWithSerializableOption; @@ -91,10 +75,6 @@ public static void startStaticServer() throws IOException { mockSpanner.setAbortProbability(0.0D); // We don't want any unpredictable aborted transactions. mockSpanner.putStatementResult(StatementResult.update(UPDATE_STATEMENT, UPDATE_COUNT)); mockSpanner.putStatementResult(StatementResult.query(SELECT1, SELECT1_RESULTSET)); - mockSpanner.putStatementResult( - StatementResult.exception( - INVALID_UPDATE_STATEMENT, - Status.INVALID_ARGUMENT.withDescription("invalid statement").asRuntimeException())); mockSpanner.putStatementResult( StatementResult.exception( INVALID_SELECT_STATEMENT, @@ -131,334 +111,208 @@ public void setUp() { .setChannelProvider(channelProvider) .setCredentials(NoCredentials.getInstance()); spanner = spannerOptionsBuilder.build().getService(); - spannerWithRepeatableReadOption = + client = spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + clientWithRepeatableReadOption = spannerOptionsBuilder .setDefaultTransactionOptions( TransactionOptionsBuilder.newBuilder() - .setIsolationLevel(Options.repeatableReadIsolationLevel()) + .setIsolationLevel(RR_ISOLATION_OPTION) .build()) .build() - .getService(); - spannerWithSerializableOption = + .getService() + .getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + clientWithSerializableOption = spannerOptionsBuilder .setDefaultTransactionOptions( TransactionOptionsBuilder.newBuilder() - .setIsolationLevel(Options.serializableIsolationLevel()) + .setIsolationLevel(SERIALIZABLE_ISOLATION_OPTION) .build()) .build() - .getService(); - client = spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); - clientWithRepeatableReadOption = - spannerWithRepeatableReadOption.getDatabaseClient( - DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); - clientWithSerializableOption = - spannerWithSerializableOption.getDatabaseClient( - DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + .getService() + .getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + } + + private void executeTest( + Consumer testAction, IsolationLevel expectedIsolationLevel) { + testAction.accept(client); + validateIsolationLevel(expectedIsolationLevel); + } + + private void executeTestWithRR( + Consumer testAction, IsolationLevel expectedIsolationLevel) { + testAction.accept(clientWithRepeatableReadOption); + validateIsolationLevel(expectedIsolationLevel); + } + + private void executeTestWithSerializable( + Consumer testAction, IsolationLevel expectedIsolationLevel) { + testAction.accept(clientWithSerializableOption); + validateIsolationLevel(expectedIsolationLevel); } @After public void tearDown() { spanner.close(); - spannerWithRepeatableReadOption.close(); - spannerWithSerializableOption.close(); } @Test public void testWrite_WithNoIsolationLevel() { - Timestamp timestamp = - client.write( - Collections.singletonList( - Mutation.newInsertBuilder("FOO").set("ID").to(1L).set("NAME").to("Bar").build())); - assertNotNull(timestamp); - validateIsolationLevel(IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED); + executeTest( + MockSpannerTestActions::writeInsertMutation, IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED); } @Test public void testWrite_WithRRSpannerOptions() { - Timestamp timestamp = - clientWithRepeatableReadOption.write( - Collections.singletonList( - Mutation.newInsertBuilder("FOO").set("ID").to(1L).set("NAME").to("Bar").build())); - assertNotNull(timestamp); - validateIsolationLevel(IsolationLevel.REPEATABLE_READ); + executeTestWithRR(MockSpannerTestActions::writeInsertMutation, IsolationLevel.REPEATABLE_READ); } @Test public void testWriteWithOptions_WithRRSpannerOptions() { - clientWithRepeatableReadOption.writeWithOptions( - Collections.singletonList( - Mutation.newInsertBuilder("FOO").set("ID").to(1L).set("NAME").to("Bar").build()), - Options.priority(RpcPriority.HIGH)); - validateIsolationLevel(IsolationLevel.REPEATABLE_READ); + executeTestWithRR( + c -> + MockSpannerTestActions.writeInsertMutationWithOptions( + c, Options.priority(RpcPriority.HIGH)), + IsolationLevel.REPEATABLE_READ); } @Test public void testWriteWithOptions_WithSerializableTxnOption() { - clientWithRepeatableReadOption.writeWithOptions( - Collections.singletonList( - Mutation.newInsertBuilder("FOO").set("ID").to(1L).set("NAME").to("Bar").build()), - Options.serializableIsolationLevel()); - validateIsolationLevel(IsolationLevel.SERIALIZABLE); + executeTestWithRR( + c -> + MockSpannerTestActions.writeInsertMutationWithOptions(c, SERIALIZABLE_ISOLATION_OPTION), + IsolationLevel.SERIALIZABLE); } @Test public void testWriteAtLeastOnce_WithSerializableSpannerOptions() { - Timestamp timestamp = - clientWithSerializableOption.writeAtLeastOnce( - Collections.singletonList( - Mutation.newInsertBuilder("FOO").set("ID").to(1L).set("NAME").to("Bar").build())); - assertNotNull(timestamp); - validateIsolationLevel(IsolationLevel.SERIALIZABLE); + executeTestWithSerializable( + MockSpannerTestActions::writeAtLeastOnceInsertMutation, IsolationLevel.SERIALIZABLE); } @Test public void testWriteAtLeastOnceWithOptions_WithRRTxnOption() { - clientWithSerializableOption.writeAtLeastOnceWithOptions( - Collections.singletonList( - Mutation.newInsertBuilder("FOO").set("ID").to(1L).set("NAME").to("Bar").build()), - Options.repeatableReadIsolationLevel()); - validateIsolationLevel(IsolationLevel.REPEATABLE_READ); + executeTestWithSerializable( + c -> + MockSpannerTestActions.writeAtLeastOnceWithOptionsInsertMutation( + c, RR_ISOLATION_OPTION), + IsolationLevel.REPEATABLE_READ); } @Test public void testReadWriteTxn_WithRRSpannerOption_batchUpdate() { - TransactionRunner runner = clientWithRepeatableReadOption.readWriteTransaction(); - runner.run(transaction -> transaction.batchUpdate(Collections.singletonList(UPDATE_STATEMENT))); - validateIsolationLevel(IsolationLevel.REPEATABLE_READ); + executeTestWithRR( + MockSpannerTestActions::executeBatchUpdateTransaction, IsolationLevel.REPEATABLE_READ); } @Test public void testReadWriteTxn_WithSerializableTxnOption_batchUpdate() { - TransactionRunner runner = - clientWithRepeatableReadOption.readWriteTransaction(Options.serializableIsolationLevel()); - runner.run(transaction -> transaction.batchUpdate(Collections.singletonList(UPDATE_STATEMENT))); - validateIsolationLevel(IsolationLevel.SERIALIZABLE); + executeTestWithRR( + c -> MockSpannerTestActions.executeBatchUpdateTransaction(c, SERIALIZABLE_ISOLATION_OPTION), + IsolationLevel.SERIALIZABLE); } @Test public void testPartitionedDML_WithRRSpannerOption() { - clientWithRepeatableReadOption.executePartitionedUpdate(UPDATE_STATEMENT); - validateIsolationLevel(IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED); + executeTestWithRR( + MockSpannerTestActions::executePartitionedUpdate, + IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED); } @Test public void testCommit_WithSerializableTxnOption() { - TransactionRunner runner = client.readWriteTransaction(Options.serializableIsolationLevel()); - runner.run( - transaction -> { - transaction.buffer(Mutation.delete("TEST", KeySet.all())); - return null; - }); - - validateIsolationLevel(IsolationLevel.SERIALIZABLE); + executeTest( + c -> MockSpannerTestActions.commitDeleteTransaction(c, SERIALIZABLE_ISOLATION_OPTION), + IsolationLevel.SERIALIZABLE); } @Test public void testTransactionManagerCommit_WithRRTxnOption() { - try (TransactionManager manager = - clientWithSerializableOption.transactionManager(Options.repeatableReadIsolationLevel())) { - TransactionContext transaction = manager.begin(); - transaction.buffer(Mutation.delete("TEST", KeySet.all())); - manager.commit(); - } - - validateIsolationLevel(IsolationLevel.REPEATABLE_READ); + executeTestWithSerializable( + c -> MockSpannerTestActions.transactionManagerCommit(c, RR_ISOLATION_OPTION), + IsolationLevel.REPEATABLE_READ); } @Test public void testAsyncRunnerCommit_WithRRSpannerOption() { - AsyncRunner runner = clientWithRepeatableReadOption.runAsync(); - get( - runner.runAsync( - txn -> { - txn.buffer(Mutation.delete("TEST", KeySet.all())); - return ApiFutures.immediateFuture(null); - }, - executor)); - - validateIsolationLevel(IsolationLevel.REPEATABLE_READ); + executeTestWithRR( + c -> MockSpannerTestActions.asyncRunnerCommit(c, executor), IsolationLevel.REPEATABLE_READ); } @Test public void testAsyncTransactionManagerCommit_WithSerializableTxnOption() { - try (AsyncTransactionManager manager = - clientWithRepeatableReadOption.transactionManagerAsync( - Options.serializableIsolationLevel())) { - TransactionContextFuture transaction = manager.beginAsync(); - get( - transaction - .then( - (txn, input) -> { - txn.buffer(Mutation.delete("TEST", KeySet.all())); - return ApiFutures.immediateFuture(null); - }, - executor) - .commitAsync()); - } - - validateIsolationLevel(IsolationLevel.SERIALIZABLE); + executeTestWithRR( + c -> + MockSpannerTestActions.transactionManagerAsyncCommit( + c, executor, SERIALIZABLE_ISOLATION_OPTION), + IsolationLevel.SERIALIZABLE); } @Test public void testReadWriteTxn_WithNoOptions() { - client - .readWriteTransaction() - .run( - transaction -> { - try (ResultSet rs = transaction.executeQuery(SELECT1)) { - while (rs.next()) { - assertEquals(rs.getLong(0), 1); - } - } - return null; - }); - validateIsolationLevel(IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED); + executeTest(MockSpannerTestActions::executeSelect1, IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED); } @Test public void executeSqlWithRWTransactionOptions_RepeatableRead() { - client - .readWriteTransaction(Options.repeatableReadIsolationLevel()) - .run( - transaction -> { - try (ResultSet rs = transaction.executeQuery(SELECT1)) { - while (rs.next()) { - assertEquals(rs.getLong(0), 1); - } - } - return null; - }); - validateIsolationLevel(IsolationLevel.REPEATABLE_READ); + executeTest( + c -> MockSpannerTestActions.executeSelect1(c, RR_ISOLATION_OPTION), + IsolationLevel.REPEATABLE_READ); } @Test public void executeSqlWithDefaultSpannerOptions_SerializableAndRWTransactionOptions_RepeatableRead() { - clientWithSerializableOption - .readWriteTransaction(Options.repeatableReadIsolationLevel()) - .run( - transaction -> { - try (ResultSet rs = transaction.executeQuery(SELECT1)) { - while (rs.next()) { - assertEquals(rs.getLong(0), 1); - } - } - return null; - }); - validateIsolationLevel(IsolationLevel.REPEATABLE_READ); + executeTestWithSerializable( + c -> MockSpannerTestActions.executeSelect1(c, RR_ISOLATION_OPTION), + IsolationLevel.REPEATABLE_READ); } @Test public void executeSqlWithDefaultSpannerOptions_RepeatableReadAndRWTransactionOptions_Serializable() { - clientWithRepeatableReadOption - .readWriteTransaction(Options.serializableIsolationLevel()) - .run( - transaction -> { - try (ResultSet rs = transaction.executeQuery(SELECT1)) { - while (rs.next()) { - assertEquals(rs.getLong(0), 1); - } - } - return null; - }); - validateIsolationLevel(IsolationLevel.SERIALIZABLE); + executeTestWithRR( + c -> MockSpannerTestActions.executeSelect1(c, SERIALIZABLE_ISOLATION_OPTION), + IsolationLevel.SERIALIZABLE); } @Test public void executeSqlWithDefaultSpannerOptions_RepeatableReadAndNoRWTransactionOptions() { - clientWithRepeatableReadOption - .readWriteTransaction() - .run( - transaction -> { - try (ResultSet rs = transaction.executeQuery(SELECT1)) { - while (rs.next()) { - assertEquals(rs.getLong(0), 1); - } - } - return null; - }); - validateIsolationLevel(IsolationLevel.REPEATABLE_READ); + executeTestWithRR(MockSpannerTestActions::executeSelect1, IsolationLevel.REPEATABLE_READ); } @Test public void executeSqlWithRWTransactionOptions_Serializable() { - client - .readWriteTransaction(Options.serializableIsolationLevel()) - .run( - transaction -> { - try (ResultSet rs = transaction.executeQuery(SELECT1)) { - while (rs.next()) { - assertEquals(rs.getLong(0), 1); - } - } - return null; - }); - validateIsolationLevel(IsolationLevel.SERIALIZABLE); + executeTest( + c -> MockSpannerTestActions.executeSelect1(c, SERIALIZABLE_ISOLATION_OPTION), + IsolationLevel.SERIALIZABLE); } @Test public void readWithRWTransactionOptions_RepeatableRead() { - client - .readWriteTransaction(Options.repeatableReadIsolationLevel()) - .run( - transaction -> { - try (ResultSet rs = - transaction.read("FOO", KeySet.all(), Collections.singletonList("ID"))) { - while (rs.next()) { - assertEquals(rs.getLong(0), 1); - } - } - return null; - }); - validateIsolationLevel(IsolationLevel.REPEATABLE_READ); + executeTest( + c -> MockSpannerTestActions.executeReadFoo(c, RR_ISOLATION_OPTION), + IsolationLevel.REPEATABLE_READ); } @Test public void readWithRWTransactionOptions_Serializable() { - client - .readWriteTransaction(Options.serializableIsolationLevel()) - .run( - transaction -> { - try (ResultSet rs = - transaction.read("FOO", KeySet.all(), Collections.singletonList("ID"))) { - while (rs.next()) { - assertEquals(rs.getLong(0), 1); - } - } - return null; - }); - validateIsolationLevel(IsolationLevel.SERIALIZABLE); + executeTest( + c -> MockSpannerTestActions.executeReadFoo(c, SERIALIZABLE_ISOLATION_OPTION), + IsolationLevel.SERIALIZABLE); } @Test public void beginTransactionWithRWTransactionOptions_RepeatableRead() { - client - .readWriteTransaction(Options.repeatableReadIsolationLevel()) - .run( - transaction -> { - try (ResultSet rs = transaction.executeQuery(INVALID_SELECT_STATEMENT)) { - SpannerException e = assertThrows(SpannerException.class, () -> rs.next()); - assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode()); - } - return transaction.executeUpdate(UPDATE_STATEMENT); - }); - validateIsolationLevel(IsolationLevel.REPEATABLE_READ); + executeTest( + c -> MockSpannerTestActions.executeInvalidAndValidSql(c, RR_ISOLATION_OPTION), + IsolationLevel.REPEATABLE_READ); } @Test public void beginTransactionWithRWTransactionOptions_Serializable() { - client - .readWriteTransaction(Options.serializableIsolationLevel()) - .run( - transaction -> { - try (ResultSet rs = transaction.executeQuery(INVALID_SELECT_STATEMENT)) { - SpannerException e = assertThrows(SpannerException.class, () -> rs.next()); - assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode()); - } - return transaction.executeUpdate(UPDATE_STATEMENT); - }); - validateIsolationLevel(IsolationLevel.SERIALIZABLE); + executeTest( + c -> MockSpannerTestActions.executeInvalidAndValidSql(c, SERIALIZABLE_ISOLATION_OPTION), + IsolationLevel.SERIALIZABLE); } private void validateIsolationLevel(IsolationLevel isolationLevel) { @@ -493,5 +347,6 @@ private void validateIsolationLevel(IsolationLevel isolationLevel) { break; } } + assertTrue("No gRPC call is made", foundMatchingRequest); } } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InlineBeginTransactionTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InlineBeginTransactionTest.java index 94b6de149c3..a70e2a7aa34 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InlineBeginTransactionTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InlineBeginTransactionTest.java @@ -590,19 +590,7 @@ public void testInlinedBeginFirstQueryReturnsUnavailable() { DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); mockSpanner.setExecuteStreamingSqlExecutionTime( SimulatedExecutionTime.ofStreamException(Status.UNAVAILABLE.asRuntimeException(), 0)); - long value = - client - .readWriteTransaction() - .run( - transaction -> { - // The first attempt will return UNAVAILABLE and retry internally. - try (ResultSet rs = transaction.executeQuery(SELECT1)) { - while (rs.next()) { - return rs.getLong(0); - } - } - return 0L; - }); + long value = MockSpannerTestActions.executeSelect1(client); assertThat(value).isEqualTo(1L); assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); assertThat(countRequests(ExecuteSqlRequest.class)).isEqualTo(2); @@ -614,20 +602,7 @@ public void testInlinedBeginFirstReadReturnsUnavailable() { DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); mockSpanner.setStreamingReadExecutionTime( SimulatedExecutionTime.ofStreamException(Status.UNAVAILABLE.asRuntimeException(), 0)); - Long value = - client - .readWriteTransaction() - .run( - transaction -> { - // The first attempt will return UNAVAILABLE and retry internally. - try (ResultSet rs = - transaction.read("FOO", KeySet.all(), Collections.singletonList("ID"))) { - while (rs.next()) { - return rs.getLong(0); - } - } - return 0L; - }); + Long value = MockSpannerTestActions.executeReadFoo(client); assertThat(value).isEqualTo(1L); assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); assertThat(countRequests(ReadRequest.class)).isEqualTo(2); @@ -641,22 +616,7 @@ public void testInlinedBeginFirstReadReturnsUnavailableRetryReturnsAborted() { SimulatedExecutionTime.ofExceptions( Arrays.asList( Status.UNAVAILABLE.asRuntimeException(), Status.ABORTED.asRuntimeException()))); - Long value = - client - .readWriteTransaction() - .run( - transaction -> { - // The first attempt will return UNAVAILABLE and retry internally. - // The second attempt will return ABORTED and should cause the transaction to - // retry. - try (ResultSet rs = - transaction.read("FOO", KeySet.all(), Collections.singletonList("ID"))) { - if (rs.next()) { - return rs.getLong(0); - } - } - return 0L; - }); + Long value = MockSpannerTestActions.executeReadFoo(client); assertThat(value).isEqualTo(1L); assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(1); assertThat(countRequests(ReadRequest.class)).isEqualTo(3); @@ -670,21 +630,7 @@ public void testInlinedBeginFirstQueryReturnsUnavailableRetryReturnsAborted() { SimulatedExecutionTime.ofExceptions( Arrays.asList( Status.UNAVAILABLE.asRuntimeException(), Status.ABORTED.asRuntimeException()))); - Long value = - client - .readWriteTransaction() - .run( - transaction -> { - // The first attempt will return UNAVAILABLE and retry internally. - // The second attempt will return ABORTED and should cause the transaction to - // retry. - try (ResultSet rs = transaction.executeQuery(SELECT1)) { - if (rs.next()) { - return rs.getLong(0); - } - } - return 0L; - }); + Long value = MockSpannerTestActions.executeSelect1(client); assertThat(value).isEqualTo(1L); assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(1); assertThat(countRequests(ExecuteSqlRequest.class)).isEqualTo(3); @@ -721,24 +667,7 @@ public void testInlinedBeginFirstReadReturnsUnavailableRetryReturnsAborted_WithC SimulatedExecutionTime.ofExceptions( Arrays.asList( Status.UNAVAILABLE.asRuntimeException(), Status.ABORTED.asRuntimeException()))); - Long value = - client - .readWriteTransaction() - .run( - transaction -> { - // The first attempt will return UNAVAILABLE and retry internally. - // The second attempt will return ABORTED and should cause the transaction to - // retry. - try (ResultSet rs = - transaction.read("FOO", KeySet.all(), Collections.singletonList("ID"))) { - if (rs.next()) { - return rs.getLong(0); - } - } catch (AbortedException e) { - // Ignore the AbortedException and let the commit handle it. - } - return 0L; - }); + Long value = MockSpannerTestActions.executeReadFoo(client); assertThat(value).isEqualTo(1L); assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(1); assertThat(countRequests(ReadRequest.class)).isEqualTo(3); @@ -780,23 +709,7 @@ public void testInlinedBeginFirstDmlReturnsUnavailableRetryReturnsAborted_WithCa SimulatedExecutionTime.ofExceptions( Arrays.asList( Status.UNAVAILABLE.asRuntimeException(), Status.ABORTED.asRuntimeException()))); - Long value = - client - .readWriteTransaction() - .run( - transaction -> { - // The first attempt will return UNAVAILABLE and retry internally. - // The second attempt will return ABORTED and should cause the transaction to - // retry. - try (ResultSet rs = transaction.executeQuery(SELECT1)) { - if (rs.next()) { - return rs.getLong(0); - } - } catch (AbortedException e) { - // Ignore the AbortedException and let the commit handle it. - } - return 0L; - }); + Long value = MockSpannerTestActions.executeSelect1(client); assertThat(value).isEqualTo(1L); assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(1); assertThat(countRequests(ExecuteSqlRequest.class)).isEqualTo(3); @@ -959,20 +872,7 @@ public void testInlinedBeginCommitAfterReadReturnsUnavailable() { DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); mockSpanner.setCommitExecutionTime( SimulatedExecutionTime.ofException(Status.UNAVAILABLE.asRuntimeException())); - Long value = - client - .readWriteTransaction() - .run( - transaction -> { - // The first attempt will return UNAVAILABLE and retry internally. - try (ResultSet rs = - transaction.read("FOO", KeySet.all(), Collections.singletonList("ID"))) { - if (rs.next()) { - return rs.getLong(0); - } - } - return 0L; - }); + Long value = MockSpannerTestActions.executeReadFoo(client); assertThat(value).isEqualTo(1L); assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); assertThat(countRequests(ReadRequest.class)).isEqualTo(1); @@ -1013,18 +913,7 @@ public void testInlinedBeginFirstReadReturnsUnavailableAndCommitAborts() { public void testInlinedBeginTxWithQuery() { DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); - long updateCount = - client - .readWriteTransaction() - .run( - transaction -> { - try (ResultSet rs = transaction.executeQuery(SELECT1)) { - while (rs.next()) { - return rs.getLong(0); - } - } - return 0L; - }); + long updateCount = MockSpannerTestActions.executeSelect1(client); assertThat(updateCount).isEqualTo(1L); assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); assertThat(countRequests(ExecuteSqlRequest.class)).isEqualTo(1); @@ -1035,19 +924,7 @@ public void testInlinedBeginTxWithQuery() { @Test public void testInlinedBeginTxWithRead() { DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); - long updateCount = - client - .readWriteTransaction() - .run( - transaction -> { - try (ResultSet rs = - transaction.read("FOO", KeySet.all(), Collections.singletonList("ID"))) { - while (rs.next()) { - return rs.getLong(0); - } - } - return 0L; - }); + long updateCount = MockSpannerTestActions.executeReadFoo(client); assertThat(updateCount).isEqualTo(1L); assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); assertThat(countRequests(ReadRequest.class)).isEqualTo(1); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerTestActions.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerTestActions.java new file mode 100644 index 00000000000..b7dbacff118 --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerTestActions.java @@ -0,0 +1,158 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.spanner; + +import static com.google.cloud.spanner.MockSpannerTestUtil.INVALID_SELECT_STATEMENT; +import static com.google.cloud.spanner.MockSpannerTestUtil.SELECT1; +import static com.google.cloud.spanner.MockSpannerTestUtil.UPDATE_STATEMENT; +import static com.google.cloud.spanner.SpannerApiFutures.get; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import com.google.api.core.ApiFutures; +import com.google.cloud.Timestamp; +import com.google.cloud.spanner.AsyncTransactionManager.TransactionContextFuture; +import com.google.cloud.spanner.Options.TransactionOption; +import java.util.Collections; +import java.util.concurrent.Executor; + +public class MockSpannerTestActions { + + static final Mutation TEST_MUTATION = + Mutation.newInsertBuilder("foo").set("id").to(1L).set("name").to("bar").build(); + + static Timestamp writeInsertMutation(DatabaseClient client) { + return client.write(Collections.singletonList(TEST_MUTATION)); + } + + static void writeInsertMutationWithOptions(DatabaseClient client, TransactionOption... options) { + client.writeWithOptions(Collections.singletonList(TEST_MUTATION), options); + } + + static Timestamp writeAtLeastOnceInsertMutation(DatabaseClient client) { + return client.writeAtLeastOnce(Collections.singletonList(TEST_MUTATION)); + } + + static void writeAtLeastOnceWithOptionsInsertMutation( + DatabaseClient client, TransactionOption... options) { + client.writeAtLeastOnceWithOptions(Collections.singletonList(TEST_MUTATION), options); + } + + static void executeBatchUpdateTransaction(DatabaseClient client, TransactionOption... options) { + client + .readWriteTransaction(options) + .run(transaction -> transaction.batchUpdate(Collections.singletonList(UPDATE_STATEMENT))); + } + + static void executePartitionedUpdate(DatabaseClient client) { + client.executePartitionedUpdate(UPDATE_STATEMENT); + } + + static void commitDeleteTransaction(DatabaseClient client, TransactionOption... options) { + client + .readWriteTransaction(options) + .run( + transaction -> { + transaction.buffer(Mutation.delete("TEST", KeySet.all())); + return null; + }); + } + + static void transactionManagerCommit(DatabaseClient client, TransactionOption... options) { + try (TransactionManager manager = client.transactionManager(options)) { + TransactionContext transaction = manager.begin(); + transaction.buffer(Mutation.delete("TEST", KeySet.all())); + manager.commit(); + } + } + + static void asyncRunnerCommit( + DatabaseClient client, Executor executor, TransactionOption... options) { + AsyncRunner runner = client.runAsync(options); + SpannerApiFutures.get( + runner.runAsync( + txn -> { + txn.buffer(Mutation.delete("TEST", KeySet.all())); + return ApiFutures.immediateFuture(null); + }, + executor)); + } + + static void transactionManagerAsyncCommit( + DatabaseClient client, Executor executor, TransactionOption... options) { + try (AsyncTransactionManager manager = client.transactionManagerAsync(options)) { + TransactionContextFuture transaction = manager.beginAsync(); + get( + transaction + .then( + (txn, input) -> { + txn.buffer(Mutation.delete("TEST", KeySet.all())); + return ApiFutures.immediateFuture(null); + }, + executor) + .commitAsync()); + } + } + + static Long executeSelect1(DatabaseClient client, TransactionOption... options) { + return client + .readWriteTransaction(options) + .run( + transaction -> { + try (ResultSet rs = transaction.executeQuery(SELECT1)) { + while (rs.next()) { + return rs.getLong(0); + } + } catch (AbortedException e) { + + } + return 0L; + }); + } + + static Long executeReadFoo(DatabaseClient client, TransactionOption... options) { + return client + .readWriteTransaction(options) + .run( + transaction -> { + try (ResultSet rs = + transaction.read("FOO", KeySet.all(), Collections.singletonList("ID"))) { + while (rs.next()) { + return rs.getLong(0); + } + } catch (AbortedException e) { + // Ignore the AbortedException and let the commit handle it. + } + return 0L; + }); + } + + static Long executeInvalidAndValidSql(DatabaseClient client, TransactionOption... options) { + return client + .readWriteTransaction(options) + .run( + transaction -> { + // This query carries the BeginTransaction, but fails. The BeginTransaction will + // then be carried by the subsequent statement. + try (ResultSet rs = transaction.executeQuery(INVALID_SELECT_STATEMENT)) { + SpannerException e = assertThrows(SpannerException.class, () -> rs.next()); + assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode()); + } + return transaction.executeUpdate(UPDATE_STATEMENT); + }); + } +} diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerTestUtil.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerTestUtil.java index 83bb1728ac0..e2e012f8ae0 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerTestUtil.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerTestUtil.java @@ -50,7 +50,8 @@ public class MockSpannerTestUtil { .setMetadata(SELECT1_METADATA) .build(); public static final Statement SELECT1_FROM_TABLE = Statement.of("SELECT 1 FROM FOO WHERE 1=1"); - + static final Statement INVALID_SELECT_STATEMENT = + Statement.of("SELECT * FROM NON_EXISTENT_TABLE"); static final String TEST_PROJECT = "my-project"; static final String TEST_INSTANCE = "my-instance"; static final String TEST_DATABASE = "my-database"; diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClientMockServerTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClientMockServerTest.java index c65bab64603..388539524fd 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClientMockServerTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClientMockServerTest.java @@ -345,10 +345,7 @@ public void testWriteAtLeastOnceAborted() { mockSpanner.setCommitExecutionTime( SimulatedExecutionTime.ofException( mockSpanner.createAbortedException(ByteString.copyFromUtf8("test")))); - Timestamp timestamp = - client.writeAtLeastOnce( - Collections.singletonList( - Mutation.newInsertBuilder("FOO").set("ID").to(1L).set("NAME").to("Bar").build())); + Timestamp timestamp = MockSpannerTestActions.writeAtLeastOnceInsertMutation(client); assertNotNull(timestamp); List commitRequests = mockSpanner.getRequestsOfType(CommitRequest.class); @@ -366,10 +363,7 @@ public void testWriteAtLeastOnceAborted() { public void testWriteAtLeastOnce() { DatabaseClientImpl client = (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); - Timestamp timestamp = - client.writeAtLeastOnce( - Collections.singletonList( - Mutation.newInsertBuilder("FOO").set("ID").to(1L).set("NAME").to("Bar").build())); + Timestamp timestamp = MockSpannerTestActions.writeAtLeastOnceInsertMutation(client); assertNotNull(timestamp); List commitRequests = mockSpanner.getRequestsOfType(CommitRequest.class); @@ -419,10 +413,8 @@ public void testWriteAtLeastOnceWithCommitStats() { public void testWriteAtLeastOnceWithOptions() { DatabaseClientImpl client = (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); - client.writeAtLeastOnceWithOptions( - Collections.singletonList( - Mutation.newInsertBuilder("FOO").set("ID").to(1L).set("NAME").to("Bar").build()), - Options.priority(RpcPriority.LOW)); + MockSpannerTestActions.writeAtLeastOnceWithOptionsInsertMutation( + client, Options.priority(RpcPriority.LOW)); List commitRequests = mockSpanner.getRequestsOfType(CommitRequest.class); assertThat(commitRequests).hasSize(1); @@ -443,10 +435,8 @@ public void testWriteAtLeastOnceWithOptions() { public void testWriteAtLeastOnceWithTagOptions() { DatabaseClientImpl client = (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); - client.writeAtLeastOnceWithOptions( - Collections.singletonList( - Mutation.newInsertBuilder("FOO").set("ID").to(1L).set("NAME").to("Bar").build()), - Options.tag("app=spanner,env=test")); + MockSpannerTestActions.writeAtLeastOnceWithOptionsInsertMutation( + client, Options.tag("app=spanner,env=test")); List commitRequests = mockSpanner.getRequestsOfType(CommitRequest.class); assertThat(commitRequests).hasSize(1); @@ -468,10 +458,8 @@ public void testWriteAtLeastOnceWithTagOptions() { public void testWriteAtLeastOnceWithExcludeTxnFromChangeStreams() { DatabaseClientImpl client = (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); - client.writeAtLeastOnceWithOptions( - Collections.singletonList( - Mutation.newInsertBuilder("FOO").set("ID").to(1L).set("NAME").to("Bar").build()), - Options.excludeTxnFromChangeStreams()); + MockSpannerTestActions.writeAtLeastOnceWithOptionsInsertMutation( + client, Options.excludeTxnFromChangeStreams()); List commitRequests = mockSpanner.getRequestsOfType(CommitRequest.class); assertThat(commitRequests).hasSize(1); @@ -585,10 +573,7 @@ public void testMutationUsingWrite() { mockSpanner.setCommitExecutionTime( SimulatedExecutionTime.ofException( mockSpanner.createAbortedException(ByteString.copyFromUtf8("test")))); - Timestamp timestamp = - client.write( - Collections.singletonList( - Mutation.newInsertBuilder("FOO").set("ID").to(1L).set("NAME").to("Bar").build())); + Timestamp timestamp = MockSpannerTestActions.writeInsertMutation(client); assertNotNull(timestamp); List beginTransactionRequests = @@ -1223,15 +1208,7 @@ public void testMutationOnlyUsingAsyncRunner() { // Test verifies mutation-only case within a R/W transaction via AsyncRunner. DatabaseClientImpl client = (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); - AsyncRunner runner = client.runAsync(); - get( - runner.runAsync( - txn -> { - txn.buffer(Mutation.delete("TEST", KeySet.all())); - return ApiFutures.immediateFuture(null); - }, - MoreExecutors.directExecutor())); - + MockSpannerTestActions.asyncRunnerCommit(client, MoreExecutors.directExecutor()); // Verify that the mutation key is set in BeginTransactionRequest List beginTransactions = mockSpanner.getRequestsOfType(BeginTransactionRequest.class); @@ -1255,18 +1232,7 @@ public void testMutationOnlyUsingAsyncTransactionManager() { // Test verifies mutation-only case within a R/W transaction via AsyncTransactionManager. DatabaseClientImpl client = (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); - try (AsyncTransactionManager manager = client.transactionManagerAsync()) { - TransactionContextFuture transaction = manager.beginAsync(); - get( - transaction - .then( - (txn, input) -> { - txn.buffer(Mutation.delete("TEST", KeySet.all())); - return ApiFutures.immediateFuture(null); - }, - MoreExecutors.directExecutor()) - .commitAsync()); - } + MockSpannerTestActions.transactionManagerAsyncCommit(client, MoreExecutors.directExecutor()); // Verify that the mutation key is set in BeginTransactionRequest List beginTransactions = diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/OptionsTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/OptionsTest.java index 52c771e330e..9a0ba5e7aa9 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/OptionsTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/OptionsTest.java @@ -17,7 +17,6 @@ package com.google.cloud.spanner; import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; @@ -37,6 +36,8 @@ 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; +import com.google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -380,7 +381,9 @@ public void testTransactionOptionsPriority() { @Test public void testTransactionOptionsIsolationLevel() { - Options options = Options.fromTransactionOptions(Options.repeatableReadIsolationLevel()); + Options options = + Options.fromTransactionOptions( + Options.isolationLevelOption(IsolationLevel.REPEATABLE_READ)); assertEquals(options.isolationLevel(), IsolationLevel.REPEATABLE_READ); assertEquals( "isolationLevel: " + IsolationLevel.REPEATABLE_READ.name() + " ", options.toString()); @@ -785,8 +788,12 @@ public void transactionOptionsExcludeTxnFromChangeStreams() { @Test public void transactionOptionsIsolationLevel() { - Options option1 = Options.fromTransactionOptions(Options.repeatableReadIsolationLevel()); - Options option2 = Options.fromTransactionOptions(Options.repeatableReadIsolationLevel()); + Options option1 = + Options.fromTransactionOptions( + Options.isolationLevelOption(IsolationLevel.REPEATABLE_READ)); + Options option2 = + Options.fromTransactionOptions( + Options.isolationLevelOption(IsolationLevel.REPEATABLE_READ)); Options option3 = Options.fromTransactionOptions(); assertEquals(option1, option2); @@ -839,43 +846,32 @@ public void testLastStatement() { assertThat(option3.toString()).doesNotContain("lastStatement: true"); } - @Test - public void testTransactionOptionCombineMutuallyExclusiveOptions() { - TransactionOption priorityOption = Options.priority(RpcPriority.HIGH); - TransactionOption[] primaryOptions = {Options.commitStats(), priorityOption}; - TransactionOption[] spannerOptions = {Options.repeatableReadIsolationLevel()}; - assertArrayEquals( - TransactionOption.combine(primaryOptions, spannerOptions), - new TransactionOption[] { - Options.commitStats(), priorityOption, Options.repeatableReadIsolationLevel() - }); - } - - @Test - public void testTransactionOptionCombine_PrimaryOptionWithIsolationLevel() { - TransactionOption[] primaryOptions = { - Options.commitStats(), Options.serializableIsolationLevel() - }; - TransactionOption[] spannerOptions = {Options.repeatableReadIsolationLevel()}; - assertArrayEquals( - TransactionOption.combine(primaryOptions, spannerOptions), - new TransactionOption[] {Options.commitStats(), Options.serializableIsolationLevel()}); - } - @Test public void testTransactionOptionCombine_WithNoSpannerOptions() { - TransactionOption[] primaryOptions = { - Options.commitStats(), Options.serializableIsolationLevel() - }; - assertArrayEquals( - TransactionOption.combine(primaryOptions, null), - new TransactionOption[] {Options.commitStats(), Options.serializableIsolationLevel()}); + com.google.spanner.v1.TransactionOptions primaryOptions = + com.google.spanner.v1.TransactionOptions.newBuilder() + .setIsolationLevel(IsolationLevel.SERIALIZABLE) + .setExcludeTxnFromChangeStreams(true) + .setReadWrite(ReadWrite.newBuilder().setReadLockMode(ReadLockMode.PESSIMISTIC)) + .build(); + com.google.spanner.v1.TransactionOptions spannerOptions = + com.google.spanner.v1.TransactionOptions.newBuilder() + .setIsolationLevel(IsolationLevel.REPEATABLE_READ) + .build(); + com.google.spanner.v1.TransactionOptions combinedOptions = + spannerOptions.toBuilder().mergeFrom(primaryOptions).build(); + assertEquals(combinedOptions.getIsolationLevel(), IsolationLevel.SERIALIZABLE); + assertTrue(combinedOptions.getExcludeTxnFromChangeStreams()); + assertEquals( + combinedOptions.getReadWrite(), + ReadWrite.newBuilder().setReadLockMode(ReadLockMode.PESSIMISTIC).build()); } @Test public void testOptions_WithMultipleDifferentIsolationLevels() { TransactionOption[] transactionOptions = { - Options.repeatableReadIsolationLevel(), Options.serializableIsolationLevel() + Options.isolationLevelOption(IsolationLevel.REPEATABLE_READ), + Options.isolationLevelOption(IsolationLevel.SERIALIZABLE) }; Options options = Options.fromTransactionOptions(transactionOptions); assertEquals(options.isolationLevel(), IsolationLevel.SERIALIZABLE); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReadWriteTransactionWithInlineBeginTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReadWriteTransactionWithInlineBeginTest.java index 225bee86347..aa268382b34 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReadWriteTransactionWithInlineBeginTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReadWriteTransactionWithInlineBeginTest.java @@ -180,18 +180,7 @@ public void singleBatchUpdate() { @Test public void singleQuery() { - Long value = - client - .readWriteTransaction() - .run( - transaction -> { - try (ResultSet rs = transaction.executeQuery(SELECT1)) { - while (rs.next()) { - return rs.getLong(0); - } - } - return 0L; - }); + Long value = MockSpannerTestActions.executeSelect1(client); assertThat(value).isEqualTo(1L); assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); assertThat(countTransactionsStarted()).isEqualTo(1); @@ -406,17 +395,7 @@ public void failedBatchUpdateAndThenUpdate() { @Test public void executeSqlWithOptimisticConcurrencyControl() { - client - .readWriteTransaction(Options.optimisticLock()) - .run( - transaction -> { - try (ResultSet rs = transaction.executeQuery(SELECT1)) { - while (rs.next()) { - assertEquals(rs.getLong(0), 1); - } - } - return null; - }); + MockSpannerTestActions.executeSelect1(client, Options.optimisticLock()); Collection requests = mockSpanner.getRequests().stream() .filter(msg -> msg.getClass().equals(ExecuteSqlRequest.class)) @@ -428,18 +407,8 @@ public void executeSqlWithOptimisticConcurrencyControl() { @Test public void readWithOptimisticConcurrencyControl() { - client - .readWriteTransaction(Options.optimisticLock()) - .run( - transaction -> { - try (ResultSet rs = - transaction.read("FOO", KeySet.all(), Collections.singletonList("ID"))) { - while (rs.next()) { - assertEquals(rs.getLong(0), 1); - } - } - return null; - }); + Long updateCount = MockSpannerTestActions.executeReadFoo(client, Options.optimisticLock()); + assertThat(updateCount).isEqualTo(1L); Collection requests = mockSpanner.getRequests().stream() .filter(msg -> msg.getClass().equals(ReadRequest.class)) @@ -451,20 +420,7 @@ public void readWithOptimisticConcurrencyControl() { @Test public void beginTransactionWithOptimisticConcurrencyControl() { - client - .readWriteTransaction(Options.optimisticLock()) - .run( - transaction -> { - // Instead of adding the BeginTransaction option to the next statement, the client - // library will force a complete retry of the entire transaction, and use an explicit - // BeginTransaction RPC invocation for that transaction in order to include the failed - // statement in the transaction as well. - try (ResultSet rs = transaction.executeQuery(INVALID_SELECT_STATEMENT)) { - SpannerException e = assertThrows(SpannerException.class, () -> rs.next()); - assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode()); - } - return transaction.executeUpdate(UPDATE_STATEMENT); - }); + MockSpannerTestActions.executeInvalidAndValidSql(client, Options.optimisticLock()); Collection requests = mockSpanner.getRequests().stream() .filter(msg -> msg.getClass().equals(BeginTransactionRequest.class)) @@ -476,19 +432,7 @@ public void beginTransactionWithOptimisticConcurrencyControl() { @Test public void failedQueryAndThenUpdate() { - Long updateCount = - client - .readWriteTransaction() - .run( - transaction -> { - // This query carries the BeginTransaction, but fails. The BeginTransaction will - // then be carried by the subsequent statement. - try (ResultSet rs = transaction.executeQuery(INVALID_SELECT_STATEMENT)) { - SpannerException e = assertThrows(SpannerException.class, () -> rs.next()); - assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode()); - } - return transaction.executeUpdate(UPDATE_STATEMENT); - }); + Long updateCount = MockSpannerTestActions.executeInvalidAndValidSql(client); assertThat(updateCount).isEqualTo(1L); assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(1); assertThat(countTransactionsStarted()).isEqualTo(2); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolTest.java index 4249f05c17f..4538be784b1 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolTest.java @@ -88,6 +88,7 @@ import com.google.spanner.v1.ResultSetStats; import com.google.spanner.v1.RollbackRequest; import com.google.spanner.v1.Transaction; +import com.google.spanner.v1.TransactionOptions; import io.opencensus.metrics.LabelValue; import io.opencensus.metrics.MetricRegistry; import io.opencensus.metrics.Metrics; @@ -1478,6 +1479,8 @@ public void testSessionNotFoundReadWriteTransaction() { .thenReturn( SpannerStubSettings.newBuilder().executeStreamingSqlSettings().getRetryableCodes()); final SessionImpl closedSession = mock(SessionImpl.class); + when(closedSession.defaultTransactionOptions()) + .thenReturn(TransactionOptions.getDefaultInstance()); when(closedSession.getName()) .thenReturn("projects/dummy/instances/dummy/database/dummy/sessions/session-closed"); when(closedSession.getErrorHandler()).thenReturn(DefaultErrorHandler.INSTANCE); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerOptionsTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerOptionsTest.java index 6e8eab467cd..d2089f1fc01 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerOptionsTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerOptionsTest.java @@ -37,7 +37,6 @@ import com.google.cloud.NoCredentials; import com.google.cloud.ServiceOptions; import com.google.cloud.TransportOptions; -import com.google.cloud.spanner.Options.TransactionOption; import com.google.cloud.spanner.SpannerOptions.Builder.TransactionOptions; import com.google.cloud.spanner.SpannerOptions.Builder.TransactionOptions.TransactionOptionsBuilder; import com.google.cloud.spanner.SpannerOptions.FixedCloseableExecutorProvider; @@ -64,6 +63,7 @@ import com.google.spanner.v1.ReadRequest; import com.google.spanner.v1.RollbackRequest; import com.google.spanner.v1.SpannerGrpc; +import com.google.spanner.v1.TransactionOptions.IsolationLevel; import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.sdk.OpenTelemetrySdk; @@ -774,29 +774,17 @@ public void testMonitoringHost() { public void testTransactionOptions() { TransactionOptions transactionOptions = TransactionOptionsBuilder.newBuilder() - .setIsolationLevel(Options.serializableIsolationLevel()) + .setIsolationLevel(Options.isolationLevelOption(IsolationLevel.SERIALIZABLE)) .build(); - assertNull(SpannerOptions.newBuilder().setProjectId("p").build().getTransactionOptions()); + assertNull( + SpannerOptions.newBuilder().setProjectId("p").build().getDefaultTransactionOptions()); assertThat( SpannerOptions.newBuilder() .setProjectId("p") .setDefaultTransactionOptions(transactionOptions) .build() - .getTransactionOptions()) - .isEqualTo(new TransactionOption[] {Options.serializableIsolationLevel()}); - } - - @Test - public void testTransactionOptionsWithError() { - assertNull(SpannerOptions.newBuilder().setProjectId("p").build().getTransactionOptions()); - SpannerException e = - assertThrows( - SpannerException.class, - () -> - TransactionOptionsBuilder.newBuilder() - .setIsolationLevel(Options.commitStats()) - .build()); - assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode()); + .getDefaultTransactionOptions()) + .isEqualTo(transactionOptions); } @Test From 97bcd9f8c8aee789a0deeedc87d435d887b10b43 Mon Sep 17 00:00:00 2001 From: Shobhit Gupta Date: Tue, 11 Mar 2025 19:23:28 +0530 Subject: [PATCH 3/8] feat: addressed review comments and added missing javadoc --- .../com/google/cloud/spanner/Options.java | 2 +- .../com/google/cloud/spanner/SessionImpl.java | 29 +++----- .../google/cloud/spanner/SpannerOptions.java | 72 +++++++++++-------- ...lWithDefaultRWTransactionOptionsTest.java} | 38 +++++----- .../google/cloud/spanner/SessionImplTest.java | 3 + .../cloud/spanner/SpannerOptionsTest.java | 16 ++--- .../spanner/TransactionManagerImplTest.java | 5 ++ .../spanner/TransactionRunnerImplTest.java | 6 +- 8 files changed, 96 insertions(+), 75 deletions(-) rename google-cloud-spanner/src/test/java/com/google/cloud/spanner/{DatabaseClientImplWithTransactionOptionsTest.java => DatabaseClientImplWithDefaultRWTransactionOptionsTest.java} (90%) diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Options.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Options.java index 2b2f64a6aae..344f1cfbb98 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Options.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Options.java @@ -163,7 +163,7 @@ public static TransactionOption optimisticLock() { /** * Specifying this instructs the transaction to request {@link IsolationLevel} from the backend. */ - public static IsolationLevelOption isolationLevelOption(IsolationLevel isolationLevel) { + public static TransactionOption isolationLevelOption(IsolationLevel isolationLevel) { return new IsolationLevelOption(isolationLevel); } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionImpl.java index fa1bbf9fea3..3da558bf8a9 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionImpl.java @@ -30,7 +30,6 @@ import com.google.cloud.spanner.Options.TransactionOption; import com.google.cloud.spanner.Options.UpdateOption; import com.google.cloud.spanner.SessionClient.SessionOption; -import com.google.cloud.spanner.SpannerOptions.Builder.TransactionOptions; import com.google.cloud.spanner.TransactionRunnerImpl.TransactionContextImpl; import com.google.cloud.spanner.spi.v1.SpannerRpc; import com.google.common.base.Ticker; @@ -45,6 +44,7 @@ import com.google.spanner.v1.CommitRequest; import com.google.spanner.v1.RequestOptions; import com.google.spanner.v1.Transaction; +import com.google.spanner.v1.TransactionOptions; import java.time.Instant; import java.util.ArrayList; import java.util.Collection; @@ -69,18 +69,15 @@ static void throwIfTransactionsPending() { } } - static com.google.spanner.v1.TransactionOptions createReadWriteTransactionOptions( + static TransactionOptions createReadWriteTransactionOptions( Options options, ByteString previousTransactionId) { - com.google.spanner.v1.TransactionOptions.Builder transactionOptions = - com.google.spanner.v1.TransactionOptions.newBuilder(); + TransactionOptions.Builder transactionOptions = TransactionOptions.newBuilder(); if (options.withExcludeTxnFromChangeStreams() == Boolean.TRUE) { transactionOptions.setExcludeTxnFromChangeStreams(true); } - com.google.spanner.v1.TransactionOptions.ReadWrite.Builder readWrite = - com.google.spanner.v1.TransactionOptions.ReadWrite.newBuilder(); + TransactionOptions.ReadWrite.Builder readWrite = TransactionOptions.ReadWrite.newBuilder(); if (options.withOptimisticLock() == Boolean.TRUE) { - readWrite.setReadLockMode( - com.google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode.OPTIMISTIC); + readWrite.setReadLockMode(TransactionOptions.ReadWrite.ReadLockMode.OPTIMISTIC); } if (previousTransactionId != null && previousTransactionId != com.google.protobuf.ByteString.EMPTY) { @@ -199,12 +196,8 @@ void markUsed(Instant instant) { sessionReference.markUsed(instant); } - com.google.spanner.v1.TransactionOptions defaultTransactionOptions() { - TransactionOptions transactionOptions = - this.spanner.getOptions().getDefaultTransactionOptions(); - return transactionOptions != null - ? transactionOptions.getTransactionOptions() - : com.google.spanner.v1.TransactionOptions.getDefaultInstance(); + TransactionOptions defaultTransactionOptions() { + return this.spanner.getOptions().getDefaultTransactionOptions(); } public DatabaseId getDatabaseId() { @@ -260,9 +253,9 @@ public CommitResponse writeAtLeastOnceWithOptions( .setReturnCommitStats(options.withCommitStats()) .addAllMutations(mutationsProto); - com.google.spanner.v1.TransactionOptions.Builder transactionOptionsBuilder = - com.google.spanner.v1.TransactionOptions.newBuilder() - .setReadWrite(com.google.spanner.v1.TransactionOptions.ReadWrite.getDefaultInstance()); + TransactionOptions.Builder transactionOptionsBuilder = + TransactionOptions.newBuilder() + .setReadWrite(TransactionOptions.ReadWrite.getDefaultInstance()); if (options.withExcludeTxnFromChangeStreams() == Boolean.TRUE) { transactionOptionsBuilder.setExcludeTxnFromChangeStreams(true); } @@ -270,7 +263,7 @@ public CommitResponse writeAtLeastOnceWithOptions( transactionOptionsBuilder.setIsolationLevel(options.isolationLevel()); } requestBuilder.setSingleUseTransaction( - this.defaultTransactionOptions().toBuilder().mergeFrom(transactionOptionsBuilder.build())); + defaultTransactionOptions().toBuilder().mergeFrom(transactionOptionsBuilder.build())); if (options.hasMaxCommitDelay()) { requestBuilder.setMaxCommitDelay( diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java index 6d5c2460e2d..515e7debe6b 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java @@ -44,10 +44,8 @@ import com.google.cloud.grpc.GcpManagedChannelOptions; import com.google.cloud.grpc.GrpcTransportOptions; import com.google.cloud.spanner.Options.DirectedReadOption; -import com.google.cloud.spanner.Options.IsolationLevelOption; import com.google.cloud.spanner.Options.QueryOption; import com.google.cloud.spanner.Options.UpdateOption; -import com.google.cloud.spanner.SpannerOptions.Builder.TransactionOptions; import com.google.cloud.spanner.admin.database.v1.DatabaseAdminSettings; import com.google.cloud.spanner.admin.database.v1.stub.DatabaseAdminStubSettings; import com.google.cloud.spanner.admin.instance.v1.InstanceAdminSettings; @@ -68,6 +66,8 @@ import com.google.spanner.v1.ExecuteSqlRequest; import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions; import com.google.spanner.v1.SpannerGrpc; +import com.google.spanner.v1.TransactionOptions; +import com.google.spanner.v1.TransactionOptions.IsolationLevel; import io.grpc.CallCredentials; import io.grpc.CompressorRegistry; import io.grpc.Context; @@ -180,7 +180,7 @@ public class SpannerOptions extends ServiceOptions { private final boolean enableExtendedTracing; private final boolean enableEndToEndTracing; private final String monitoringHost; - private final TransactionOptions transactionOptions; + private final TransactionOptions defaultTransactionOptions; enum TracingFramework { OPEN_CENSUS, @@ -810,7 +810,7 @@ protected SpannerOptions(Builder builder) { enableBuiltInMetrics = builder.enableBuiltInMetrics; enableEndToEndTracing = builder.enableEndToEndTracing; monitoringHost = builder.monitoringHost; - transactionOptions = builder.transactionOptions; + defaultTransactionOptions = builder.defaultTransactionOptions; } /** @@ -992,7 +992,7 @@ public static class Builder private String monitoringHost = SpannerOptions.environment.getMonitoringHost(); private SslContext mTLSContext = null; private boolean isExperimentalHost = false; - private TransactionOptions transactionOptions; + private TransactionOptions defaultTransactionOptions = TransactionOptions.getDefaultInstance(); private static String createCustomClientLibToken(String token) { return token + " " + ServiceOptions.getGoogApiClientLibName(); @@ -1061,7 +1061,7 @@ protected Builder() { this.enableBuiltInMetrics = options.enableBuiltInMetrics; this.enableEndToEndTracing = options.enableEndToEndTracing; this.monitoringHost = options.monitoringHost; - this.transactionOptions = options.transactionOptions; + this.defaultTransactionOptions = options.defaultTransactionOptions; } @Override @@ -1651,42 +1651,52 @@ public Builder setEnableEndToEndTracing(boolean enableEndToEndTracing) { return this; } - public static class TransactionOptions { - private com.google.spanner.v1.TransactionOptions transactionOptions; - - private TransactionOptions() {} + /** + * Provides the default read-write transaction options for all databases, limited to setting the + * {@link IsolationLevel}. These defaults are overridden by any explicit {@link + * com.google.cloud.spanner.Options.TransactionOption} provided through {@link DatabaseClient}. + * + *

    Example Usage: + * + *

    {@code
    +     * DefaultReadWriteTransactionOptions options = DefaultReadWriteTransactionOptions.newBuilder()
    +     * .setIsolationLevel(IsolationLevel.SERIALIZABLE)
    +     * .build();
    +     * }
    + */ + public static class DefaultReadWriteTransactionOptions { + private final TransactionOptions defaultTransactionOptions; - com.google.spanner.v1.TransactionOptions getTransactionOptions() { - return transactionOptions; + private DefaultReadWriteTransactionOptions(TransactionOptions defaultTransactionOptions) { + this.defaultTransactionOptions = defaultTransactionOptions; } - public static class TransactionOptionsBuilder { - private IsolationLevelOption isolationLevelOption; + public static DefaultReadWriteTransactionOptionsBuilder newBuilder() { + return new DefaultReadWriteTransactionOptionsBuilder(); + } - public static TransactionOptionsBuilder newBuilder() { - return new TransactionOptionsBuilder(); - } + public static class DefaultReadWriteTransactionOptionsBuilder { + private final TransactionOptions.Builder transactionOptionsBuilder = + TransactionOptions.newBuilder(); - public TransactionOptionsBuilder setIsolationLevel(IsolationLevelOption option) { - this.isolationLevelOption = option; + public DefaultReadWriteTransactionOptionsBuilder setIsolationLevel( + IsolationLevel isolationLevel) { + transactionOptionsBuilder.setIsolationLevel(isolationLevel); return this; } - public TransactionOptions build() { - TransactionOptions transactionOptions = new TransactionOptions(); - transactionOptions.transactionOptions = - com.google.spanner.v1.TransactionOptions.newBuilder() - .setIsolationLevel( - Options.fromTransactionOptions(isolationLevelOption).isolationLevel()) - .build(); - return transactionOptions; + public DefaultReadWriteTransactionOptions build() { + return new DefaultReadWriteTransactionOptions(transactionOptionsBuilder.build()); } } } - /** Sets the default transaction options. */ - public Builder setDefaultTransactionOptions(TransactionOptions transactionOptions) { - this.transactionOptions = transactionOptions; + /** Sets the {@link DefaultReadWriteTransactionOptions} for read-write transactions. */ + public Builder setDefaultTransactionOptions( + DefaultReadWriteTransactionOptions defaultReadWriteTransactionOptions) { + Preconditions.checkNotNull( + defaultReadWriteTransactionOptions, "DefaultReadWriteTransactionOptions cannot be null"); + this.defaultTransactionOptions = defaultReadWriteTransactionOptions.defaultTransactionOptions; return this; } @@ -2036,7 +2046,7 @@ String getMonitoringHost() { } public TransactionOptions getDefaultTransactionOptions() { - return transactionOptions; + return defaultTransactionOptions; } @BetaApi diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplWithTransactionOptionsTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplWithDefaultRWTransactionOptionsTest.java similarity index 90% rename from google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplWithTransactionOptionsTest.java rename to google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplWithDefaultRWTransactionOptionsTest.java index 54809ee7098..827e8bd64bc 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplWithTransactionOptionsTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplWithDefaultRWTransactionOptionsTest.java @@ -27,9 +27,9 @@ import com.google.api.gax.grpc.testing.LocalChannelProvider; import com.google.cloud.NoCredentials; import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult; -import com.google.cloud.spanner.Options.IsolationLevelOption; import com.google.cloud.spanner.Options.RpcPriority; -import com.google.cloud.spanner.SpannerOptions.Builder.TransactionOptions.TransactionOptionsBuilder; +import com.google.cloud.spanner.Options.TransactionOption; +import com.google.cloud.spanner.SpannerOptions.Builder.DefaultReadWriteTransactionOptions; import com.google.protobuf.AbstractMessage; import com.google.spanner.v1.BeginTransactionRequest; import com.google.spanner.v1.CommitRequest; @@ -55,16 +55,20 @@ import org.junit.runners.JUnit4; @RunWith(JUnit4.class) -public class DatabaseClientImplWithTransactionOptionsTest { - private static final IsolationLevelOption SERIALIZABLE_ISOLATION_OPTION = +public class DatabaseClientImplWithDefaultRWTransactionOptionsTest { + private static final TransactionOption SERIALIZABLE_ISOLATION_OPTION = Options.isolationLevelOption(IsolationLevel.SERIALIZABLE); - private static final IsolationLevelOption RR_ISOLATION_OPTION = + private static final TransactionOption RR_ISOLATION_OPTION = Options.isolationLevelOption(IsolationLevel.REPEATABLE_READ); + private static final DatabaseId DATABASE_ID = + DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]"); private static MockSpannerServiceImpl mockSpanner; private static Server server; private static ExecutorService executor; private static LocalChannelProvider channelProvider; private Spanner spanner; + private Spanner spannerWithRR; + private Spanner spannerWithSerializable; private DatabaseClient client; private DatabaseClient clientWithRepeatableReadOption; private DatabaseClient clientWithSerializableOption; @@ -111,25 +115,25 @@ public void setUp() { .setChannelProvider(channelProvider) .setCredentials(NoCredentials.getInstance()); spanner = spannerOptionsBuilder.build().getService(); - client = spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); - clientWithRepeatableReadOption = + spannerWithRR = spannerOptionsBuilder .setDefaultTransactionOptions( - TransactionOptionsBuilder.newBuilder() - .setIsolationLevel(RR_ISOLATION_OPTION) + DefaultReadWriteTransactionOptions.newBuilder() + .setIsolationLevel(IsolationLevel.REPEATABLE_READ) .build()) .build() - .getService() - .getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); - clientWithSerializableOption = + .getService(); + spannerWithSerializable = spannerOptionsBuilder .setDefaultTransactionOptions( - TransactionOptionsBuilder.newBuilder() - .setIsolationLevel(SERIALIZABLE_ISOLATION_OPTION) + DefaultReadWriteTransactionOptions.newBuilder() + .setIsolationLevel(IsolationLevel.SERIALIZABLE) .build()) .build() - .getService() - .getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + .getService(); + client = spanner.getDatabaseClient(DATABASE_ID); + clientWithRepeatableReadOption = spannerWithRR.getDatabaseClient(DATABASE_ID); + clientWithSerializableOption = spannerWithSerializable.getDatabaseClient(DATABASE_ID); } private void executeTest( @@ -153,6 +157,8 @@ private void executeTestWithSerializable( @After public void tearDown() { spanner.close(); + spannerWithRR.close(); + spannerWithSerializable.close(); } @Test diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionImplTest.java index 2a850514d0d..e0403f72d1c 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionImplTest.java @@ -48,6 +48,7 @@ import com.google.spanner.v1.RollbackRequest; import com.google.spanner.v1.Session; import com.google.spanner.v1.Transaction; +import com.google.spanner.v1.TransactionOptions; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.trace.Span; import io.opentelemetry.context.Scope; @@ -90,6 +91,8 @@ public static void setupOpenTelemetry() { public void setUp() { MockitoAnnotations.initMocks(this); when(spannerOptions.getNumChannels()).thenReturn(4); + when(spannerOptions.getDefaultTransactionOptions()) + .thenReturn(TransactionOptions.getDefaultInstance()); when(spannerOptions.getPrefetchChunks()).thenReturn(1); when(spannerOptions.getDatabaseRole()).thenReturn("role"); when(spannerOptions.getRetrySettings()).thenReturn(RetrySettings.newBuilder().build()); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerOptionsTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerOptionsTest.java index d2089f1fc01..9fc065f944c 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerOptionsTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerOptionsTest.java @@ -37,8 +37,7 @@ import com.google.cloud.NoCredentials; import com.google.cloud.ServiceOptions; import com.google.cloud.TransportOptions; -import com.google.cloud.spanner.SpannerOptions.Builder.TransactionOptions; -import com.google.cloud.spanner.SpannerOptions.Builder.TransactionOptions.TransactionOptionsBuilder; +import com.google.cloud.spanner.SpannerOptions.Builder.DefaultReadWriteTransactionOptions; import com.google.cloud.spanner.SpannerOptions.FixedCloseableExecutorProvider; import com.google.cloud.spanner.SpannerOptions.SpannerCallContextTimeoutConfigurator; import com.google.cloud.spanner.admin.database.v1.stub.DatabaseAdminStubSettings; @@ -772,19 +771,20 @@ public void testMonitoringHost() { @Test public void testTransactionOptions() { - TransactionOptions transactionOptions = - TransactionOptionsBuilder.newBuilder() - .setIsolationLevel(Options.isolationLevelOption(IsolationLevel.SERIALIZABLE)) + DefaultReadWriteTransactionOptions transactionOptions = + DefaultReadWriteTransactionOptions.newBuilder() + .setIsolationLevel(IsolationLevel.SERIALIZABLE) .build(); - assertNull( + assertNotNull( SpannerOptions.newBuilder().setProjectId("p").build().getDefaultTransactionOptions()); assertThat( SpannerOptions.newBuilder() .setProjectId("p") .setDefaultTransactionOptions(transactionOptions) .build() - .getDefaultTransactionOptions()) - .isEqualTo(transactionOptions); + .getDefaultTransactionOptions() + .getIsolationLevel()) + .isEqualTo(IsolationLevel.SERIALIZABLE); } @Test diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerImplTest.java index aee3d5ed5b4..547f6b70a22 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerImplTest.java @@ -49,6 +49,7 @@ import com.google.spanner.v1.ResultSetStats; import com.google.spanner.v1.Session; import com.google.spanner.v1.Transaction; +import com.google.spanner.v1.TransactionOptions; import io.opentelemetry.api.OpenTelemetry; import java.util.Collections; import java.util.UUID; @@ -207,6 +208,8 @@ public void commitAfterRollbackFails() { public void usesPreparedTransaction() { SpannerOptions options = mock(SpannerOptions.class); when(options.getNumChannels()).thenReturn(4); + when(options.getDefaultTransactionOptions()) + .thenReturn(TransactionOptions.getDefaultInstance()); GrpcTransportOptions transportOptions = mock(GrpcTransportOptions.class); when(transportOptions.getExecutorFactory()).thenReturn(new TestExecutorFactory()); when(options.getTransportOptions()).thenReturn(transportOptions); @@ -288,6 +291,8 @@ public void inlineBegin() { when(options.getNumChannels()).thenReturn(4); GrpcTransportOptions transportOptions = mock(GrpcTransportOptions.class); when(transportOptions.getExecutorFactory()).thenReturn(new TestExecutorFactory()); + when(options.getDefaultTransactionOptions()) + .thenReturn(TransactionOptions.getDefaultInstance()); when(options.getTransportOptions()).thenReturn(transportOptions); SessionPoolOptions sessionPoolOptions = SessionPoolOptions.newBuilder().setMinSessions(0).setIncStep(1).build(); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionRunnerImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionRunnerImplTest.java index d8bd6ed448d..3068b38f3ef 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionRunnerImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionRunnerImplTest.java @@ -58,6 +58,7 @@ import com.google.spanner.v1.RollbackRequest; import com.google.spanner.v1.Session; import com.google.spanner.v1.Transaction; +import com.google.spanner.v1.TransactionOptions; import io.grpc.Metadata; import io.grpc.Status; import io.grpc.StatusRuntimeException; @@ -160,6 +161,8 @@ public void setUp() { public void usesPreparedTransaction() { SpannerOptions options = mock(SpannerOptions.class); when(options.getNumChannels()).thenReturn(4); + when(options.getDefaultTransactionOptions()) + .thenReturn(TransactionOptions.getDefaultInstance()); GrpcTransportOptions transportOptions = mock(GrpcTransportOptions.class); when(transportOptions.getExecutorFactory()).thenReturn(new TestExecutorFactory()); when(options.getTransportOptions()).thenReturn(transportOptions); @@ -316,7 +319,8 @@ public void batchDmlFailedPrecondition() { public void inlineBegin() { SpannerImpl spanner = mock(SpannerImpl.class); SpannerOptions options = mock(SpannerOptions.class); - + when(options.getDefaultTransactionOptions()) + .thenReturn(TransactionOptions.getDefaultInstance()); when(spanner.getRpc()).thenReturn(rpc); when(spanner.getDefaultQueryOptions(Mockito.any(DatabaseId.class))) .thenReturn(QueryOptions.getDefaultInstance()); From 2df96f925b7f1928c9df458fea4e670c341f3b5d Mon Sep 17 00:00:00 2001 From: Shobhit Date: Tue, 11 Mar 2025 20:07:04 +0530 Subject: [PATCH 4/8] Update google-cloud-spanner/src/main/java/com/google/cloud/spanner/Options.java MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Knut Olav Løite --- .../src/main/java/com/google/cloud/spanner/Options.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Options.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Options.java index 344f1cfbb98..c36f1902648 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Options.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Options.java @@ -163,7 +163,7 @@ public static TransactionOption optimisticLock() { /** * Specifying this instructs the transaction to request {@link IsolationLevel} from the backend. */ - public static TransactionOption isolationLevelOption(IsolationLevel isolationLevel) { + public static TransactionOption isolationLevel(IsolationLevel isolationLevel) { return new IsolationLevelOption(isolationLevel); } From d63e36edbd3899fc80cbde532fa9617426024f61 Mon Sep 17 00:00:00 2001 From: Shobhit Date: Tue, 11 Mar 2025 20:07:55 +0530 Subject: [PATCH 5/8] Update google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Knut Olav Løite --- .../src/main/java/com/google/cloud/spanner/SpannerOptions.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java index 515e7debe6b..3f19f28a157 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java @@ -1652,8 +1652,7 @@ public Builder setEnableEndToEndTracing(boolean enableEndToEndTracing) { } /** - * Provides the default read-write transaction options for all databases, limited to setting the - * {@link IsolationLevel}. These defaults are overridden by any explicit {@link + * Provides the default read-write transaction options for all databases. These defaults are overridden by any explicit {@link * com.google.cloud.spanner.Options.TransactionOption} provided through {@link DatabaseClient}. * *

    Example Usage: From afdf0eb8b114f108ddca1932f8ba43304dd0b479 Mon Sep 17 00:00:00 2001 From: Shobhit Gupta Date: Wed, 12 Mar 2025 06:47:09 +0530 Subject: [PATCH 6/8] feat: code reformat and minor test cases fix due to renaming of Options.isolationLevel --- .../com/google/cloud/spanner/SpannerOptions.java | 5 +++-- ...ientImplWithDefaultRWTransactionOptionsTest.java | 4 ++-- .../java/com/google/cloud/spanner/OptionsTest.java | 13 +++++-------- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java index 3f19f28a157..695e156dfc3 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java @@ -1652,8 +1652,9 @@ public Builder setEnableEndToEndTracing(boolean enableEndToEndTracing) { } /** - * Provides the default read-write transaction options for all databases. These defaults are overridden by any explicit {@link - * com.google.cloud.spanner.Options.TransactionOption} provided through {@link DatabaseClient}. + * Provides the default read-write transaction options for all databases. These defaults are + * overridden by any explicit {@link com.google.cloud.spanner.Options.TransactionOption} + * provided through {@link DatabaseClient}. * *

    Example Usage: * diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplWithDefaultRWTransactionOptionsTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplWithDefaultRWTransactionOptionsTest.java index 827e8bd64bc..634356f2223 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplWithDefaultRWTransactionOptionsTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplWithDefaultRWTransactionOptionsTest.java @@ -57,9 +57,9 @@ @RunWith(JUnit4.class) public class DatabaseClientImplWithDefaultRWTransactionOptionsTest { private static final TransactionOption SERIALIZABLE_ISOLATION_OPTION = - Options.isolationLevelOption(IsolationLevel.SERIALIZABLE); + Options.isolationLevel(IsolationLevel.SERIALIZABLE); private static final TransactionOption RR_ISOLATION_OPTION = - Options.isolationLevelOption(IsolationLevel.REPEATABLE_READ); + Options.isolationLevel(IsolationLevel.REPEATABLE_READ); private static final DatabaseId DATABASE_ID = DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]"); private static MockSpannerServiceImpl mockSpanner; diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/OptionsTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/OptionsTest.java index 9a0ba5e7aa9..e2bcc92fedc 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/OptionsTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/OptionsTest.java @@ -382,8 +382,7 @@ public void testTransactionOptionsPriority() { @Test public void testTransactionOptionsIsolationLevel() { Options options = - Options.fromTransactionOptions( - Options.isolationLevelOption(IsolationLevel.REPEATABLE_READ)); + Options.fromTransactionOptions(Options.isolationLevel(IsolationLevel.REPEATABLE_READ)); assertEquals(options.isolationLevel(), IsolationLevel.REPEATABLE_READ); assertEquals( "isolationLevel: " + IsolationLevel.REPEATABLE_READ.name() + " ", options.toString()); @@ -789,11 +788,9 @@ public void transactionOptionsExcludeTxnFromChangeStreams() { @Test public void transactionOptionsIsolationLevel() { Options option1 = - Options.fromTransactionOptions( - Options.isolationLevelOption(IsolationLevel.REPEATABLE_READ)); + Options.fromTransactionOptions(Options.isolationLevel(IsolationLevel.REPEATABLE_READ)); Options option2 = - Options.fromTransactionOptions( - Options.isolationLevelOption(IsolationLevel.REPEATABLE_READ)); + Options.fromTransactionOptions(Options.isolationLevel(IsolationLevel.REPEATABLE_READ)); Options option3 = Options.fromTransactionOptions(); assertEquals(option1, option2); @@ -870,8 +867,8 @@ public void testTransactionOptionCombine_WithNoSpannerOptions() { @Test public void testOptions_WithMultipleDifferentIsolationLevels() { TransactionOption[] transactionOptions = { - Options.isolationLevelOption(IsolationLevel.REPEATABLE_READ), - Options.isolationLevelOption(IsolationLevel.SERIALIZABLE) + Options.isolationLevel(IsolationLevel.REPEATABLE_READ), + Options.isolationLevel(IsolationLevel.SERIALIZABLE) }; Options options = Options.fromTransactionOptions(transactionOptions); assertEquals(options.isolationLevel(), IsolationLevel.SERIALIZABLE); From 12f8a93ae6d11fcb3a58deeaebffbcf0a26686cf Mon Sep 17 00:00:00 2001 From: Shobhit Gupta Date: Fri, 14 Mar 2025 14:16:22 +0530 Subject: [PATCH 7/8] feat: Options method renamed in javadoc --- .../java/com/google/cloud/spanner/DatabaseClient.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClient.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClient.java index df6708ebcd2..81f05b86699 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClient.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClient.java @@ -415,7 +415,7 @@ ServerStream batchWriteAtLeastOnce( * applied to any other requests on the transaction. *

  • {@link Options#commitStats()}: Request that the server includes commit statistics in the * {@link CommitResponse}. - *
  • {@link Options#isolationLevelOption(IsolationLevel)}: The isolation level for the + *
  • {@link Options#isolationLevel(IsolationLevel)}: The isolation level for the * transaction * */ @@ -457,7 +457,7 @@ ServerStream batchWriteAtLeastOnce( * applied to any other requests on the transaction. *
  • {@link Options#commitStats()}: Request that the server includes commit statistics in the * {@link CommitResponse}. - *
  • {@link Options#isolationLevelOption(IsolationLevel)}: The isolation level for the + *
  • {@link Options#isolationLevel(IsolationLevel)}: The isolation level for the * transaction * */ @@ -499,7 +499,7 @@ ServerStream batchWriteAtLeastOnce( * applied to any other requests on the transaction. *
  • {@link Options#commitStats()}: Request that the server includes commit statistics in the * {@link CommitResponse}. - *
  • {@link Options#isolationLevelOption(IsolationLevel)}: The isolation level for the + *
  • {@link Options#isolationLevel(IsolationLevel)}: The isolation level for the * transaction * */ @@ -555,7 +555,7 @@ ServerStream batchWriteAtLeastOnce( * applied to any other requests on the transaction. *
  • {@link Options#commitStats()}: Request that the server includes commit statistics in the * {@link CommitResponse}. - *
  • {@link Options#isolationLevelOption(IsolationLevel)}: The isolation level for the + *
  • {@link Options#isolationLevel(IsolationLevel)}: The isolation level for the * transaction * */ From 515db9c7e37117078193f72838a9708633f1dd2b Mon Sep 17 00:00:00 2001 From: Shobhit Gupta Date: Mon, 17 Mar 2025 12:32:36 +0530 Subject: [PATCH 8/8] code formatting fixed --- .../com/google/cloud/spanner/DatabaseClient.java | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClient.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClient.java index 81f05b86699..a33f39d47fd 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClient.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClient.java @@ -415,8 +415,7 @@ ServerStream batchWriteAtLeastOnce( * applied to any other requests on the transaction. *
  • {@link Options#commitStats()}: Request that the server includes commit statistics in the * {@link CommitResponse}. - *
  • {@link Options#isolationLevel(IsolationLevel)}: The isolation level for the - * transaction + *
  • {@link Options#isolationLevel(IsolationLevel)}: The isolation level for the transaction * */ TransactionRunner readWriteTransaction(TransactionOption... options); @@ -457,8 +456,7 @@ ServerStream batchWriteAtLeastOnce( * applied to any other requests on the transaction. *
  • {@link Options#commitStats()}: Request that the server includes commit statistics in the * {@link CommitResponse}. - *
  • {@link Options#isolationLevel(IsolationLevel)}: The isolation level for the - * transaction + *
  • {@link Options#isolationLevel(IsolationLevel)}: The isolation level for the transaction * */ TransactionManager transactionManager(TransactionOption... options); @@ -499,8 +497,7 @@ ServerStream batchWriteAtLeastOnce( * applied to any other requests on the transaction. *
  • {@link Options#commitStats()}: Request that the server includes commit statistics in the * {@link CommitResponse}. - *
  • {@link Options#isolationLevel(IsolationLevel)}: The isolation level for the - * transaction + *
  • {@link Options#isolationLevel(IsolationLevel)}: The isolation level for the transaction * */ AsyncRunner runAsync(TransactionOption... options); @@ -555,8 +552,7 @@ ServerStream batchWriteAtLeastOnce( * applied to any other requests on the transaction. *
  • {@link Options#commitStats()}: Request that the server includes commit statistics in the * {@link CommitResponse}. - *
  • {@link Options#isolationLevel(IsolationLevel)}: The isolation level for the - * transaction + *
  • {@link Options#isolationLevel(IsolationLevel)}: The isolation level for the transaction * */ AsyncTransactionManager transactionManagerAsync(TransactionOption... options);