diff --git a/google-cloud-clients/google-cloud-spanner/pom.xml b/google-cloud-clients/google-cloud-spanner/pom.xml
index 3a563fe7eddd..ad5981934bf0 100644
--- a/google-cloud-clients/google-cloud-spanner/pom.xml
+++ b/google-cloud-clients/google-cloud-spanner/pom.xml
@@ -18,6 +18,7 @@
google-cloud-spanner
+ ${skipTests}
@@ -27,6 +28,7 @@
2.12.4
com.google.cloud.spanner.IntegrationTest
+ ${skipUnitTests}
diff --git a/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java b/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java
index 850bf2898f57..f17c97dbcf45 100644
--- a/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java
+++ b/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java
@@ -81,7 +81,6 @@
import io.opencensus.trace.Span;
import io.opencensus.trace.Tracer;
import io.opencensus.trace.Tracing;
-
import java.io.IOException;
import java.io.Serializable;
import java.util.AbstractList;
@@ -129,6 +128,19 @@ class SpannerImpl extends BaseService implements Spanner {
private static final String QUERY = "CloudSpannerOperation.ExecuteStreamingQuery";
private static final String READ = "CloudSpannerOperation.ExecuteStreamingRead";
+ private static final ThreadLocal hasPendingTransaction = new ThreadLocal() {
+ @Override
+ protected Boolean initialValue() {
+ return false;
+ }
+ };
+
+ private static void throwIfTransactionsPending() {
+ if (hasPendingTransaction.get() == Boolean.TRUE) {
+ throw newSpannerException(ErrorCode.INTERNAL, "Nested transactions are not supported");
+ }
+ }
+
static {
TraceUtil.exportSpans(CREATE_SESSION, DELETE_SESSION, BEGIN_TRANSACTION, COMMIT, QUERY, READ);
}
@@ -905,6 +917,8 @@ TransactionContextImpl newTransaction() {
}
T setActive(@Nullable T ctx) {
+ throwIfTransactionsPending();
+
if (activeTransaction != null) {
activeTransaction.invalidate();
}
@@ -1239,11 +1253,13 @@ void backoffSleep(Context context, long backoffMillis) {
@Override
public T run(TransactionCallable callable) {
try (Scope s = tracer.withSpan(span)) {
+ hasPendingTransaction.set(Boolean.TRUE);
return runInternal(callable);
} catch (RuntimeException e) {
TraceUtil.endSpanWithFailure(span, e);
throw e;
} finally {
+ hasPendingTransaction.set(Boolean.FALSE);
span.end();
}
}
@@ -1660,6 +1676,8 @@ ByteString getTransactionId() {
}
void initTransaction() {
+ throwIfTransactionsPending();
+
// Since we only support synchronous calls, just block on "txnLock" while the RPC is in
// flight. Note that we use the strategy of sending an explicit BeginTransaction() RPC,
// rather than using the first read in the transaction to begin it implicitly. The chosen
diff --git a/google-cloud-clients/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionRunnerImplTest.java b/google-cloud-clients/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionRunnerImplTest.java
index 9362814146ab..0cd804c6a89e 100644
--- a/google-cloud-clients/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionRunnerImplTest.java
+++ b/google-cloud-clients/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionRunnerImplTest.java
@@ -32,7 +32,6 @@
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import java.util.concurrent.atomic.AtomicInteger;
-
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
diff --git a/google-cloud-clients/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionTest.java b/google-cloud-clients/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionTest.java
index 1bacea50d627..139c75f66a04 100644
--- a/google-cloud-clients/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionTest.java
+++ b/google-cloud-clients/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionTest.java
@@ -23,13 +23,17 @@
import com.google.cloud.Timestamp;
import com.google.cloud.spanner.AbortedException;
+import com.google.cloud.spanner.BatchClient;
+import com.google.cloud.spanner.BatchReadOnlyTransaction;
import com.google.cloud.spanner.Database;
import com.google.cloud.spanner.DatabaseClient;
import com.google.cloud.spanner.ErrorCode;
import com.google.cloud.spanner.IntegrationTest;
import com.google.cloud.spanner.IntegrationTestEnv;
import com.google.cloud.spanner.Key;
+import com.google.cloud.spanner.KeySet;
import com.google.cloud.spanner.Mutation;
+import com.google.cloud.spanner.PartitionOptions;
import com.google.cloud.spanner.ReadContext;
import com.google.cloud.spanner.ResultSet;
import com.google.cloud.spanner.SpannerException;
@@ -349,4 +353,110 @@ public Void run(TransactionContext transaction) throws SpannerException {
.getLong(0))
.isEqualTo(2);
}
+
+ private void doNestedRwTransaction() {
+ client
+ .readWriteTransaction()
+ .run(
+ new TransactionCallable() {
+ @Override
+ public Void run(TransactionContext transaction) throws SpannerException {
+ client
+ .readWriteTransaction()
+ .run(
+ new TransactionCallable() {
+ @Override
+ public Void run(TransactionContext transaction) throws Exception {
+ return null;
+ }
+ });
+
+ return null;
+ }
+ });
+ }
+
+ @Test
+ public void nestedRwRwTransactionShouldThrowException() {
+ try {
+ doNestedRwTransaction();
+ fail("Expected exception");
+ } catch (SpannerException e) {
+ assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INTERNAL);
+ assertThat(e.getMessage()).contains("not supported");
+ }
+ }
+
+ @Test
+ public void nestedRwRdTransactionShouldThrowException() {
+ try {
+ client
+ .readWriteTransaction()
+ .run(
+ new TransactionCallable() {
+ @Override
+ public Void run(TransactionContext transaction) throws SpannerException {
+ client
+ .readOnlyTransaction()
+ .getReadTimestamp();
+
+ return null;
+ }
+ });
+ fail("Expected exception");
+ } catch (SpannerException e) {
+ assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INTERNAL);
+ assertThat(e.getMessage()).contains("not supported");
+ }
+ }
+
+ @Test
+ public void nestedRwBatchTransactionShouldThrowException() {
+ try {
+ client
+ .readWriteTransaction()
+ .run(
+ new TransactionCallable() {
+ @Override
+ public Void run(TransactionContext transaction) throws SpannerException {
+ BatchClient batchClient = env.getTestHelper().getBatchClient(db);
+ BatchReadOnlyTransaction batchTxn = batchClient
+ .batchReadOnlyTransaction(TimestampBound.strong());
+ batchTxn.partitionReadUsingIndex(
+ PartitionOptions.getDefaultInstance(),
+ "Test",
+ "Index",
+ KeySet.all(),
+ Arrays.asList("Fingerprint"));
+
+ return null;
+ }
+ });
+ fail("Expected exception");
+ } catch (SpannerException e) {
+ assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INTERNAL);
+ assertThat(e.getMessage()).contains("not supported");
+ }
+ }
+
+ @Test
+ public void nestedRwSingleUseReadTransactionShouldThrowException() {
+ try {
+ client
+ .readWriteTransaction()
+ .run(
+ new TransactionCallable() {
+ @Override
+ public Void run(TransactionContext transaction) throws SpannerException {
+ client.singleUseReadOnlyTransaction();
+
+ return null;
+ }
+ });
+ fail("Expected exception");
+ } catch (SpannerException e) {
+ assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INTERNAL);
+ assertThat(e.getMessage()).contains("not supported");
+ }
+ }
}