From 9fb15fee2dcf09f672c94b3d353924d64c6b42e9 Mon Sep 17 00:00:00 2001 From: Olav Loite Date: Wed, 8 Jul 2020 05:54:56 +0200 Subject: [PATCH 01/19] feat: inline begin tx with first statement --- .../cloud/spanner/AbstractReadContext.java | 6 +++ .../cloud/spanner/DatabaseClientImpl.java | 44 +++++++++++++++++-- 2 files changed, 47 insertions(+), 3 deletions(-) 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 f05fd10dda..277772eb54 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 @@ -672,9 +672,15 @@ public void close() { } } + /** + * Returns the {@link TransactionSelector} that should be used for a statement that is executed on + * this read context. This could be a reference to an existing transaction ID, or it could be a + * BeginTransaction option that should be included with the statement. + */ @Nullable abstract TransactionSelector getTransactionSelector(); + /** This method is called when a statement returned a new transaction as part of its results. */ @Override public void onTransactionMetadata(Transaction transaction) {} diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java index 4dd10001c7..d17813ec67 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java @@ -29,6 +29,8 @@ class DatabaseClientImpl implements DatabaseClient { private static final String READ_WRITE_TRANSACTION = "CloudSpanner.ReadWriteTransaction"; + private static final String READ_WRITE_TRANSACTION_WITH_INLINE_BEGIN = + "CloudSpanner.ReadWriteTransactionWithInlineBegin"; private static final String READ_ONLY_TRANSACTION = "CloudSpanner.ReadOnlyTransaction"; private static final String PARTITION_DML_TRANSACTION = "CloudSpanner.PartitionDMLTransaction"; private static final Tracer tracer = Tracing.getTracer(); @@ -40,15 +42,17 @@ private enum SessionMode { @VisibleForTesting final String clientId; @VisibleForTesting final SessionPool pool; + private final boolean inlineBeginReadWriteTransactions; @VisibleForTesting - DatabaseClientImpl(SessionPool pool) { - this("", pool); + DatabaseClientImpl(SessionPool pool, boolean inlineBeginReadWriteTransactions) { + this("", pool, inlineBeginReadWriteTransactions); } - DatabaseClientImpl(String clientId, SessionPool pool) { + DatabaseClientImpl(String clientId, SessionPool pool, boolean inlineBeginReadWriteTransactions) { this.clientId = clientId; this.pool = pool; + this.inlineBeginReadWriteTransactions = inlineBeginReadWriteTransactions; } @VisibleForTesting @@ -169,6 +173,12 @@ public ReadOnlyTransaction readOnlyTransaction(TimestampBound bound) { @Override public TransactionRunner readWriteTransaction() { + return inlineBeginReadWriteTransactions + ? inlinedReadWriteTransaction() + : preparedReadWriteTransaction(); + } + + private TransactionRunner preparedReadWriteTransaction() { Span span = tracer.spanBuilder(READ_WRITE_TRANSACTION).startSpan(); try (Scope s = tracer.withSpan(span)) { return getReadWriteSession().readWriteTransaction(); @@ -179,9 +189,26 @@ public TransactionRunner readWriteTransaction() { span.end(TraceUtil.END_SPAN_OPTIONS); } } + + private TransactionRunner inlinedReadWriteTransaction() { + Span span = tracer.spanBuilder(READ_WRITE_TRANSACTION_WITH_INLINE_BEGIN).startSpan(); + try (Scope s = tracer.withSpan(span)) { + // An inlined read/write transaction does not need a write-prepared session. + return getReadSession().readWriteTransaction(); + } catch (RuntimeException e) { + TraceUtil.endSpanWithFailure(span, e); + throw e; + } + } @Override public TransactionManager transactionManager() { + return inlineBeginReadWriteTransactions + ? inlinedTransactionManager() + : preparedTransactionManager(); + } + + private TransactionManager preparedTransactionManager() { Span span = tracer.spanBuilder(READ_WRITE_TRANSACTION).startSpan(); try (Scope s = tracer.withSpan(span)) { return getReadWriteSession().transactionManager(); @@ -190,6 +217,17 @@ public TransactionManager transactionManager() { throw e; } } + + private TransactionManager inlinedTransactionManager() { + Span span = tracer.spanBuilder(READ_WRITE_TRANSACTION_WITH_INLINE_BEGIN).startSpan(); + try (Scope s = tracer.withSpan(span)) { + // An inlined read/write transaction does not need a write-prepared session. + return getReadSession().transactionManager(); + } catch (RuntimeException e) { + TraceUtil.endSpanWithFailure(span, e); + throw e; + } + } @Override public AsyncRunner runAsync() { From 6b8d9dc0e855f64ef4e145a4d469f70ada2917c4 Mon Sep 17 00:00:00 2001 From: Olav Loite Date: Wed, 8 Jul 2020 16:14:03 +0200 Subject: [PATCH 02/19] feat: support inlining BeginTransaction --- .../spanner/AsyncTransactionManagerImpl.java | 12 +- .../cloud/spanner/DatabaseClientImpl.java | 4 +- .../com/google/cloud/spanner/SessionImpl.java | 12 +- .../com/google/cloud/spanner/SpannerImpl.java | 3 +- .../google/cloud/spanner/SpannerOptions.java | 36 ++ .../cloud/spanner/TransactionManagerImpl.java | 12 +- .../cloud/spanner/TransactionRunnerImpl.java | 348 +++++++----- .../spanner/InlineBeginTransactionTest.java | 430 +++++++++++++++ .../IntegrationTestWithClosedSessionsEnv.java | 8 +- .../cloud/spanner/MockSpannerServiceImpl.java | 20 +- ...adWriteTransactionWithInlineBeginTest.java | 496 ++++++++++++++++++ .../google/cloud/spanner/SessionImplTest.java | 5 + .../google/cloud/spanner/SessionPoolTest.java | 26 +- .../spanner/TransactionManagerImplTest.java | 116 +++- .../spanner/TransactionRunnerImplTest.java | 70 ++- .../google/cloud/spanner/it/ITDMLTest.java | 36 +- 16 files changed, 1454 insertions(+), 180 deletions(-) create mode 100644 google-cloud-spanner/src/test/java/com/google/cloud/spanner/InlineBeginTransactionTest.java create mode 100644 google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReadWriteTransactionWithInlineBeginTest.java diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManagerImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManagerImpl.java index 082fa827e7..a306e7caa2 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManagerImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManagerImpl.java @@ -16,6 +16,7 @@ package com.google.cloud.spanner; +import com.google.api.core.ApiAsyncFunction; import com.google.api.core.ApiFuture; import com.google.api.core.ApiFutureCallback; import com.google.api.core.ApiFutures; @@ -26,6 +27,7 @@ import com.google.cloud.spanner.TransactionManager.TransactionState; import com.google.common.base.Preconditions; import com.google.common.util.concurrent.MoreExecutors; +import com.google.protobuf.Empty; import io.opencensus.trace.Span; import io.opencensus.trace.Tracer; import io.opencensus.trace.Tracing; @@ -138,7 +140,15 @@ public ApiFuture rollbackAsync() { txnState == TransactionState.STARTED, "rollback can only be called if the transaction is in progress"); try { - return txn.rollbackAsync(); + return ApiFutures.transformAsync( + txn.rollbackAsync(), + new ApiAsyncFunction() { + @Override + public ApiFuture apply(Empty input) throws Exception { + return ApiFutures.immediateFuture(null); + } + }, + MoreExecutors.directExecutor()); } finally { txnState = TransactionState.ROLLED_BACK; } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java index d17813ec67..68f69fc97e 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java @@ -189,7 +189,7 @@ private TransactionRunner preparedReadWriteTransaction() { span.end(TraceUtil.END_SPAN_OPTIONS); } } - + private TransactionRunner inlinedReadWriteTransaction() { Span span = tracer.spanBuilder(READ_WRITE_TRANSACTION_WITH_INLINE_BEGIN).startSpan(); try (Scope s = tracer.withSpan(span)) { @@ -217,7 +217,7 @@ private TransactionManager preparedTransactionManager() { throw e; } } - + private TransactionManager inlinedTransactionManager() { Span span = tracer.spanBuilder(READ_WRITE_TRANSACTION_WITH_INLINE_BEGIN).startSpan(); try (Scope s = tracer.withSpan(span)) { 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 6a91d85fef..7b325dafd7 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 @@ -228,19 +228,25 @@ public ReadOnlyTransaction readOnlyTransaction(TimestampBound bound) { @Override public TransactionRunner readWriteTransaction() { return setActive( - new TransactionRunnerImpl(this, spanner.getRpc(), spanner.getDefaultPrefetchChunks())); + new TransactionRunnerImpl( + this, + spanner.getRpc(), + spanner.getDefaultPrefetchChunks(), + spanner.getOptions().isInlineBeginForReadWriteTransaction())); } @Override public AsyncRunner runAsync() { return new AsyncRunnerImpl( setActive( - new TransactionRunnerImpl(this, spanner.getRpc(), spanner.getDefaultPrefetchChunks()))); + new TransactionRunnerImpl( + this, spanner.getRpc(), spanner.getDefaultPrefetchChunks(), false))); } @Override public TransactionManager transactionManager() { - return new TransactionManagerImpl(this, currentSpan); + return new TransactionManagerImpl( + this, currentSpan, spanner.getOptions().isInlineBeginForReadWriteTransaction()); } @Override diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java index 2d034eda88..7df8105c8f 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java @@ -233,7 +233,8 @@ public DatabaseClient getDatabaseClient(DatabaseId db) { @VisibleForTesting DatabaseClientImpl createDatabaseClient(String clientId, SessionPool pool) { - return new DatabaseClientImpl(clientId, pool); + return new DatabaseClientImpl( + clientId, pool, getOptions().isInlineBeginForReadWriteTransaction()); } @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 bc3f513ce0..4568de65a3 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 @@ -89,6 +89,7 @@ public class SpannerOptions extends ServiceOptions { private final int prefetchChunks; private final int numChannels; private final ImmutableMap sessionLabels; + private final boolean inlineBeginForReadWriteTransaction; private final SpannerStubSettings spannerStubSettings; private final InstanceAdminStubSettings instanceAdminStubSettings; private final DatabaseAdminStubSettings databaseAdminStubSettings; @@ -220,6 +221,7 @@ private SpannerOptions(Builder builder) { : SessionPoolOptions.newBuilder().build(); prefetchChunks = builder.prefetchChunks; sessionLabels = builder.sessionLabels; + inlineBeginForReadWriteTransaction = builder.inlineBeginForReadWriteTransaction; try { spannerStubSettings = builder.spannerStubSettingsBuilder.build(); instanceAdminStubSettings = builder.instanceAdminStubSettingsBuilder.build(); @@ -298,6 +300,7 @@ public static class Builder private int prefetchChunks = DEFAULT_PREFETCH_CHUNKS; private SessionPoolOptions sessionPoolOptions; private ImmutableMap sessionLabels; + private boolean inlineBeginForReadWriteTransaction; private SpannerStubSettings.Builder spannerStubSettingsBuilder = SpannerStubSettings.newBuilder(); private InstanceAdminStubSettings.Builder instanceAdminStubSettingsBuilder = @@ -347,6 +350,7 @@ private Builder() { this.sessionPoolOptions = options.sessionPoolOptions; this.prefetchChunks = options.prefetchChunks; this.sessionLabels = options.sessionLabels; + this.inlineBeginForReadWriteTransaction = options.inlineBeginForReadWriteTransaction; this.spannerStubSettingsBuilder = options.spannerStubSettings.toBuilder(); this.instanceAdminStubSettingsBuilder = options.instanceAdminStubSettings.toBuilder(); this.databaseAdminStubSettingsBuilder = options.databaseAdminStubSettings.toBuilder(); @@ -439,6 +443,34 @@ public Builder setSessionLabels(Map sessionLabels) { return this; } + /** + * Sets whether {@link DatabaseClient}s should inline the BeginTransaction option with the first + * query or update statement that is executed by a read/write transaction instead of using a + * write-prepared session from the session pool. Enabling this option can improve execution + * times for read/write transactions in the following scenarios: + * + *

+ * + *

    + *
  • Applications with a very high rate of read/write transactions where the session pool is + * not able to prepare new read/write transactions at the same rate as the application is + * requesting read/write transactions. + *
  • Applications with a very low rate of read/write transactions where sessions with a + * prepared read/write transaction are kept in the session pool for a long time without + * being used. + *
+ * + * If you enable this option, you should also re-evaluate the value for {@link + * SessionPoolOptions.Builder#setWriteSessionsFraction(float)}. When this option is enabled, + * write-prepared sessions are only used for calls to {@link DatabaseClient#write(Iterable)}. If + * your application does not use this method, you should set the write fraction for the session + * pool to zero. + */ + public Builder setInlineBeginForReadWriteTransaction(boolean inlineBegin) { + this.inlineBeginForReadWriteTransaction = inlineBegin; + return this; + } + /** * {@link SpannerOptions.Builder} does not support global retry settings, as it creates three * different gRPC clients: {@link Spanner}, {@link DatabaseAdminClient} and {@link @@ -735,6 +767,10 @@ public Map getSessionLabels() { return sessionLabels; } + public boolean isInlineBeginForReadWriteTransaction() { + return inlineBeginForReadWriteTransaction; + } + public SpannerStubSettings getSpannerStubSettings() { return spannerStubSettings; } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionManagerImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionManagerImpl.java index 8dbab88314..f5c505e6c5 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionManagerImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionManagerImpl.java @@ -30,13 +30,15 @@ final class TransactionManagerImpl implements TransactionManager, SessionTransac private final SessionImpl session; private Span span; + private final boolean inlineBegin; private TransactionRunnerImpl.TransactionContextImpl txn; private TransactionState txnState; - TransactionManagerImpl(SessionImpl session, Span span) { + TransactionManagerImpl(SessionImpl session, Span span, boolean inlineBegin) { this.session = session; this.span = span; + this.inlineBegin = inlineBegin; } Span getSpan() { @@ -54,7 +56,9 @@ public TransactionContext begin() { try (Scope s = tracer.withSpan(span)) { txn = session.newTransaction(); session.setActive(this); - txn.ensureTxn(); + if (!inlineBegin) { + txn.ensureTxn(); + } txnState = TransactionState.STARTED; return txn; } @@ -102,7 +106,9 @@ public TransactionContext resetForRetry() { } try (Scope s = tracer.withSpan(span)) { txn = session.newTransaction(); - txn.ensureTxn(); + if (!inlineBegin) { + txn.ensureTxn(); + } txnState = TransactionState.STARTED; return txn; } 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 21812aa96a..4f23cadd73 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 @@ -21,7 +21,6 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; -import com.google.api.core.ApiAsyncFunction; import com.google.api.core.ApiFunction; import com.google.api.core.ApiFuture; import com.google.api.core.ApiFutures; @@ -45,6 +44,8 @@ import com.google.spanner.v1.ExecuteSqlRequest.QueryMode; import com.google.spanner.v1.ResultSet; import com.google.spanner.v1.RollbackRequest; +import com.google.spanner.v1.Transaction; +import com.google.spanner.v1.TransactionOptions; import com.google.spanner.v1.TransactionSelector; import io.opencensus.common.Scope; import io.opencensus.trace.AttributeValue; @@ -57,6 +58,7 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nullable; @@ -150,7 +152,15 @@ public void removeListener(Runnable listener) { @GuardedBy("lock") private long retryDelayInMillis = -1L; - private ByteString transactionId; + /** + * transactionLock guards that only one request can be beginning the transaction at any time. We + * only hold on to this lock while a request is creating a transaction. After a transaction has + * been created, the lock is released and concurrent requests can be executed on the + * transaction. + */ + private final ReentrantLock transactionLock = new ReentrantLock(); + + private volatile ByteString transactionId; private Timestamp commitTimestamp; private TransactionContextImpl(Builder builder) { @@ -190,36 +200,7 @@ void ensureTxn() { ApiFuture ensureTxnAsync() { final SettableApiFuture res = SettableApiFuture.create(); if (transactionId == null || isAborted()) { - span.addAnnotation("Creating Transaction"); - final ApiFuture fut = session.beginTransactionAsync(); - fut.addListener( - new Runnable() { - @Override - public void run() { - try { - transactionId = fut.get(); - span.addAnnotation( - "Transaction Creation Done", - ImmutableMap.of( - "Id", AttributeValue.stringAttributeValue(transactionId.toStringUtf8()))); - txnLogger.log( - Level.FINER, - "Started transaction {0}", - txnLogger.isLoggable(Level.FINER) - ? transactionId.asReadOnlyByteBuffer() - : null); - res.set(null); - } catch (ExecutionException e) { - span.addAnnotation( - "Transaction Creation Failed", - TraceUtil.getExceptionAnnotations(e.getCause() == null ? e : e.getCause())); - res.setException(e.getCause() == null ? e : e.getCause()); - } catch (InterruptedException e) { - res.setException(SpannerExceptionFactory.propagateInterrupt(e)); - } - } - }, - MoreExecutors.directExecutor()); + createTxnAsync(res); } else { span.addAnnotation( "Transaction Initialized", @@ -234,6 +215,39 @@ public void run() { return res; } + private void createTxnAsync(final SettableApiFuture res) { + span.addAnnotation("Creating Transaction"); + final ApiFuture fut = session.beginTransactionAsync(); + fut.addListener( + new Runnable() { + @Override + public void run() { + try { + transactionId = fut.get(); + span.addAnnotation( + "Transaction Creation Done", + ImmutableMap.of( + "Id", AttributeValue.stringAttributeValue(transactionId.toStringUtf8()))); + txnLogger.log( + Level.FINER, + "Started transaction {0}", + txnLogger.isLoggable(Level.FINER) + ? transactionId.asReadOnlyByteBuffer() + : null); + res.set(null); + } catch (ExecutionException e) { + span.addAnnotation( + "Transaction Creation Failed", + TraceUtil.getExceptionAnnotations(e.getCause() == null ? e : e.getCause())); + res.setException(e.getCause() == null ? e : e.getCause()); + } catch (InterruptedException e) { + res.setException(SpannerExceptionFactory.propagateInterrupt(e)); + } + } + }, + MoreExecutors.directExecutor()); + } + void commit() { try { commitTimestamp = commitAsync().get(); @@ -246,87 +260,94 @@ void commit() { ApiFuture commitAsync() { final SettableApiFuture res = SettableApiFuture.create(); - final SettableApiFuture latch; + final SettableApiFuture finishOps; synchronized (lock) { - latch = finishedAsyncOperations; + if (finishedAsyncOperations.isDone() && transactionId == null) { + finishOps = SettableApiFuture.create(); + createTxnAsync(finishOps); + } else { + finishOps = finishedAsyncOperations; + } } - latch.addListener( - new Runnable() { - @Override - public void run() { - try { - latch.get(); - CommitRequest.Builder builder = - CommitRequest.newBuilder() - .setSession(session.getName()) - .setTransactionId(transactionId); - synchronized (lock) { - if (!mutations.isEmpty()) { - List mutationsProto = new ArrayList<>(); - Mutation.toProto(mutations, mutationsProto); - builder.addAllMutations(mutationsProto); - } - // Ensure that no call to buffer mutations that would be lost can succeed. - mutations = null; - } - final CommitRequest commitRequest = builder.build(); - span.addAnnotation("Starting Commit"); - final Span opSpan = - tracer.spanBuilderWithExplicitParent(SpannerImpl.COMMIT, span).startSpan(); - final ApiFuture commitFuture = - rpc.commitAsync(commitRequest, session.getOptions()); - commitFuture.addListener( - tracer.withSpan( - opSpan, - new Runnable() { - @Override - public void run() { - try { - CommitResponse commitResponse = commitFuture.get(); - if (!commitResponse.hasCommitTimestamp()) { - throw newSpannerException( - ErrorCode.INTERNAL, - "Missing commitTimestamp:\n" + session.getName()); - } - Timestamp ts = - Timestamp.fromProto(commitResponse.getCommitTimestamp()); - span.addAnnotation("Commit Done"); - opSpan.end(TraceUtil.END_SPAN_OPTIONS); - res.set(ts); - } catch (Throwable e) { - if (e instanceof ExecutionException) { - e = - SpannerExceptionFactory.newSpannerException( - e.getCause() == null ? e : e.getCause()); - } else if (e instanceof InterruptedException) { - e = - SpannerExceptionFactory.propagateInterrupt( - (InterruptedException) e); - } else { - e = SpannerExceptionFactory.newSpannerException(e); - } - span.addAnnotation( - "Commit Failed", TraceUtil.getExceptionAnnotations(e)); - TraceUtil.endSpanWithFailure(opSpan, e); - onError((SpannerException) e); - res.setException(e); - } - } - }), - MoreExecutors.directExecutor()); - } catch (InterruptedException e) { - res.setException(SpannerExceptionFactory.propagateInterrupt(e)); - } catch (ExecutionException e) { - res.setException( - SpannerExceptionFactory.newSpannerException( - e.getCause() == null ? e : e.getCause())); - } - } - }, - MoreExecutors.directExecutor()); + finishOps.addListener(new CommitRunnable(res, finishOps), MoreExecutors.directExecutor()); return res; } + private final class CommitRunnable implements Runnable { + private final SettableApiFuture res; + private final ApiFuture prev; + + CommitRunnable(SettableApiFuture res, ApiFuture prev) { + this.res = res; + this.prev = prev; + } + + @Override + public void run() { + try { + prev.get(); + CommitRequest.Builder builder = + CommitRequest.newBuilder() + .setSession(session.getName()) + .setTransactionId(transactionId); + synchronized (lock) { + if (!mutations.isEmpty()) { + List mutationsProto = new ArrayList<>(); + Mutation.toProto(mutations, mutationsProto); + builder.addAllMutations(mutationsProto); + } + // Ensure that no call to buffer mutations that would be lost can succeed. + mutations = null; + } + final CommitRequest commitRequest = builder.build(); + span.addAnnotation("Starting Commit"); + final Span opSpan = + tracer.spanBuilderWithExplicitParent(SpannerImpl.COMMIT, span).startSpan(); + final ApiFuture commitFuture = + rpc.commitAsync(commitRequest, session.getOptions()); + commitFuture.addListener( + tracer.withSpan( + opSpan, + new Runnable() { + @Override + public void run() { + try { + CommitResponse commitResponse = commitFuture.get(); + if (!commitResponse.hasCommitTimestamp()) { + throw newSpannerException( + ErrorCode.INTERNAL, "Missing commitTimestamp:\n" + session.getName()); + } + Timestamp ts = Timestamp.fromProto(commitResponse.getCommitTimestamp()); + span.addAnnotation("Commit Done"); + opSpan.end(TraceUtil.END_SPAN_OPTIONS); + res.set(ts); + } catch (Throwable e) { + if (e instanceof ExecutionException) { + e = + SpannerExceptionFactory.newSpannerException( + e.getCause() == null ? e : e.getCause()); + } else if (e instanceof InterruptedException) { + e = SpannerExceptionFactory.propagateInterrupt((InterruptedException) e); + } else { + e = SpannerExceptionFactory.newSpannerException(e); + } + span.addAnnotation("Commit Failed", TraceUtil.getExceptionAnnotations(e)); + TraceUtil.endSpanWithFailure(opSpan, e); + onError((SpannerException) e); + res.setException(e); + } + } + }), + MoreExecutors.directExecutor()); + } catch (InterruptedException e) { + res.setException(SpannerExceptionFactory.propagateInterrupt(e)); + } catch (ExecutionException e) { + res.setException( + SpannerExceptionFactory.newSpannerException(e.getCause() == null ? e : e.getCause())); + } + } + } + Timestamp commitTimestamp() { checkState(commitTimestamp != null, "run() has not yet returned normally"); return commitTimestamp; @@ -339,54 +360,88 @@ boolean isAborted() { } void rollback() { - // We're exiting early due to a user exception, but the transaction is still active. - // Send a rollback for the transaction to release any locks held. - // TODO(user): Make this an async fire-and-forget request. try { - // Note that we're not retrying this request since we don't particularly care about the - // response. Normally, the next thing that will happen is that we will make a fresh - // transaction attempt, which should implicitly abort this one. + rollbackAsync().get(); + } catch (ExecutionException e) { + txnLogger.log(Level.FINE, "Exception during rollback", e); + span.addAnnotation("Rollback Failed", TraceUtil.getExceptionAnnotations(e)); + } catch (InterruptedException e) { + throw SpannerExceptionFactory.propagateInterrupt(e); + } + } + + ApiFuture rollbackAsync() { + // It could be that there is no transaction if the transaction has been marked + // withInlineBegin, and there has not been any query/update statement that has been executed. + // In that case, we do not need to do anything, as there is no transaction. + // + // We do not take the transactionLock before trying to rollback to prevent a rollback call + // from blocking if an async query or update statement that is trying to begin the transaction + // is still in flight. That transaction will then automatically be terminated by the server. + if (transactionId != null) { span.addAnnotation("Starting Rollback"); - rpc.rollback( + return rpc.rollbackAsync( RollbackRequest.newBuilder() .setSession(session.getName()) .setTransactionId(transactionId) .build(), session.getOptions()); - span.addAnnotation("Rollback Done"); - } catch (SpannerException e) { - txnLogger.log(Level.FINE, "Exception during rollback", e); - span.addAnnotation("Rollback Failed", TraceUtil.getExceptionAnnotations(e)); + } else { + return ApiFutures.immediateFuture(Empty.getDefaultInstance()); } } - ApiFuture rollbackAsync() { - span.addAnnotation("Starting Rollback"); - return ApiFutures.transformAsync( - rpc.rollbackAsync( - RollbackRequest.newBuilder() - .setSession(session.getName()) - .setTransactionId(transactionId) - .build(), - session.getOptions()), - new ApiAsyncFunction() { - @Override - public ApiFuture apply(Empty input) throws Exception { - span.addAnnotation("Rollback Done"); - return ApiFutures.immediateFuture(null); - } - }, - MoreExecutors.directExecutor()); - } - @Nullable @Override TransactionSelector getTransactionSelector() { + // Check if there is already a transactionId available. That is the case if this transaction + // has already been prepared by the session pool, or if this transaction has been marked + // withInlineBegin and an earlier statement has already started a transaction. + if (transactionId == null) { + try { + // Wait if another request is already beginning, committing or rolling back the + // transaction. + transactionLock.lockInterruptibly(); + // Check again if a transactionId is now available. It could be that the thread that was + // holding the lock and that had sent a statement with a BeginTransaction request caused + // an error and did not return a transaction. + if (transactionId == null) { + // Return a TransactionSelector that will start a new transaction as part of the + // statement that is being executed. + return TransactionSelector.newBuilder() + .setBegin( + TransactionOptions.newBuilder() + .setReadWrite(TransactionOptions.ReadWrite.getDefaultInstance())) + .build(); + } else { + transactionLock.unlock(); + } + } catch (InterruptedException e) { + throw SpannerExceptionFactory.newSpannerExceptionForCancellation(null, e); + } + } + // There is already a transactionId available. Include that id as the transaction to use. return TransactionSelector.newBuilder().setId(transactionId).build(); } + @Override + public void onTransactionMetadata(Transaction transaction) { + // A transaction has been returned by a statement that was executed. Set the id of the + // transaction on this instance and release the lock to allow other statements to proceed. + if (this.transactionId == null && transaction != null && transaction.getId() != null) { + this.transactionId = transaction.getId(); + transactionLock.unlock(); + } + } + @Override public void onError(SpannerException e) { + // Release the transactionLock if that is being held by this thread. That would mean that the + // statement that was trying to start a transaction caused an error. The next statement should + // in that case also include a BeginTransaction option. + if (transactionLock.isHeldByCurrentThread()) { + transactionLock.unlock(); + } if (e.getErrorCode() == ErrorCode.ABORTED) { long delay = -1L; if (e instanceof AbortedException) { @@ -433,6 +488,9 @@ public long executeUpdate(Statement statement) { throw new IllegalArgumentException( "DML response missing stats possibly due to non-DML statement as input"); } + if (resultSet.getMetadata().hasTransaction()) { + onTransactionMetadata(resultSet.getMetadata().getTransaction()); + } // For standard DML, using the exact row count. return resultSet.getStats().getRowCountExact(); } catch (SpannerException e) { @@ -506,6 +564,9 @@ public long[] batchUpdate(Iterable statements) { long[] results = new long[response.getResultSetsCount()]; for (int i = 0; i < response.getResultSetsCount(); ++i) { results[i] = response.getResultSets(i).getStats().getRowCountExact(); + if (response.getResultSets(i).getMetadata().hasTransaction()) { + onTransactionMetadata(response.getResultSets(i).getMetadata().getTransaction()); + } } // If one of the DML statements was aborted, we should throw an aborted exception. @@ -610,6 +671,7 @@ public ListenableAsyncResultSet executeQueryAsync( private boolean blockNestedTxn = true; private final SessionImpl session; private Span span; + private final boolean inlineBegin; private TransactionContextImpl txn; private volatile boolean isValid = true; @@ -619,8 +681,10 @@ public TransactionRunner allowNestedTransaction() { return this; } - TransactionRunnerImpl(SessionImpl session, SpannerRpc rpc, int defaultPrefetchChunks) { + TransactionRunnerImpl( + SessionImpl session, SpannerRpc rpc, int defaultPrefetchChunks, boolean inlineBegin) { this.session = session; + this.inlineBegin = inlineBegin; this.txn = session.newTransaction(); } @@ -666,7 +730,11 @@ public T call() { span.addAnnotation( "Starting Transaction Attempt", ImmutableMap.of("Attempt", AttributeValue.longAttributeValue(attempt.longValue()))); - txn.ensureTxn(); + // Only ensure that there is a transaction if we should not inline the beginTransaction + // with the first statement. + if (!inlineBegin) { + txn.ensureTxn(); + } T result; boolean shouldRollback = true; 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 new file mode 100644 index 0000000000..f7c10848ad --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InlineBeginTransactionTest.java @@ -0,0 +1,430 @@ +/* + * Copyright 2020 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.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +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.TransactionRunner.TransactionCallable; +import com.google.protobuf.AbstractMessage; +import com.google.protobuf.ListValue; +import com.google.spanner.v1.BeginTransactionRequest; +import com.google.spanner.v1.ResultSetMetadata; +import com.google.spanner.v1.StructType; +import com.google.spanner.v1.StructType.Field; +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.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.atomic.AtomicBoolean; +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 InlineBeginTransactionTest { + private static MockSpannerServiceImpl mockSpanner; + private static Server server; + 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 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; + + @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())); + + String uniqueName = InProcessServerBuilder.generateName(); + 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() throws IOException { + mockSpanner.reset(); + mockSpanner.removeAllExecutionTimes(); + // Create a Spanner instance that will inline BeginTransaction calls. It also has no prepared + // sessions in the pool to prevent session preparing from interfering with test cases. + spanner = + SpannerOptions.newBuilder() + .setProjectId("[PROJECT]") + .setChannelProvider(channelProvider) + .setCredentials(NoCredentials.getInstance()) + .setInlineBeginForReadWriteTransaction(true) + .setSessionPoolOption( + SessionPoolOptions.newBuilder().setWriteSessionsFraction(0.0f).build()) + .build() + .getService(); + } + + @After + public void tearDown() throws Exception { + spanner.close(); + mockSpanner.reset(); + } + + @Test + public void testInlinedBeginTx() { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + long updateCount = + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Long run(TransactionContext transaction) throws Exception { + return transaction.executeUpdate(UPDATE_STATEMENT); + } + }); + assertThat(updateCount).isEqualTo(UPDATE_COUNT); + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + + @Test + public void testInlinedBeginTxAborted() { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + final AtomicBoolean firstAttempt = new AtomicBoolean(true); + long updateCount = + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Long run(TransactionContext transaction) throws Exception { + long res = transaction.executeUpdate(UPDATE_STATEMENT); + if (firstAttempt.getAndSet(false)) { + mockSpanner.abortTransaction(transaction); + } + return res; + } + }); + assertThat(updateCount).isEqualTo(UPDATE_COUNT); + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + // We have started 2 transactions, because the first transaction aborted. + assertThat(countTransactionsStarted()).isEqualTo(2); + } + + @Test + public void testInlinedBeginTxWithQuery() { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + long updateCount = + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Long run(TransactionContext transaction) throws Exception { + try (ResultSet rs = transaction.executeQuery(SELECT1)) { + while (rs.next()) { + return rs.getLong(0); + } + } + return 0L; + } + }); + assertThat(updateCount).isEqualTo(1L); + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + + @Test + public void testInlinedBeginTxWithBatchDml() { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + long[] updateCounts = + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public long[] run(TransactionContext transaction) throws Exception { + return transaction.batchUpdate( + Arrays.asList(UPDATE_STATEMENT, UPDATE_STATEMENT)); + } + }); + assertThat(updateCounts).asList().containsExactly(UPDATE_COUNT, UPDATE_COUNT); + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + + @Test + public void testInlinedBeginTxWithError() { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + long updateCount = + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Long run(TransactionContext transaction) throws Exception { + try { + transaction.executeUpdate(INVALID_UPDATE_STATEMENT); + fail("missing expected exception"); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + } + return transaction.executeUpdate(UPDATE_STATEMENT); + } + }); + assertThat(updateCount).isEqualTo(UPDATE_COUNT); + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + // The first update will start a transaction, but then fail the update statement. This will + // start a transaction on the mock server, but that transaction will never be returned to the + // client. + assertThat(countTransactionsStarted()).isEqualTo(2); + } + + @Test + public void testInlinedBeginTxWithParallelQueries() { + final int numQueries = 100; + final ScheduledExecutorService executor = Executors.newScheduledThreadPool(16); + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + long updateCount = + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Long run(final TransactionContext transaction) throws Exception { + List> futures = new ArrayList<>(numQueries); + for (int i = 0; i < numQueries; i++) { + futures.add( + executor.submit( + new Callable() { + @Override + public Long call() throws Exception { + try (ResultSet rs = transaction.executeQuery(SELECT1)) { + while (rs.next()) { + return rs.getLong(0); + } + } + return 0L; + } + })); + } + Long res = 0L; + for (Future f : futures) { + res += f.get(); + } + return res; + } + }); + assertThat(updateCount).isEqualTo(1L * numQueries); + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + + @Test + public void testInlinedBeginTxWithOnlyMutations() { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Void run(TransactionContext transaction) throws Exception { + transaction.buffer(Mutation.delete("FOO", Key.of(1L))); + return null; + } + }); + // There should be 1 call to BeginTransaction because there is no statement that we can use to + // inline the BeginTransaction call with. + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(1); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + + @SuppressWarnings("resource") + @Test + public void testTransactionManagerInlinedBeginTx() { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + try (TransactionManager txMgr = client.transactionManager()) { + TransactionContext txn = txMgr.begin(); + while (true) { + try { + assertThat(txn.executeUpdate(UPDATE_STATEMENT)).isEqualTo(UPDATE_COUNT); + txMgr.commit(); + break; + } catch (AbortedException e) { + txn = txMgr.resetForRetry(); + } + } + } + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + + @SuppressWarnings("resource") + @Test + public void testTransactionManagerInlinedBeginTxAborted() { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + try (TransactionManager txMgr = client.transactionManager()) { + TransactionContext txn = txMgr.begin(); + boolean first = true; + while (true) { + try { + assertThat(txn.executeUpdate(UPDATE_STATEMENT)).isEqualTo(UPDATE_COUNT); + if (first) { + mockSpanner.abortAllTransactions(); + first = false; + } + txMgr.commit(); + break; + } catch (AbortedException e) { + txn = txMgr.resetForRetry(); + } + } + } + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countTransactionsStarted()).isEqualTo(2); + } + + @SuppressWarnings("resource") + @Test + public void testTransactionManagerInlinedBeginTxWithOnlyMutations() { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + try (TransactionManager txMgr = client.transactionManager()) { + TransactionContext txn = txMgr.begin(); + while (true) { + try { + txn.buffer(Mutation.delete("FOO", Key.of(1L))); + txMgr.commit(); + break; + } catch (AbortedException e) { + txn = txMgr.resetForRetry(); + } + } + } + // There should be 1 call to BeginTransaction because there is no statement that we can use to + // inline the BeginTransaction call with. + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(1); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + + @SuppressWarnings("resource") + @Test + public void testTransactionManagerInlinedBeginTxWithError() { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + try (TransactionManager txMgr = client.transactionManager()) { + TransactionContext txn = txMgr.begin(); + while (true) { + try { + try { + txn.executeUpdate(INVALID_UPDATE_STATEMENT); + fail("missing expected exception"); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + } + assertThat(txn.executeUpdate(UPDATE_STATEMENT)).isEqualTo(UPDATE_COUNT); + txMgr.commit(); + break; + } catch (AbortedException e) { + txn = txMgr.resetForRetry(); + } + } + } + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + // The first statement will start a transaction, but it will never be returned to the client as + // the update statement fails. + assertThat(countTransactionsStarted()).isEqualTo(2); + } + + private int countRequests(Class requestType) { + int count = 0; + for (AbstractMessage msg : mockSpanner.getRequests()) { + if (msg.getClass().equals(requestType)) { + count++; + } + } + return count; + } + + private int countTransactionsStarted() { + return mockSpanner.getTransactionsStarted().size(); + } +} diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/IntegrationTestWithClosedSessionsEnv.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/IntegrationTestWithClosedSessionsEnv.java index edbc7976c0..4c50667a39 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/IntegrationTestWithClosedSessionsEnv.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/IntegrationTestWithClosedSessionsEnv.java @@ -47,7 +47,8 @@ private static class SpannerWithClosedSessionsImpl extends SpannerImpl { @Override DatabaseClientImpl createDatabaseClient(String clientId, SessionPool pool) { - return new DatabaseClientWithClosedSessionImpl(clientId, pool); + return new DatabaseClientWithClosedSessionImpl( + clientId, pool, getOptions().isInlineBeginForReadWriteTransaction()); } } @@ -59,8 +60,9 @@ public static class DatabaseClientWithClosedSessionImpl extends DatabaseClientIm private boolean invalidateNextSession = false; private boolean allowReplacing = true; - DatabaseClientWithClosedSessionImpl(String clientId, SessionPool pool) { - super(clientId, pool); + DatabaseClientWithClosedSessionImpl( + String clientId, SessionPool pool, boolean inlineBeginReadWriteTransactions) { + super(clientId, pool, inlineBeginReadWriteTransactions); } /** Invalidate the next session that is checked out from the pool. */ diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java index cae41510fc..6154c26f2f 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java @@ -505,6 +505,7 @@ private static void checkException(Queue exceptions, boolean keepExce private ConcurrentMap sessions = new ConcurrentHashMap<>(); private ConcurrentMap sessionLastUsed = new ConcurrentHashMap<>(); private ConcurrentMap transactions = new ConcurrentHashMap<>(); + private final Queue transactionsStarted = new ConcurrentLinkedQueue<>(); private ConcurrentMap isPartitionedDmlTransaction = new ConcurrentHashMap<>(); private ConcurrentMap abortedTransactions = new ConcurrentHashMap<>(); @@ -931,14 +932,6 @@ public void executeSql(ExecuteSqlRequest request, StreamObserver resp } } - private ResultSetMetadata createTransactionMetadata(TransactionSelector transactionSelector) { - if (transactionSelector.hasBegin() || transactionSelector.hasSingleUse()) { - Transaction transaction = getTemporaryTransactionOrNull(transactionSelector); - return ResultSetMetadata.newBuilder().setTransaction(transaction).build(); - } - return ResultSetMetadata.getDefaultInstance(); - } - private void returnResultSet( ResultSet resultSet, ByteString transactionId, @@ -1033,7 +1026,10 @@ public void executeBatchDml( ResultSet.newBuilder() .setStats( ResultSetStats.newBuilder().setRowCountExact(res.getUpdateCount()).build()) - .setMetadata(createTransactionMetadata(request.getTransaction())) + .setMetadata( + ResultSetMetadata.newBuilder() + .setTransaction(Transaction.newBuilder().setId(transactionId).build()) + .build()) .build()); } builder.setStatus(status); @@ -1597,6 +1593,7 @@ private Transaction beginTransaction(Session session, TransactionOptions options } Transaction transaction = builder.build(); transactions.put(transaction.getId(), transaction); + transactionsStarted.add(transaction.getId()); isPartitionedDmlTransaction.put( transaction.getId(), options.getModeCase() == ModeCase.PARTITIONED_DML); if (abortNextTransaction.getAndSet(false)) { @@ -1864,6 +1861,10 @@ public void waitForLastRequestToBe(Class type, long t } } + public List getTransactionsStarted() { + return new ArrayList<>(transactionsStarted); + } + @Override public void addResponse(AbstractMessage response) { throw new UnsupportedOperationException(); @@ -1896,6 +1897,7 @@ public void reset() { sessions = new ConcurrentHashMap<>(); sessionLastUsed = new ConcurrentHashMap<>(); transactions = new ConcurrentHashMap<>(); + transactionsStarted.clear(); isPartitionedDmlTransaction = new ConcurrentHashMap<>(); abortedTransactions = new ConcurrentHashMap<>(); transactionCounters = new ConcurrentHashMap<>(); 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 new file mode 100644 index 0000000000..6d8893b664 --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReadWriteTransactionWithInlineBeginTest.java @@ -0,0 +1,496 @@ +/* + * Copyright 2020 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.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import com.google.api.gax.grpc.testing.LocalChannelProvider; +import com.google.cloud.NoCredentials; +import com.google.cloud.spanner.MockSpannerServiceImpl.SimulatedExecutionTime; +import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult; +import com.google.cloud.spanner.TransactionRunner.TransactionCallable; +import com.google.protobuf.ListValue; +import com.google.spanner.v1.ResultSetMetadata; +import com.google.spanner.v1.StructType; +import com.google.spanner.v1.StructType.Field; +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.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class ReadWriteTransactionWithInlineBeginTest { + @Rule public ExpectedException exception = ExpectedException.none(); + + private static MockSpannerServiceImpl mockSpanner; + private static Server server; + 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 DatabaseClient client; + + @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())); + + String uniqueName = InProcessServerBuilder.generateName(); + 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() throws IOException { + mockSpanner.reset(); + mockSpanner.removeAllExecutionTimes(); + // Create a Spanner instance with no read/write prepared sessions in the pool. + spanner = + SpannerOptions.newBuilder() + .setProjectId("[PROJECT]") + .setInlineBeginForReadWriteTransaction(true) + .setSessionPoolOption( + SessionPoolOptions.newBuilder().setWriteSessionsFraction(0.0f).build()) + .setChannelProvider(channelProvider) + .setCredentials(NoCredentials.getInstance()) + .build() + .getService(); + // Make sure that calling BeginTransaction would lead to an error. + mockSpanner.setBeginTransactionExecutionTime( + SimulatedExecutionTime.ofException(Status.PERMISSION_DENIED.asRuntimeException())); + client = spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + } + + @After + public void tearDown() throws Exception { + spanner.close(); + } + + @Test + public void singleUpdate() { + Long updateCount = + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Long run(TransactionContext transaction) throws Exception { + return transaction.executeUpdate(UPDATE_STATEMENT); + } + }); + assertThat(updateCount).isEqualTo(UPDATE_COUNT); + } + + @Test + public void singleBatchUpdate() { + long[] updateCounts = + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public long[] run(TransactionContext transaction) throws Exception { + return transaction.batchUpdate( + Arrays.asList(UPDATE_STATEMENT, UPDATE_STATEMENT)); + } + }); + assertThat(updateCounts).isEqualTo(new long[] {UPDATE_COUNT, UPDATE_COUNT}); + } + + @Test + public void singleQuery() { + Long value = + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Long run(TransactionContext transaction) throws Exception { + try (ResultSet rs = transaction.executeQuery(SELECT1)) { + while (rs.next()) { + return rs.getLong(0); + } + } + return 0L; + } + }); + assertThat(value).isEqualTo(1L); + } + + @Test + public void updateAndQuery() { + long[] res = + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public long[] run(TransactionContext transaction) throws Exception { + long updateCount = transaction.executeUpdate(UPDATE_STATEMENT); + long val = 0L; + try (ResultSet rs = transaction.executeQuery(SELECT1)) { + while (rs.next()) { + val = rs.getLong(0); + } + } + return new long[] {updateCount, val}; + } + }); + assertThat(res).isEqualTo(new long[] {UPDATE_COUNT, 1L}); + } + + @Test + public void concurrentUpdates() { + final int updates = 100; + final ExecutorService service = Executors.newFixedThreadPool(8); + Long updateCount = + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Long run(final TransactionContext transaction) throws Exception { + List> list = new ArrayList<>(updates); + for (int i = 0; i < updates; i++) { + list.add( + service.submit( + new Callable() { + @Override + public Long call() throws Exception { + return transaction.executeUpdate(UPDATE_STATEMENT); + } + })); + } + long totalUpdateCount = 0L; + for (Future fut : list) { + totalUpdateCount += fut.get(); + } + return totalUpdateCount; + } + }); + assertThat(updateCount).isEqualTo(UPDATE_COUNT * updates); + } + + @Test + public void concurrentBatchUpdates() { + final int updates = 100; + final ExecutorService service = Executors.newFixedThreadPool(8); + Long updateCount = + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Long run(final TransactionContext transaction) throws Exception { + List> list = new ArrayList<>(updates); + for (int i = 0; i < updates; i++) { + list.add( + service.submit( + new Callable() { + @Override + public long[] call() throws Exception { + return transaction.batchUpdate( + Arrays.asList(UPDATE_STATEMENT, UPDATE_STATEMENT)); + } + })); + } + long totalUpdateCount = 0L; + for (Future fut : list) { + for (long l : fut.get()) { + totalUpdateCount += l; + } + } + return totalUpdateCount; + } + }); + assertThat(updateCount).isEqualTo(UPDATE_COUNT * updates * 2); + } + + @Test + public void concurrentQueries() { + final int queries = 100; + final ExecutorService service = Executors.newFixedThreadPool(8); + Long selectedTotal = + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Long run(final TransactionContext transaction) throws Exception { + List> list = new ArrayList<>(queries); + for (int i = 0; i < queries; i++) { + list.add( + service.submit( + new Callable() { + @Override + public Long call() throws Exception { + try (ResultSet rs = transaction.executeQuery(SELECT1)) { + while (rs.next()) { + return rs.getLong(0); + } + } + return 0L; + } + })); + } + long selectedTotal = 0L; + for (Future fut : list) { + selectedTotal += fut.get(); + } + return selectedTotal; + } + }); + assertThat(selectedTotal).isEqualTo(queries); + } + + @Test + public void failedUpdate() { + exception.expect(SpannerMatchers.isSpannerException(ErrorCode.INVALID_ARGUMENT)); + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Long run(TransactionContext transaction) throws Exception { + return transaction.executeUpdate(INVALID_UPDATE_STATEMENT); + } + }); + } + + @Test + public void failedBatchUpdate() { + exception.expect(SpannerMatchers.isSpannerException(ErrorCode.INVALID_ARGUMENT)); + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public long[] run(TransactionContext transaction) throws Exception { + return transaction.batchUpdate( + Arrays.asList(INVALID_UPDATE_STATEMENT, UPDATE_STATEMENT)); + } + }); + } + + @Test + public void failedQuery() { + exception.expect(SpannerMatchers.isSpannerException(ErrorCode.INVALID_ARGUMENT)); + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Void run(TransactionContext transaction) throws Exception { + try (ResultSet rs = transaction.executeQuery(INVALID_SELECT_STATEMENT)) { + rs.next(); + } + return null; + } + }); + } + + @Test + public void failedUpdateAndThenUpdate() { + Long updateCount = + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Long run(TransactionContext transaction) throws Exception { + try { + // This update statement carries the BeginTransaction, but fails. The + // BeginTransaction will then be carried by the subsequent statement. + transaction.executeUpdate(INVALID_UPDATE_STATEMENT); + fail("Missing expected exception"); + } catch (SpannerException e) { + if (e.getErrorCode() != ErrorCode.INVALID_ARGUMENT) { + fail("Error mismatch, expected INVALID_ARGUMENT"); + } + } + return transaction.executeUpdate(UPDATE_STATEMENT); + } + }); + assertThat(updateCount).isEqualTo(1L); + } + + @Test + public void failedBatchUpdateAndThenUpdate() { + Long updateCount = + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Long run(TransactionContext transaction) throws Exception { + try { + // This update statement carries the BeginTransaction, but fails. The + // BeginTransaction will then be carried by the subsequent statement. + transaction.batchUpdate( + Arrays.asList(INVALID_UPDATE_STATEMENT, UPDATE_STATEMENT)); + fail("Missing expected exception"); + } catch (SpannerException e) { + if (e.getErrorCode() != ErrorCode.INVALID_ARGUMENT) { + fail("Error mismatch, expected INVALID_ARGUMENT"); + } + } + return transaction.executeUpdate(UPDATE_STATEMENT); + } + }); + assertThat(updateCount).isEqualTo(1L); + } + + @Test + public void failedQueryAndThenUpdate() { + Long updateCount = + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Long run(TransactionContext transaction) throws Exception { + // 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)) { + rs.next(); + fail("Missing expected exception"); + } catch (SpannerException e) { + if (e.getErrorCode() != ErrorCode.INVALID_ARGUMENT) { + fail("Error mismatch, expected INVALID_ARGUMENT"); + } + } + return transaction.executeUpdate(UPDATE_STATEMENT); + } + }); + assertThat(updateCount).isEqualTo(1L); + } + + @Test + public void abortedUpdate() { + final AtomicInteger attempt = new AtomicInteger(); + Long updateCount = + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Long run(TransactionContext transaction) throws Exception { + if (attempt.incrementAndGet() == 1) { + // We use abortNextTransaction here, as the transaction context does not yet + // have a transaction (it will be requested by the first update statement). + mockSpanner.abortNextTransaction(); + } + return transaction.executeUpdate(UPDATE_STATEMENT); + } + }); + assertThat(updateCount).isEqualTo(UPDATE_COUNT); + assertThat(attempt.get()).isEqualTo(2); + } + + @Test + public void abortedBatchUpdate() { + final AtomicInteger attempt = new AtomicInteger(); + long[] updateCounts = + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public long[] run(TransactionContext transaction) throws Exception { + if (attempt.incrementAndGet() == 1) { + // We use abortNextTransaction here, as the transaction context does not yet + // have a transaction (it will be requested by the first update statement). + mockSpanner.abortNextTransaction(); + } + return transaction.batchUpdate( + Arrays.asList(UPDATE_STATEMENT, UPDATE_STATEMENT)); + } + }); + assertThat(updateCounts).isEqualTo(new long[] {UPDATE_COUNT, UPDATE_COUNT}); + assertThat(attempt.get()).isEqualTo(2); + } +} 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 c756a7898a..3827b2a280 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 @@ -31,6 +31,7 @@ import com.google.cloud.spanner.TransactionRunner.TransactionCallable; import com.google.cloud.spanner.spi.v1.SpannerRpc; import com.google.protobuf.ByteString; +import com.google.protobuf.Empty; import com.google.protobuf.ListValue; import com.google.protobuf.util.Timestamps; import com.google.spanner.v1.BeginTransactionRequest; @@ -40,6 +41,7 @@ import com.google.spanner.v1.PartialResultSet; import com.google.spanner.v1.ReadRequest; import com.google.spanner.v1.ResultSetMetadata; +import com.google.spanner.v1.RollbackRequest; import com.google.spanner.v1.Session; import com.google.spanner.v1.Transaction; import io.opencensus.trace.Span; @@ -85,6 +87,7 @@ public void setUp() { GrpcTransportOptions transportOptions = mock(GrpcTransportOptions.class); when(transportOptions.getExecutorFactory()).thenReturn(mock(ExecutorFactory.class)); when(spannerOptions.getTransportOptions()).thenReturn(transportOptions); + when(spannerOptions.getSessionPoolOptions()).thenReturn(mock(SessionPoolOptions.class)); @SuppressWarnings("resource") SpannerImpl spanner = new SpannerImpl(rpc, spannerOptions); String dbName = "projects/p1/instances/i1/databases/d1"; @@ -109,6 +112,8 @@ public void setUp() { .build(); Mockito.when(rpc.commitAsync(Mockito.any(CommitRequest.class), Mockito.any(Map.class))) .thenReturn(ApiFutures.immediateFuture(commitResponse)); + Mockito.when(rpc.rollbackAsync(Mockito.any(RollbackRequest.class), Mockito.anyMap())) + .thenReturn(ApiFutures.immediateFuture(Empty.getDefaultInstance())); session = spanner.getSessionClient(db).createSession(); ((SessionImpl) session).setCurrentSpan(mock(Span.class)); // We expect the same options, "options", on all calls on "session". 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 d5ea648bbd..8c7e920f78 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 @@ -1343,7 +1343,8 @@ public void testSessionNotFoundReadWriteTransaction() { .thenThrow(sessionNotFound); when(rpc.commitAsync(any(CommitRequest.class), any(Map.class))) .thenReturn(ApiFutures.immediateFailedFuture(sessionNotFound)); - doThrow(sessionNotFound).when(rpc).rollback(any(RollbackRequest.class), any(Map.class)); + when(rpc.rollbackAsync(any(RollbackRequest.class), any(Map.class))) + .thenReturn(ApiFutures.immediateFailedFuture(sessionNotFound)); final SessionImpl closedSession = mock(SessionImpl.class); when(closedSession.getName()) .thenReturn("projects/dummy/instances/dummy/database/dummy/sessions/session-closed"); @@ -1360,7 +1361,7 @@ public void testSessionNotFoundReadWriteTransaction() { when(closedSession.newTransaction()).thenReturn(closedTransactionContext); when(closedSession.beginTransactionAsync()).thenThrow(sessionNotFound); TransactionRunnerImpl closedTransactionRunner = - new TransactionRunnerImpl(closedSession, rpc, 10); + new TransactionRunnerImpl(closedSession, rpc, 10, false); closedTransactionRunner.setSpan(mock(Span.class)); when(closedSession.readWriteTransaction()).thenReturn(closedTransactionRunner); @@ -1374,7 +1375,7 @@ public void testSessionNotFoundReadWriteTransaction() { when(openSession.beginTransactionAsync()) .thenReturn(ApiFutures.immediateFuture(ByteString.copyFromUtf8("open-txn"))); TransactionRunnerImpl openTransactionRunner = - new TransactionRunnerImpl(openSession, mock(SpannerRpc.class), 10); + new TransactionRunnerImpl(openSession, mock(SpannerRpc.class), 10, false); openTransactionRunner.setSpan(mock(Span.class)); when(openSession.readWriteTransaction()).thenReturn(openTransactionRunner); @@ -1504,10 +1505,15 @@ public Integer run(TransactionContext transaction) { // The rollback will also cause a SessionNotFoundException, but this is caught, logged // and further ignored by the library, meaning that the session will not be re-created // for retry. Hence rollback at call 1. - assertThat( - executeStatementType == ReadWriteTransactionTestStatementType.EXCEPTION - && e.getMessage().contains("rollback at call 1")) - .isTrue(); + assertThat(executeStatementType) + .isEqualTo(ReadWriteTransactionTestStatementType.EXCEPTION); + assertThat(e.getMessage()).contains("rollback at call 1"); + + // assertThat( + // executeStatementType == + // ReadWriteTransactionTestStatementType.EXCEPTION + // && e.getMessage().contains("rollback at call 1")) + // .isTrue(); } } pool.closeAsync(new SpannerImpl.ClosedException()); @@ -1617,7 +1623,7 @@ public void run() { FakeClock clock = new FakeClock(); clock.currentTimeMillis = System.currentTimeMillis(); pool = createPool(clock); - DatabaseClientImpl impl = new DatabaseClientImpl(pool); + DatabaseClientImpl impl = new DatabaseClientImpl(pool, false); assertThat(impl.write(mutations)).isNotNull(); } @@ -1668,7 +1674,7 @@ public void run() { FakeClock clock = new FakeClock(); clock.currentTimeMillis = System.currentTimeMillis(); pool = createPool(clock); - DatabaseClientImpl impl = new DatabaseClientImpl(pool); + DatabaseClientImpl impl = new DatabaseClientImpl(pool, false); assertThat(impl.writeAtLeastOnce(mutations)).isNotNull(); } @@ -1719,7 +1725,7 @@ public void run() { FakeClock clock = new FakeClock(); clock.currentTimeMillis = System.currentTimeMillis(); pool = createPool(clock); - DatabaseClientImpl impl = new DatabaseClientImpl(pool); + DatabaseClientImpl impl = new DatabaseClientImpl(pool, false); assertThat(impl.executePartitionedUpdate(statement)).isEqualTo(1L); } 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 38aa66516e..340d24c55e 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 @@ -38,6 +38,10 @@ import com.google.spanner.v1.BeginTransactionRequest; import com.google.spanner.v1.CommitRequest; import com.google.spanner.v1.CommitResponse; +import com.google.spanner.v1.ExecuteSqlRequest; +import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions; +import com.google.spanner.v1.ResultSetMetadata; +import com.google.spanner.v1.ResultSetStats; import com.google.spanner.v1.Transaction; import io.opencensus.trace.Span; import java.util.Arrays; @@ -46,6 +50,7 @@ import java.util.UUID; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.atomic.AtomicInteger; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -77,7 +82,7 @@ public void release(ScheduledExecutorService exec) { @Before public void setUp() { initMocks(this); - manager = new TransactionManagerImpl(session, mock(Span.class)); + manager = new TransactionManagerImpl(session, mock(Span.class), false); } @Test @@ -284,4 +289,113 @@ public ApiFuture answer(InvocationOnMock invocation) .beginTransactionAsync(Mockito.any(BeginTransactionRequest.class), Mockito.anyMap()); } } + + @SuppressWarnings({"unchecked", "resource"}) + @Test + public void inlineBegin() { + SpannerOptions options = mock(SpannerOptions.class); + when(options.getNumChannels()).thenReturn(4); + GrpcTransportOptions transportOptions = mock(GrpcTransportOptions.class); + when(transportOptions.getExecutorFactory()).thenReturn(new TestExecutorFactory()); + when(options.getTransportOptions()).thenReturn(transportOptions); + SessionPoolOptions sessionPoolOptions = + SessionPoolOptions.newBuilder().setMinSessions(0).setIncStep(1).build(); + when(options.getSessionPoolOptions()).thenReturn(sessionPoolOptions); + when(options.isInlineBeginForReadWriteTransaction()).thenReturn(true); + when(options.getSessionLabels()).thenReturn(Collections.emptyMap()); + when(options.getDefaultQueryOptions(Mockito.any(DatabaseId.class))) + .thenReturn(QueryOptions.getDefaultInstance()); + SpannerRpc rpc = mock(SpannerRpc.class); + when(rpc.asyncDeleteSession(Mockito.anyString(), Mockito.anyMap())) + .thenReturn(ApiFutures.immediateFuture(Empty.getDefaultInstance())); + when(rpc.batchCreateSessions( + Mockito.anyString(), Mockito.eq(1), Mockito.anyMap(), Mockito.anyMap())) + .thenAnswer( + new Answer>() { + @Override + public List answer(InvocationOnMock invocation) + throws Throwable { + return Arrays.asList( + com.google.spanner.v1.Session.newBuilder() + .setName((String) invocation.getArguments()[0] + "/sessions/1") + .setCreateTime( + com.google.protobuf.Timestamp.newBuilder() + .setSeconds(System.currentTimeMillis() * 1000)) + .build()); + } + }); + when(rpc.beginTransactionAsync(Mockito.any(BeginTransactionRequest.class), Mockito.anyMap())) + .thenAnswer( + new Answer>() { + @Override + public ApiFuture answer(InvocationOnMock invocation) throws Throwable { + return ApiFutures.immediateFuture( + Transaction.newBuilder() + .setId(ByteString.copyFromUtf8(UUID.randomUUID().toString())) + .build()); + } + }); + final AtomicInteger transactionsStarted = new AtomicInteger(); + when(rpc.executeQuery(Mockito.any(ExecuteSqlRequest.class), Mockito.anyMap())) + .thenAnswer( + new Answer() { + @Override + public com.google.spanner.v1.ResultSet answer(InvocationOnMock invocation) + throws Throwable { + com.google.spanner.v1.ResultSet.Builder builder = + com.google.spanner.v1.ResultSet.newBuilder() + .setStats(ResultSetStats.newBuilder().setRowCountExact(1L).build()); + ExecuteSqlRequest request = invocation.getArgumentAt(0, ExecuteSqlRequest.class); + if (request.getTransaction() != null && request.getTransaction().hasBegin()) { + transactionsStarted.incrementAndGet(); + builder.setMetadata( + ResultSetMetadata.newBuilder() + .setTransaction( + Transaction.newBuilder() + .setId(ByteString.copyFromUtf8("test-tx")) + .build()) + .build()); + } + return builder.build(); + } + }); + when(rpc.commitAsync(Mockito.any(CommitRequest.class), Mockito.anyMap())) + .thenAnswer( + new Answer>() { + @Override + public ApiFuture answer(InvocationOnMock invocation) + throws Throwable { + return ApiFutures.immediateFuture( + CommitResponse.newBuilder() + .setCommitTimestamp( + com.google.protobuf.Timestamp.newBuilder() + .setSeconds(System.currentTimeMillis() * 1000)) + .build()); + } + }); + DatabaseId db = DatabaseId.of("test", "test", "test"); + try (SpannerImpl spanner = new SpannerImpl(rpc, options)) { + DatabaseClient client = spanner.getDatabaseClient(db); + try (TransactionManager mgr = client.transactionManager()) { + TransactionContext tx = mgr.begin(); + while (true) { + try { + tx.executeUpdate(Statement.of("UPDATE FOO SET BAR=1")); + tx.executeUpdate(Statement.of("UPDATE FOO SET BAZ=2")); + mgr.commit(); + break; + } catch (AbortedException e) { + tx = mgr.resetForRetry(); + } + } + } + // BeginTransaction should not be called, as we are inlining it with the ExecuteSql request. + verify(rpc, Mockito.never()) + .beginTransaction(Mockito.any(BeginTransactionRequest.class), Mockito.anyMap()); + // We should have 2 ExecuteSql requests. + verify(rpc, times(2)).executeQuery(Mockito.any(ExecuteSqlRequest.class), Mockito.anyMap()); + // But only 1 with a BeginTransaction. + assertThat(transactionsStarted.get()).isEqualTo(1); + } + } } 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 d61c89300f..8b66ecb959 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 @@ -44,8 +44,12 @@ import com.google.spanner.v1.CommitResponse; import com.google.spanner.v1.ExecuteBatchDmlRequest; import com.google.spanner.v1.ExecuteBatchDmlResponse; +import com.google.spanner.v1.ExecuteSqlRequest; +import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions; import com.google.spanner.v1.ResultSet; +import com.google.spanner.v1.ResultSetMetadata; import com.google.spanner.v1.ResultSetStats; +import com.google.spanner.v1.RollbackRequest; import com.google.spanner.v1.Transaction; import io.grpc.Metadata; import io.grpc.Status; @@ -90,19 +94,43 @@ public void release(ScheduledExecutorService exec) { @Mock private TransactionRunnerImpl.TransactionContextImpl txn; private TransactionRunnerImpl transactionRunner; private boolean firstRun; + private boolean usedInlinedBegin; @Before public void setUp() { MockitoAnnotations.initMocks(this); firstRun = true; when(session.newTransaction()).thenReturn(txn); - transactionRunner = new TransactionRunnerImpl(session, rpc, 1); + when(rpc.executeQuery(Mockito.any(ExecuteSqlRequest.class), Mockito.anyMap())) + .thenAnswer( + new Answer() { + @Override + public ResultSet answer(InvocationOnMock invocation) throws Throwable { + ResultSet.Builder builder = + ResultSet.newBuilder() + .setStats(ResultSetStats.newBuilder().setRowCountExact(1L).build()); + ExecuteSqlRequest request = invocation.getArgumentAt(0, ExecuteSqlRequest.class); + if (request.getTransaction().hasBegin() + && request.getTransaction().getBegin().hasReadWrite()) { + builder.setMetadata( + ResultSetMetadata.newBuilder() + .setTransaction( + Transaction.newBuilder().setId(ByteString.copyFromUtf8("test"))) + .build()); + usedInlinedBegin = true; + } + return builder.build(); + } + }); + transactionRunner = new TransactionRunnerImpl(session, rpc, 1, false); when(rpc.commitAsync(Mockito.any(CommitRequest.class), Mockito.anyMap())) .thenReturn( ApiFutures.immediateFuture( CommitResponse.newBuilder() .setCommitTimestamp(Timestamp.getDefaultInstance()) .build())); + when(rpc.rollbackAsync(Mockito.any(RollbackRequest.class), Mockito.anyMap())) + .thenReturn(ApiFutures.immediateFuture(Empty.getDefaultInstance())); transactionRunner.setSpan(mock(Span.class)); } @@ -274,6 +302,44 @@ public void batchDmlFailedPrecondition() { } } + @SuppressWarnings("unchecked") + @Test + public void inlineBegin() { + SpannerImpl spanner = mock(SpannerImpl.class); + when(spanner.getRpc()).thenReturn(rpc); + when(spanner.getDefaultQueryOptions(Mockito.any(DatabaseId.class))) + .thenReturn(QueryOptions.getDefaultInstance()); + SessionImpl session = + new SessionImpl( + spanner, "projects/p/instances/i/databases/d/sessions/s", Collections.EMPTY_MAP) { + @Override + public void prepareReadWriteTransaction() { + // Using a prepared transaction is not allowed when the beginTransaction should be + // inlined with the first statement. + throw new IllegalStateException(); + } + }; + session.setCurrentSpan(mock(Span.class)); + // Create a transaction runner that will inline the BeginTransaction call with the first + // statement. + TransactionRunnerImpl runner = new TransactionRunnerImpl(session, rpc, 10, true); + runner.setSpan(mock(Span.class)); + assertThat(usedInlinedBegin).isFalse(); + runner.run( + new TransactionCallable() { + @Override + public Void run(TransactionContext transaction) throws Exception { + transaction.executeUpdate(Statement.of("UPDATE FOO SET BAR=1 WHERE BAZ=2")); + return null; + } + }); + verify(rpc, Mockito.never()) + .beginTransaction(Mockito.any(BeginTransactionRequest.class), Mockito.anyMap()); + verify(rpc, Mockito.never()) + .beginTransactionAsync(Mockito.any(BeginTransactionRequest.class), Mockito.anyMap()); + assertThat(usedInlinedBegin).isTrue(); + } + @SuppressWarnings("unchecked") private long[] batchDmlException(int status) { Preconditions.checkArgument(status != Code.OK_VALUE); @@ -288,7 +354,7 @@ private long[] batchDmlException(int status) { .thenReturn( ApiFutures.immediateFuture(ByteString.copyFromUtf8(UUID.randomUUID().toString()))); when(session.getName()).thenReturn(SessionId.of("p", "i", "d", "test").getName()); - TransactionRunnerImpl runner = new TransactionRunnerImpl(session, rpc, 10); + TransactionRunnerImpl runner = new TransactionRunnerImpl(session, rpc, 10, false); runner.setSpan(mock(Span.class)); ExecuteBatchDmlResponse response1 = ExecuteBatchDmlResponse.newBuilder() diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDMLTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDMLTest.java index aabf93b3a6..b8a4ae81ae 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDMLTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDMLTest.java @@ -30,6 +30,7 @@ import com.google.cloud.spanner.Mutation; import com.google.cloud.spanner.ParallelIntegrationTest; import com.google.cloud.spanner.ResultSet; +import com.google.cloud.spanner.Spanner; import com.google.cloud.spanner.SpannerException; import com.google.cloud.spanner.SpannerExceptionFactory; import com.google.cloud.spanner.Statement; @@ -38,21 +39,24 @@ import com.google.cloud.spanner.TransactionRunner; import com.google.cloud.spanner.TransactionRunner.TransactionCallable; import java.util.Arrays; +import java.util.Collection; +import org.junit.After; import org.junit.Before; import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Test; import org.junit.experimental.categories.Category; import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; /** Integration tests for DML. */ @Category(ParallelIntegrationTest.class) -@RunWith(JUnit4.class) +@RunWith(Parameterized.class) public final class ITDMLTest { @ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv(); private static Database db; - private static DatabaseClient client; /** Sequence for assigning unique keys to test cases. */ private static int seq; @@ -67,6 +71,15 @@ public final class ITDMLTest { private static boolean throwAbortOnce = false; + @Parameters(name = "InlineBeginTx = {0}") + public static Collection data() { + return Arrays.asList(new Object[][] {{false}, {true}}); + } + + @Parameter public boolean inlineBeginTx; + private Spanner spanner; + private DatabaseClient client; + @BeforeClass public static void setUpDatabase() { db = @@ -76,14 +89,27 @@ public static void setUpDatabase() { + " K STRING(MAX) NOT NULL," + " V INT64," + ") PRIMARY KEY (K)"); - client = env.getTestHelper().getDatabaseClient(db); } @Before - public void increaseTestId() { + public void setupClient() { + spanner = + env.getTestHelper() + .getOptions() + .toBuilder() + .setInlineBeginForReadWriteTransaction(inlineBeginTx) + .build() + .getService(); + client = spanner.getDatabaseClient(db.getId()); + client.writeAtLeastOnce(Arrays.asList(Mutation.delete("T", KeySet.all()))); id++; } + @After + public void teardownClient() { + spanner.close(); + } + private static String uniqueKey() { return "k" + seq++; } From 59ff2aa93a4ffebbfed78bc65fa6447220ff0b28 Mon Sep 17 00:00:00 2001 From: Olav Loite Date: Wed, 8 Jul 2020 16:37:40 +0200 Subject: [PATCH 03/19] fix: invalid dml statement can still return tx id --- .../com/google/cloud/spanner/TransactionRunnerImpl.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 4f23cadd73..59b2219e19 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 @@ -484,13 +484,13 @@ public long executeUpdate(Statement statement) { try { com.google.spanner.v1.ResultSet resultSet = rpc.executeQuery(builder.build(), session.getOptions()); + if (resultSet.getMetadata().hasTransaction()) { + onTransactionMetadata(resultSet.getMetadata().getTransaction()); + } if (!resultSet.hasStats()) { throw new IllegalArgumentException( "DML response missing stats possibly due to non-DML statement as input"); } - if (resultSet.getMetadata().hasTransaction()) { - onTransactionMetadata(resultSet.getMetadata().getTransaction()); - } // For standard DML, using the exact row count. return resultSet.getStats().getRowCountExact(); } catch (SpannerException e) { From 4d4446f51dede347634b08d20a78ef9b790f0e05 Mon Sep 17 00:00:00 2001 From: Olav Loite Date: Thu, 16 Jul 2020 17:17:49 +0200 Subject: [PATCH 04/19] bench: add benchmarks for inline begin --- .../cloud/spanner/InlineBeginBenchmark.java | 239 ++++++++++++++++++ 1 file changed, 239 insertions(+) create mode 100644 google-cloud-spanner/src/test/java/com/google/cloud/spanner/InlineBeginBenchmark.java diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InlineBeginBenchmark.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InlineBeginBenchmark.java new file mode 100644 index 0000000000..c0d192dd2a --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InlineBeginBenchmark.java @@ -0,0 +1,239 @@ +/* + * Copyright 2020 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.common.truth.Truth.assertThat; + +import com.google.api.gax.rpc.TransportChannelProvider; +import com.google.cloud.NoCredentials; +import com.google.cloud.spanner.TransactionRunner.TransactionCallable; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningScheduledExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.Callable; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Warmup; + +/** + * Benchmarks for inlining the BeginTransaction RPC with the first statement of a transaction. The simulated execution times are based on + * reasonable estimates and are primarily intended to keep the benchmarks comparable with each other + * before and after changes have been made to the pool. The benchmarks are bound to the Maven + * profile `benchmark` and can be executed like this: + * mvn clean test -DskipTests -Pbenchmark -Dbenchmark.name=InlineBeginBenchmark + * + */ +@BenchmarkMode(Mode.AverageTime) +@Fork(value = 1, warmups = 0) +@Measurement(batchSize = 1, iterations = 1, timeUnit = TimeUnit.MILLISECONDS) +@Warmup(batchSize = 0, iterations = 0) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +public class InlineBeginBenchmark { + private static final String TEST_PROJECT = "my-project"; + private static final String TEST_INSTANCE = "my-instance"; + private static final String TEST_DATABASE = "my-database"; + private static final int HOLD_SESSION_TIME = 100; + private static final int RND_WAIT_TIME_BETWEEN_REQUESTS = 10; + private static final Random RND = new Random(); + + @State(Scope.Thread) + public static class BenchmarkState { + private StandardBenchmarkMockServer mockServer; + private Spanner spanner; + private DatabaseClientImpl client; + + @Param({"false", "true"}) + boolean inlineBegin; + + @Param({"0.0", "0.2"}) + float writeFraction; + + @Setup(Level.Invocation) + public void setup() throws Exception { + mockServer = new StandardBenchmarkMockServer(); + TransportChannelProvider channelProvider = mockServer.start(); + + SpannerOptions options = + SpannerOptions.newBuilder() + .setProjectId(TEST_PROJECT) + .setChannelProvider(channelProvider) + .setCredentials(NoCredentials.getInstance()) + .setSessionPoolOption( + SessionPoolOptions.newBuilder() + .setWriteSessionsFraction(writeFraction) + .build()) + .setInlineBeginForReadWriteTransaction(inlineBegin) + .build(); + + spanner = options.getService(); + client = + (DatabaseClientImpl) + spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); + // Wait until the session pool has initialized. + while (client.pool.getNumberOfSessionsInPool() + < spanner.getOptions().getSessionPoolOptions().getMinSessions()) { + Thread.sleep(1L); + } + } + + @TearDown(Level.Invocation) + public void teardown() throws Exception { + spanner.close(); + mockServer.shutdown(); + } + } + + /** Measures the time needed to execute a burst of read requests. */ + @Benchmark + public void burstRead(final BenchmarkState server) throws Exception { + int totalQueries = server.spanner.getOptions().getSessionPoolOptions().getMaxSessions() * 8; + int parallelThreads = server.spanner.getOptions().getSessionPoolOptions().getMaxSessions() * 2; + final DatabaseClient client = + server.spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); + SessionPool pool = ((DatabaseClientImpl) client).pool; + assertThat(pool.totalSessions()).isEqualTo(server.spanner.getOptions().getSessionPoolOptions().getMinSessions()); + + ListeningScheduledExecutorService service = + MoreExecutors.listeningDecorator(Executors.newScheduledThreadPool(parallelThreads)); + List> futures = new ArrayList<>(totalQueries); + for (int i = 0; i < totalQueries; i++) { + futures.add( + service.submit( + new Callable() { + @Override + public Void call() throws Exception { + Thread.sleep(RND.nextInt(RND_WAIT_TIME_BETWEEN_REQUESTS)); + try (ResultSet rs = + client.singleUse().executeQuery(StandardBenchmarkMockServer.SELECT1)) { + while (rs.next()) { + Thread.sleep(RND.nextInt(HOLD_SESSION_TIME)); + } + return null; + } + } + })); + } + Futures.allAsList(futures).get(); + service.shutdown(); + } + + /** Measures the time needed to execute a burst of write requests. */ + @Benchmark + public void burstWrite(final BenchmarkState server) throws Exception { + int totalWrites = server.spanner.getOptions().getSessionPoolOptions().getMaxSessions() * 8; + int parallelThreads = server.spanner.getOptions().getSessionPoolOptions().getMaxSessions() * 2; + final DatabaseClient client = + server.spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); + SessionPool pool = ((DatabaseClientImpl) client).pool; + assertThat(pool.totalSessions()).isEqualTo(server.spanner.getOptions().getSessionPoolOptions().getMinSessions()); + + ListeningScheduledExecutorService service = + MoreExecutors.listeningDecorator(Executors.newScheduledThreadPool(parallelThreads)); + List> futures = new ArrayList<>(totalWrites); + for (int i = 0; i < totalWrites; i++) { + futures.add( + service.submit( + new Callable() { + @Override + public Long call() throws Exception { + Thread.sleep(RND.nextInt(RND_WAIT_TIME_BETWEEN_REQUESTS)); + TransactionRunner runner = client.readWriteTransaction(); + return runner.run( + new TransactionCallable() { + @Override + public Long run(TransactionContext transaction) throws Exception { + return transaction.executeUpdate( + StandardBenchmarkMockServer.UPDATE_STATEMENT); + } + }); + } + })); + } + Futures.allAsList(futures).get(); + service.shutdown(); + } + + /** Measures the time needed to execute a burst of read and write requests. */ + @Benchmark + public void burstReadAndWrite(final BenchmarkState server) throws Exception { + int totalWrites = server.spanner.getOptions().getSessionPoolOptions().getMaxSessions() * 4; + int totalReads = server.spanner.getOptions().getSessionPoolOptions().getMaxSessions() * 4; + int parallelThreads = server.spanner.getOptions().getSessionPoolOptions().getMaxSessions() * 2; + final DatabaseClient client = + server.spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); + SessionPool pool = ((DatabaseClientImpl) client).pool; + assertThat(pool.totalSessions()).isEqualTo(server.spanner.getOptions().getSessionPoolOptions().getMinSessions()); + + ListeningScheduledExecutorService service = + MoreExecutors.listeningDecorator(Executors.newScheduledThreadPool(parallelThreads)); + List> futures = new ArrayList<>(totalReads + totalWrites); + for (int i = 0; i < totalWrites; i++) { + futures.add( + service.submit( + new Callable() { + @Override + public Long call() throws Exception { + Thread.sleep(RND.nextInt(RND_WAIT_TIME_BETWEEN_REQUESTS)); + TransactionRunner runner = client.readWriteTransaction(); + return runner.run( + new TransactionCallable() { + @Override + public Long run(TransactionContext transaction) throws Exception { + return transaction.executeUpdate( + StandardBenchmarkMockServer.UPDATE_STATEMENT); + } + }); + } + })); + } + for (int i = 0; i < totalReads; i++) { + futures.add( + service.submit( + new Callable() { + @Override + public Void call() throws Exception { + Thread.sleep(RND.nextInt(RND_WAIT_TIME_BETWEEN_REQUESTS)); + try (ResultSet rs = + client.singleUse().executeQuery(StandardBenchmarkMockServer.SELECT1)) { + while (rs.next()) { + Thread.sleep(RND.nextInt(HOLD_SESSION_TIME)); + } + return null; + } + } + })); + } + Futures.allAsList(futures).get(); + service.shutdown(); + } +} From 48a5db577dab88594dd7a823e86bdcfcf40ea75f Mon Sep 17 00:00:00 2001 From: Olav Loite Date: Fri, 17 Jul 2020 14:59:11 +0200 Subject: [PATCH 05/19] feat: add inline begin for async runner --- .../cloud/spanner/AbstractReadContext.java | 8 +- .../cloud/spanner/AbstractResultSet.java | 13 +- .../cloud/spanner/DatabaseClientImpl.java | 14 + .../com/google/cloud/spanner/SessionImpl.java | 5 +- .../cloud/spanner/TransactionRunnerImpl.java | 94 +++++-- .../cloud/spanner/GrpcResultSetTest.java | 8 +- .../cloud/spanner/InlineBeginBenchmark.java | 101 ++++--- .../spanner/InlineBeginTransactionTest.java | 246 +++++++++++++++++- .../cloud/spanner/ReadFormatTestRunner.java | 4 +- 9 files changed, 412 insertions(+), 81 deletions(-) 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 277772eb54..4ec5133b1a 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 @@ -633,7 +633,8 @@ CloseableIterator startStream(@Nullable ByteString resumeToken return stream; } }; - return new GrpcResultSet(stream, this); + return new GrpcResultSet( + stream, this, request.hasTransaction() && request.getTransaction().hasBegin()); } /** @@ -685,7 +686,7 @@ public void close() { public void onTransactionMetadata(Transaction transaction) {} @Override - public void onError(SpannerException e) {} + public void onError(SpannerException e, boolean withBeginTransaction) {} @Override public void onDone() {} @@ -746,7 +747,8 @@ CloseableIterator startStream(@Nullable ByteString resumeToken return stream; } }; - GrpcResultSet resultSet = new GrpcResultSet(stream, this); + GrpcResultSet resultSet = + new GrpcResultSet(stream, this, selector != null && selector.hasBegin()); return resultSet; } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractResultSet.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractResultSet.java index 43bef07ce7..3c5e60f51a 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractResultSet.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractResultSet.java @@ -81,7 +81,7 @@ interface Listener { void onTransactionMetadata(Transaction transaction) throws SpannerException; /** Called when the read finishes with an error. */ - void onError(SpannerException e); + void onError(SpannerException e, boolean withBeginTransaction); /** Called when the read finishes normally. */ void onDone(); @@ -91,14 +91,17 @@ interface Listener { static class GrpcResultSet extends AbstractResultSet> { private final GrpcValueIterator iterator; private final Listener listener; + private final boolean beginTransaction; private GrpcStruct currRow; private SpannerException error; private ResultSetStats statistics; private boolean closed; - GrpcResultSet(CloseableIterator iterator, Listener listener) { + GrpcResultSet( + CloseableIterator iterator, Listener listener, boolean beginTransaction) { this.iterator = new GrpcValueIterator(iterator); this.listener = listener; + this.beginTransaction = beginTransaction; } @Override @@ -127,7 +130,7 @@ public boolean next() throws SpannerException { } return hasNext; } catch (SpannerException e) { - throw yieldError(e); + throw yieldError(e, beginTransaction && currRow == null); } } @@ -149,9 +152,9 @@ public Type getType() { return currRow.getType(); } - private SpannerException yieldError(SpannerException e) { + private SpannerException yieldError(SpannerException e, boolean beginTransaction) { close(); - listener.onError(e); + listener.onError(e, beginTransaction); throw e; } } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java index 68f69fc97e..dd16e0a616 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java @@ -231,6 +231,10 @@ private TransactionManager inlinedTransactionManager() { @Override public AsyncRunner runAsync() { + return inlineBeginReadWriteTransactions ? inlinedRunAsync() : preparedRunAsync(); + } + + private AsyncRunner preparedRunAsync() { Span span = tracer.spanBuilder(READ_WRITE_TRANSACTION).startSpan(); try (Scope s = tracer.withSpan(span)) { return getReadWriteSession().runAsync(); @@ -240,6 +244,16 @@ public AsyncRunner runAsync() { } } + private AsyncRunner inlinedRunAsync() { + Span span = tracer.spanBuilder(READ_WRITE_TRANSACTION_WITH_INLINE_BEGIN).startSpan(); + try (Scope s = tracer.withSpan(span)) { + return getReadSession().runAsync(); + } catch (RuntimeException e) { + TraceUtil.endSpanWithFailure(span, e); + throw e; + } + } + @Override public AsyncTransactionManager transactionManagerAsync() { Span span = tracer.spanBuilder(READ_WRITE_TRANSACTION).startSpan(); 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 7b325dafd7..5214c5887a 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 @@ -240,7 +240,10 @@ public AsyncRunner runAsync() { return new AsyncRunnerImpl( setActive( new TransactionRunnerImpl( - this, spanner.getRpc(), spanner.getDefaultPrefetchChunks(), false))); + this, + spanner.getRpc(), + spanner.getDefaultPrefetchChunks(), + spanner.getOptions().isInlineBeginForReadWriteTransaction()))); } @Override 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 59b2219e19..4187b5358a 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 @@ -55,10 +55,10 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nullable; @@ -158,7 +158,8 @@ public void removeListener(Runnable listener) { * been created, the lock is released and concurrent requests can be executed on the * transaction. */ - private final ReentrantLock transactionLock = new ReentrantLock(); + // private final ReentrantLock transactionLock = new ReentrantLock(); + private volatile CountDownLatch transactionLatch = new CountDownLatch(0); private volatile ByteString transactionId; private Timestamp commitTimestamp; @@ -333,7 +334,7 @@ public void run() { } span.addAnnotation("Commit Failed", TraceUtil.getExceptionAnnotations(e)); TraceUtil.endSpanWithFailure(opSpan, e); - onError((SpannerException) e); + onError((SpannerException) e, false); res.setException(e); } } @@ -401,20 +402,38 @@ TransactionSelector getTransactionSelector() { try { // Wait if another request is already beginning, committing or rolling back the // transaction. - transactionLock.lockInterruptibly(); - // Check again if a transactionId is now available. It could be that the thread that was - // holding the lock and that had sent a statement with a BeginTransaction request caused - // an error and did not return a transaction. - if (transactionId == null) { - // Return a TransactionSelector that will start a new transaction as part of the - // statement that is being executed. - return TransactionSelector.newBuilder() - .setBegin( - TransactionOptions.newBuilder() - .setReadWrite(TransactionOptions.ReadWrite.getDefaultInstance())) - .build(); - } else { - transactionLock.unlock(); + + // transactionLock.lockInterruptibly(); + while (true) { + CountDownLatch latch; + synchronized (lock) { + latch = transactionLatch; + } + latch.await(); + + synchronized (lock) { + if (transactionLatch.getCount() > 0L) { + continue; + } + // Check again if a transactionId is now available. It could be that the thread that + // was + // holding the lock and that had sent a statement with a BeginTransaction request + // caused + // an error and did not return a transaction. + if (transactionId == null) { + transactionLatch = new CountDownLatch(1); + // Return a TransactionSelector that will start a new transaction as part of the + // statement that is being executed. + return TransactionSelector.newBuilder() + .setBegin( + TransactionOptions.newBuilder() + .setReadWrite(TransactionOptions.ReadWrite.getDefaultInstance())) + .build(); + } else { + // transactionLock.unlock(); + break; + } + } } } catch (InterruptedException e) { throw SpannerExceptionFactory.newSpannerExceptionForCancellation(null, e); @@ -430,18 +449,24 @@ public void onTransactionMetadata(Transaction transaction) { // transaction on this instance and release the lock to allow other statements to proceed. if (this.transactionId == null && transaction != null && transaction.getId() != null) { this.transactionId = transaction.getId(); - transactionLock.unlock(); + transactionLatch.countDown(); + // transactionLock.unlock(); } } @Override - public void onError(SpannerException e) { + public void onError(SpannerException e, boolean withBeginTransaction) { // Release the transactionLock if that is being held by this thread. That would mean that the // statement that was trying to start a transaction caused an error. The next statement should // in that case also include a BeginTransaction option. - if (transactionLock.isHeldByCurrentThread()) { - transactionLock.unlock(); + + // if (transactionLock.isHeldByCurrentThread()) { + // transactionLock.unlock(); + // } + if (withBeginTransaction) { + transactionLatch.countDown(); } + if (e.getErrorCode() == ErrorCode.ABORTED) { long delay = -1L; if (e instanceof AbortedException) { @@ -494,7 +519,7 @@ public long executeUpdate(Statement statement) { // For standard DML, using the exact row count. return resultSet.getStats().getRowCountExact(); } catch (SpannerException e) { - onError(e); + onError(e, builder.hasTransaction() && builder.getTransaction().hasBegin()); throw e; } } @@ -504,7 +529,7 @@ public ApiFuture executeUpdateAsync(Statement statement) { beforeReadOrQuery(); final ExecuteSqlRequest.Builder builder = getExecuteSqlRequestBuilder(statement, QueryMode.NORMAL); - ApiFuture resultSet; + final ApiFuture resultSet; try { // Register the update as an async operation that must finish before the transaction may // commit. @@ -538,7 +563,7 @@ public Long apply(ResultSet input) { @Override public Long apply(Throwable input) { SpannerException e = SpannerExceptionFactory.newSpannerException(input); - onError(e); + onError(e, builder.hasTransaction() && builder.getTransaction().hasBegin()); throw e; } }, @@ -547,6 +572,14 @@ public Long apply(Throwable input) { new Runnable() { @Override public void run() { + try { + if (resultSet.get().getMetadata().hasTransaction()) { + onTransactionMetadata(resultSet.get().getMetadata().getTransaction()); + } + } catch (ExecutionException | InterruptedException e) { + // Ignore this error here as it is handled by the future that is returned by the + // executeUpdateAsync method. + } decreaseAsyncOperations(); } }, @@ -582,7 +615,7 @@ public long[] batchUpdate(Iterable statements) { } return results; } catch (SpannerException e) { - onError(e); + onError(e, builder.hasTransaction() && builder.getTransaction().hasBegin()); throw e; } } @@ -610,6 +643,9 @@ public long[] apply(ExecuteBatchDmlResponse input) { long[] results = new long[input.getResultSetsCount()]; for (int i = 0; i < input.getResultSetsCount(); ++i) { results[i] = input.getResultSets(i).getStats().getRowCountExact(); + if (input.getResultSets(i).getMetadata().hasTransaction()) { + onTransactionMetadata(input.getResultSets(i).getMetadata().getTransaction()); + } } // If one of the DML statements was aborted, we should throw an aborted exception. // In all other cases, we should throw a BatchUpdateException. @@ -633,9 +669,13 @@ public void run() { try { updateCounts.get(); } catch (ExecutionException e) { - onError(SpannerExceptionFactory.newSpannerException(e.getCause())); + onError( + SpannerExceptionFactory.newSpannerException(e.getCause()), + builder.hasTransaction() && builder.getTransaction().hasBegin()); } catch (InterruptedException e) { - onError(SpannerExceptionFactory.propagateInterrupt(e)); + onError( + SpannerExceptionFactory.propagateInterrupt(e), + builder.hasTransaction() && builder.getTransaction().hasBegin()); } finally { decreaseAsyncOperations(); } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/GrpcResultSetTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/GrpcResultSetTest.java index 4952e179ad..9cf66dd222 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/GrpcResultSetTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/GrpcResultSetTest.java @@ -58,7 +58,7 @@ private static class NoOpListener implements AbstractResultSet.Listener { public void onTransactionMetadata(Transaction transaction) throws SpannerException {} @Override - public void onError(SpannerException e) {} + public void onError(SpannerException e, boolean withBeginTransaction) {} @Override public void onDone() {} @@ -76,11 +76,11 @@ public void cancel(@Nullable String message) {} public void request(int numMessages) {} }); consumer = stream.consumer(); - resultSet = new AbstractResultSet.GrpcResultSet(stream, new NoOpListener()); + resultSet = new AbstractResultSet.GrpcResultSet(stream, new NoOpListener(), false); } public AbstractResultSet.GrpcResultSet resultSetWithMode(QueryMode queryMode) { - return new AbstractResultSet.GrpcResultSet(stream, new NoOpListener()); + return new AbstractResultSet.GrpcResultSet(stream, new NoOpListener(), false); } @Test @@ -641,7 +641,7 @@ public com.google.protobuf.Value apply(@Nullable Value input) { private void verifySerialization( Function protoFn, Value... values) { - resultSet = new AbstractResultSet.GrpcResultSet(stream, new NoOpListener()); + resultSet = new AbstractResultSet.GrpcResultSet(stream, new NoOpListener(), false); PartialResultSet.Builder builder = PartialResultSet.newBuilder(); List types = new ArrayList<>(); for (Value value : values) { diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InlineBeginBenchmark.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InlineBeginBenchmark.java index c0d192dd2a..3e08f0f633 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InlineBeginBenchmark.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InlineBeginBenchmark.java @@ -21,10 +21,12 @@ import com.google.api.gax.rpc.TransportChannelProvider; import com.google.cloud.NoCredentials; import com.google.cloud.spanner.TransactionRunner.TransactionCallable; +import com.google.common.base.Stopwatch; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningScheduledExecutorService; import com.google.common.util.concurrent.MoreExecutors; +import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Random; @@ -46,10 +48,10 @@ import org.openjdk.jmh.annotations.Warmup; /** - * Benchmarks for inlining the BeginTransaction RPC with the first statement of a transaction. The simulated execution times are based on - * reasonable estimates and are primarily intended to keep the benchmarks comparable with each other - * before and after changes have been made to the pool. The benchmarks are bound to the Maven - * profile `benchmark` and can be executed like this: + * Benchmarks for inlining the BeginTransaction RPC with the first statement of a transaction. The + * simulated execution times are based on reasonable estimates and are primarily intended to keep + * the benchmarks comparable with each other before and after changes have been made to the pool. + * The benchmarks are bound to the Maven profile `benchmark` and can be executed like this: * mvn clean test -DskipTests -Pbenchmark -Dbenchmark.name=InlineBeginBenchmark * */ @@ -68,10 +70,13 @@ public class InlineBeginBenchmark { @State(Scope.Thread) public static class BenchmarkState { + private final boolean useRealServer = Boolean.valueOf(System.getProperty("useRealServer")); + private final String instance = System.getProperty("instance", TEST_INSTANCE); + private final String database = System.getProperty("database", TEST_DATABASE); private StandardBenchmarkMockServer mockServer; private Spanner spanner; private DatabaseClientImpl client; - + @Param({"false", "true"}) boolean inlineBegin; @@ -80,36 +85,61 @@ public static class BenchmarkState { @Setup(Level.Invocation) public void setup() throws Exception { - mockServer = new StandardBenchmarkMockServer(); - TransportChannelProvider channelProvider = mockServer.start(); - - SpannerOptions options = - SpannerOptions.newBuilder() - .setProjectId(TEST_PROJECT) - .setChannelProvider(channelProvider) - .setCredentials(NoCredentials.getInstance()) - .setSessionPoolOption( - SessionPoolOptions.newBuilder() - .setWriteSessionsFraction(writeFraction) - .build()) - .setInlineBeginForReadWriteTransaction(inlineBegin) - .build(); + System.out.println("useRealServer: " + System.getProperty("useRealServer")); + System.out.println("instance: " + System.getProperty("instance")); + SpannerOptions options; + if (useRealServer) { + System.out.println("running benchmark with **REAL** server"); + System.out.println("instance: " + instance); + System.out.println("database: " + database); + options = createRealServerOptions(); + } else { + System.out.println("running benchmark with **MOCK** server"); + mockServer = new StandardBenchmarkMockServer(); + TransportChannelProvider channelProvider = mockServer.start(); + options = createBenchmarkServerOptions(channelProvider); + } spanner = options.getService(); client = (DatabaseClientImpl) - spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); + spanner.getDatabaseClient(DatabaseId.of(options.getProjectId(), instance, database)); + Stopwatch watch = Stopwatch.createStarted(); // Wait until the session pool has initialized. while (client.pool.getNumberOfSessionsInPool() < spanner.getOptions().getSessionPoolOptions().getMinSessions()) { Thread.sleep(1L); + if (watch.elapsed(TimeUnit.SECONDS) > 10L) { + break; + } } } + SpannerOptions createBenchmarkServerOptions(TransportChannelProvider channelProvider) { + return SpannerOptions.newBuilder() + .setProjectId(TEST_PROJECT) + .setChannelProvider(channelProvider) + .setCredentials(NoCredentials.getInstance()) + .setSessionPoolOption( + SessionPoolOptions.newBuilder().setWriteSessionsFraction(writeFraction).build()) + .setInlineBeginForReadWriteTransaction(inlineBegin) + .build(); + } + + SpannerOptions createRealServerOptions() throws IOException { + return SpannerOptions.newBuilder() + .setSessionPoolOption( + SessionPoolOptions.newBuilder().setWriteSessionsFraction(writeFraction).build()) + .setInlineBeginForReadWriteTransaction(inlineBegin) + .build(); + } + @TearDown(Level.Invocation) public void teardown() throws Exception { spanner.close(); - mockServer.shutdown(); + if (mockServer != null) { + mockServer.shutdown(); + } } } @@ -118,10 +148,9 @@ public void teardown() throws Exception { public void burstRead(final BenchmarkState server) throws Exception { int totalQueries = server.spanner.getOptions().getSessionPoolOptions().getMaxSessions() * 8; int parallelThreads = server.spanner.getOptions().getSessionPoolOptions().getMaxSessions() * 2; - final DatabaseClient client = - server.spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); - SessionPool pool = ((DatabaseClientImpl) client).pool; - assertThat(pool.totalSessions()).isEqualTo(server.spanner.getOptions().getSessionPoolOptions().getMinSessions()); + SessionPool pool = server.client.pool; + assertThat(pool.totalSessions()) + .isEqualTo(server.spanner.getOptions().getSessionPoolOptions().getMinSessions()); ListeningScheduledExecutorService service = MoreExecutors.listeningDecorator(Executors.newScheduledThreadPool(parallelThreads)); @@ -134,7 +163,7 @@ public void burstRead(final BenchmarkState server) throws Exception { public Void call() throws Exception { Thread.sleep(RND.nextInt(RND_WAIT_TIME_BETWEEN_REQUESTS)); try (ResultSet rs = - client.singleUse().executeQuery(StandardBenchmarkMockServer.SELECT1)) { + server.client.singleUse().executeQuery(StandardBenchmarkMockServer.SELECT1)) { while (rs.next()) { Thread.sleep(RND.nextInt(HOLD_SESSION_TIME)); } @@ -152,10 +181,9 @@ public Void call() throws Exception { public void burstWrite(final BenchmarkState server) throws Exception { int totalWrites = server.spanner.getOptions().getSessionPoolOptions().getMaxSessions() * 8; int parallelThreads = server.spanner.getOptions().getSessionPoolOptions().getMaxSessions() * 2; - final DatabaseClient client = - server.spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); - SessionPool pool = ((DatabaseClientImpl) client).pool; - assertThat(pool.totalSessions()).isEqualTo(server.spanner.getOptions().getSessionPoolOptions().getMinSessions()); + SessionPool pool = server.client.pool; + assertThat(pool.totalSessions()) + .isEqualTo(server.spanner.getOptions().getSessionPoolOptions().getMinSessions()); ListeningScheduledExecutorService service = MoreExecutors.listeningDecorator(Executors.newScheduledThreadPool(parallelThreads)); @@ -167,7 +195,7 @@ public void burstWrite(final BenchmarkState server) throws Exception { @Override public Long call() throws Exception { Thread.sleep(RND.nextInt(RND_WAIT_TIME_BETWEEN_REQUESTS)); - TransactionRunner runner = client.readWriteTransaction(); + TransactionRunner runner = server.client.readWriteTransaction(); return runner.run( new TransactionCallable() { @Override @@ -189,10 +217,9 @@ public void burstReadAndWrite(final BenchmarkState server) throws Exception { int totalWrites = server.spanner.getOptions().getSessionPoolOptions().getMaxSessions() * 4; int totalReads = server.spanner.getOptions().getSessionPoolOptions().getMaxSessions() * 4; int parallelThreads = server.spanner.getOptions().getSessionPoolOptions().getMaxSessions() * 2; - final DatabaseClient client = - server.spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); - SessionPool pool = ((DatabaseClientImpl) client).pool; - assertThat(pool.totalSessions()).isEqualTo(server.spanner.getOptions().getSessionPoolOptions().getMinSessions()); + SessionPool pool = server.client.pool; + assertThat(pool.totalSessions()) + .isEqualTo(server.spanner.getOptions().getSessionPoolOptions().getMinSessions()); ListeningScheduledExecutorService service = MoreExecutors.listeningDecorator(Executors.newScheduledThreadPool(parallelThreads)); @@ -204,7 +231,7 @@ public void burstReadAndWrite(final BenchmarkState server) throws Exception { @Override public Long call() throws Exception { Thread.sleep(RND.nextInt(RND_WAIT_TIME_BETWEEN_REQUESTS)); - TransactionRunner runner = client.readWriteTransaction(); + TransactionRunner runner = server.client.readWriteTransaction(); return runner.run( new TransactionCallable() { @Override @@ -224,7 +251,7 @@ public Long run(TransactionContext transaction) throws Exception { public Void call() throws Exception { Thread.sleep(RND.nextInt(RND_WAIT_TIME_BETWEEN_REQUESTS)); try (ResultSet rs = - client.singleUse().executeQuery(StandardBenchmarkMockServer.SELECT1)) { + server.client.singleUse().executeQuery(StandardBenchmarkMockServer.SELECT1)) { while (rs.next()) { Thread.sleep(RND.nextInt(HOLD_SESSION_TIME)); } 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 f7c10848ad..f9f6b0cd2d 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 @@ -19,10 +19,18 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; +import com.google.api.core.ApiAsyncFunction; +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutures; +import com.google.api.core.SettableApiFuture; import com.google.api.gax.grpc.testing.LocalChannelProvider; import com.google.cloud.NoCredentials; +import com.google.cloud.spanner.AsyncResultSet.CallbackResponse; +import com.google.cloud.spanner.AsyncResultSet.ReadyCallback; +import com.google.cloud.spanner.AsyncRunner.AsyncWork; import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult; import com.google.cloud.spanner.TransactionRunner.TransactionCallable; +import com.google.common.util.concurrent.MoreExecutors; import com.google.protobuf.AbstractMessage; import com.google.protobuf.ListValue; import com.google.spanner.v1.BeginTransactionRequest; @@ -36,8 +44,12 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; @@ -49,10 +61,24 @@ import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; -@RunWith(JUnit4.class) +@RunWith(Parameterized.class) public class InlineBeginTransactionTest { + @Parameter public Executor executor; + + @Parameters(name = "executor = {0}") + public static Collection data() { + return Arrays.asList( + new Object[][] { + {MoreExecutors.directExecutor()}, + {Executors.newSingleThreadExecutor()}, + {Executors.newFixedThreadPool(4)} + }); + } + private static MockSpannerServiceImpl mockSpanner; private static Server server; private static LocalChannelProvider channelProvider; @@ -414,6 +440,222 @@ public void testTransactionManagerInlinedBeginTxWithError() { assertThat(countTransactionsStarted()).isEqualTo(2); } + @Test + public void testInlinedBeginAsyncTx() throws InterruptedException, ExecutionException { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + ApiFuture updateCount = + client + .runAsync() + .runAsync( + new AsyncWork() { + @Override + public ApiFuture doWorkAsync(TransactionContext txn) { + return txn.executeUpdateAsync(UPDATE_STATEMENT); + } + }, + executor); + assertThat(updateCount.get()).isEqualTo(UPDATE_COUNT); + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + + @Test + public void testInlinedBeginAsyncTxAborted() throws InterruptedException, ExecutionException { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + final AtomicBoolean firstAttempt = new AtomicBoolean(true); + ApiFuture updateCount = + client + .runAsync() + .runAsync( + new AsyncWork() { + @Override + public ApiFuture doWorkAsync(TransactionContext txn) { + ApiFuture res = txn.executeUpdateAsync(UPDATE_STATEMENT); + if (firstAttempt.getAndSet(false)) { + mockSpanner.abortTransaction(txn); + } + return res; + } + }, + executor); + assertThat(updateCount.get()).isEqualTo(UPDATE_COUNT); + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + // We have started 2 transactions, because the first transaction aborted. + assertThat(countTransactionsStarted()).isEqualTo(2); + } + + @Test + public void testInlinedBeginAsyncTxWithQuery() throws InterruptedException, ExecutionException { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + final ExecutorService queryExecutor = Executors.newSingleThreadExecutor(); + ApiFuture updateCount = + client + .runAsync() + .runAsync( + new AsyncWork() { + @Override + public ApiFuture doWorkAsync(TransactionContext txn) { + final SettableApiFuture res = SettableApiFuture.create(); + try (AsyncResultSet rs = txn.executeQueryAsync(SELECT1)) { + rs.setCallback( + executor, + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + switch (resultSet.tryNext()) { + case DONE: + return CallbackResponse.DONE; + case NOT_READY: + return CallbackResponse.CONTINUE; + case OK: + res.set(resultSet.getLong(0)); + default: + throw new IllegalStateException(); + } + } + }); + } + return res; + } + }, + queryExecutor); + assertThat(updateCount.get()).isEqualTo(1L); + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countTransactionsStarted()).isEqualTo(1); + queryExecutor.shutdown(); + } + + @Test + public void testInlinedBeginAsyncTxWithBatchDml() + throws InterruptedException, ExecutionException { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + ApiFuture updateCounts = + client + .runAsync() + .runAsync( + new AsyncWork() { + @Override + public ApiFuture doWorkAsync(TransactionContext transaction) { + return transaction.batchUpdateAsync( + Arrays.asList(UPDATE_STATEMENT, UPDATE_STATEMENT)); + } + }, + executor); + assertThat(updateCounts.get()).asList().containsExactly(UPDATE_COUNT, UPDATE_COUNT); + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + + @Test + public void testInlinedBeginAsyncTxWithError() throws InterruptedException, ExecutionException { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + ApiFuture updateCount = + client + .runAsync() + .runAsync( + new AsyncWork() { + @Override + public ApiFuture doWorkAsync(TransactionContext transaction) { + transaction.executeUpdateAsync(INVALID_UPDATE_STATEMENT); + return transaction.executeUpdateAsync(UPDATE_STATEMENT); + } + }, + executor); + assertThat(updateCount.get()).isEqualTo(UPDATE_COUNT); + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + // The first update will start a transaction, but then fail the update statement. This will + // start a transaction on the mock server, but that transaction will never be returned to the + // client. + assertThat(countTransactionsStarted()).isEqualTo(2); + } + + @Test + public void testInlinedBeginAsyncTxWithParallelQueries() + throws InterruptedException, ExecutionException { + final int numQueries = 100; + final ScheduledExecutorService executor = Executors.newScheduledThreadPool(16); + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + ApiFuture updateCount = + client + .runAsync() + .runAsync( + new AsyncWork() { + @Override + public ApiFuture doWorkAsync(final TransactionContext txn) { + List> futures = new ArrayList<>(numQueries); + for (int i = 0; i < numQueries; i++) { + final SettableApiFuture res = SettableApiFuture.create(); + try (AsyncResultSet rs = txn.executeQueryAsync(SELECT1)) { + rs.setCallback( + executor, + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + switch (resultSet.tryNext()) { + case DONE: + return CallbackResponse.DONE; + case NOT_READY: + return CallbackResponse.CONTINUE; + case OK: + res.set(resultSet.getLong(0)); + default: + throw new IllegalStateException(); + } + } + }); + } + futures.add(res); + } + return ApiFutures.transformAsync( + ApiFutures.allAsList(futures), + new ApiAsyncFunction, Long>() { + @Override + public ApiFuture apply(List input) throws Exception { + long sum = 0L; + for (Long l : input) { + sum += l; + } + return ApiFutures.immediateFuture(sum); + } + }, + MoreExecutors.directExecutor()); + } + }, + executor); + assertThat(updateCount.get()).isEqualTo(1L * numQueries); + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + + @Test + public void testInlinedBeginAsyncTxWithOnlyMutations() + throws InterruptedException, ExecutionException { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + client + .runAsync() + .runAsync( + new AsyncWork() { + @Override + public ApiFuture doWorkAsync(TransactionContext transaction) { + transaction.buffer(Mutation.delete("FOO", Key.of(1L))); + return ApiFutures.immediateFuture(null); + } + }, + executor) + .get(); + // There should be 1 call to BeginTransaction because there is no statement that we can use to + // inline the BeginTransaction call with. + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(1); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + private int countRequests(Class requestType) { int count = 0; for (AbstractMessage msg : mockSpanner.getRequests()) { diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReadFormatTestRunner.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReadFormatTestRunner.java index 475d8325a9..50cf96ff3c 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReadFormatTestRunner.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReadFormatTestRunner.java @@ -47,7 +47,7 @@ private static class NoOpListener implements AbstractResultSet.Listener { public void onTransactionMetadata(Transaction transaction) throws SpannerException {} @Override - public void onError(SpannerException e) {} + public void onError(SpannerException e, boolean withBeginTransaction) {} @Override public void onDone() {} @@ -119,7 +119,7 @@ public void cancel(@Nullable String message) {} public void request(int numMessages) {} }); consumer = stream.consumer(); - resultSet = new AbstractResultSet.GrpcResultSet(stream, new NoOpListener()); + resultSet = new AbstractResultSet.GrpcResultSet(stream, new NoOpListener(), false); JSONArray chunks = testCase.getJSONArray("chunks"); JSONObject expectedResult = testCase.getJSONObject("result"); From 261c9113f3408a627f5899fa808e619400bd41dc Mon Sep 17 00:00:00 2001 From: Olav Loite Date: Fri, 17 Jul 2020 20:10:22 +0200 Subject: [PATCH 06/19] test: add additional tests and ITs --- .../spanner/AsyncTransactionManagerImpl.java | 11 +- .../cloud/spanner/DatabaseClientImpl.java | 16 ++ .../com/google/cloud/spanner/SessionImpl.java | 3 +- .../spanner/InlineBeginTransactionTest.java | 162 ++++++++++++++++++ .../it/ITTransactionManagerAsyncTest.java | 36 +++- .../spanner/it/ITTransactionManagerTest.java | 43 ++++- .../cloud/spanner/it/ITTransactionTest.java | 45 ++++- 7 files changed, 297 insertions(+), 19 deletions(-) diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManagerImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManagerImpl.java index a306e7caa2..f6085adf38 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManagerImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManagerImpl.java @@ -39,14 +39,16 @@ final class AsyncTransactionManagerImpl private final SessionImpl session; private Span span; + private final boolean inlineBegin; private TransactionRunnerImpl.TransactionContextImpl txn; private TransactionState txnState; private final SettableApiFuture commitTimestamp = SettableApiFuture.create(); - AsyncTransactionManagerImpl(SessionImpl session, Span span) { + AsyncTransactionManagerImpl(SessionImpl session, Span span, boolean inlineBegin) { this.session = session; this.span = span; + this.inlineBegin = inlineBegin; } @Override @@ -74,7 +76,12 @@ private ApiFuture internalBeginAsync(boolean setActive) { session.setActive(this); } final SettableApiFuture res = SettableApiFuture.create(); - final ApiFuture fut = txn.ensureTxnAsync(); + final ApiFuture fut; + if (inlineBegin) { + fut = ApiFutures.immediateFuture(null); + } else { + fut = txn.ensureTxnAsync(); + } ApiFutures.addCallback( fut, new ApiFutureCallback() { diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java index dd16e0a616..3883134582 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java @@ -256,6 +256,12 @@ private AsyncRunner inlinedRunAsync() { @Override public AsyncTransactionManager transactionManagerAsync() { + return inlineBeginReadWriteTransactions + ? inlinedTransactionManagerAsync() + : preparedTransactionManagerAsync(); + } + + private AsyncTransactionManager preparedTransactionManagerAsync() { Span span = tracer.spanBuilder(READ_WRITE_TRANSACTION).startSpan(); try (Scope s = tracer.withSpan(span)) { return getReadWriteSession().transactionManagerAsync(); @@ -265,6 +271,16 @@ public AsyncTransactionManager transactionManagerAsync() { } } + private AsyncTransactionManager inlinedTransactionManagerAsync() { + Span span = tracer.spanBuilder(READ_WRITE_TRANSACTION_WITH_INLINE_BEGIN).startSpan(); + try (Scope s = tracer.withSpan(span)) { + return getReadSession().transactionManagerAsync(); + } catch (RuntimeException e) { + TraceUtil.endSpanWithFailure(span, e); + throw e; + } + } + @Override public long executePartitionedUpdate(final Statement stmt) { Span span = tracer.spanBuilder(PARTITION_DML_TRANSACTION).startSpan(); 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 5214c5887a..d707cf0ef6 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 @@ -254,7 +254,8 @@ public TransactionManager transactionManager() { @Override public AsyncTransactionManagerImpl transactionManagerAsync() { - return new AsyncTransactionManagerImpl(this, currentSpan); + return new AsyncTransactionManagerImpl( + this, currentSpan, spanner.getOptions().isInlineBeginForReadWriteTransaction()); } @Override 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 f9f6b0cd2d..9759f15361 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 @@ -28,6 +28,10 @@ import com.google.cloud.spanner.AsyncResultSet.CallbackResponse; import com.google.cloud.spanner.AsyncResultSet.ReadyCallback; import com.google.cloud.spanner.AsyncRunner.AsyncWork; +import com.google.cloud.spanner.AsyncTransactionManager.AsyncTransactionFunction; +import com.google.cloud.spanner.AsyncTransactionManager.AsyncTransactionStep; +import com.google.cloud.spanner.AsyncTransactionManager.CommitTimestampFuture; +import com.google.cloud.spanner.AsyncTransactionManager.TransactionContextFuture; import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult; import com.google.cloud.spanner.TransactionRunner.TransactionCallable; import com.google.common.util.concurrent.MoreExecutors; @@ -656,6 +660,164 @@ public ApiFuture doWorkAsync(TransactionContext transaction) { assertThat(countTransactionsStarted()).isEqualTo(1); } + @Test + public void testAsyncTransactionManagerInlinedBeginTx() + throws InterruptedException, ExecutionException { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + try (AsyncTransactionManager txMgr = client.transactionManagerAsync()) { + TransactionContextFuture txn = txMgr.beginAsync(); + while (true) { + AsyncTransactionStep updateCount = + txn.then( + new AsyncTransactionFunction() { + @Override + public ApiFuture apply(TransactionContext txn, Void input) + throws Exception { + return txn.executeUpdateAsync(UPDATE_STATEMENT); + } + }, + executor); + CommitTimestampFuture commitTimestamp = updateCount.commitAsync(); + try { + assertThat(updateCount.get()).isEqualTo(UPDATE_COUNT); + assertThat(commitTimestamp.get()).isNotNull(); + break; + } catch (AbortedException e) { + txn = txMgr.resetForRetryAsync(); + } + } + } + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + + @Test + public void testAsyncTransactionManagerInlinedBeginTxAborted() + throws InterruptedException, ExecutionException { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + try (AsyncTransactionManager txMgr = client.transactionManagerAsync()) { + TransactionContextFuture txn = txMgr.beginAsync(); + boolean first = true; + while (true) { + try { + AsyncTransactionStep updateCount = + txn.then( + new AsyncTransactionFunction() { + @Override + public ApiFuture apply(TransactionContext txn, Void input) + throws Exception { + return txn.executeUpdateAsync(UPDATE_STATEMENT); + } + }, + executor); + if (first) { + // Abort the transaction after the statement has been executed to ensure that the + // transaction has actually been started before the test tries to abort it. + updateCount.then( + new AsyncTransactionFunction() { + @Override + public ApiFuture apply(TransactionContext txn, Long input) + throws Exception { + mockSpanner.abortAllTransactions(); + return ApiFutures.immediateFuture(null); + } + }, + MoreExecutors.directExecutor()); + first = false; + } + assertThat(updateCount.commitAsync().get()).isNotNull(); + assertThat(updateCount.get()).isEqualTo(UPDATE_COUNT); + break; + } catch (AbortedException e) { + txn = txMgr.resetForRetryAsync(); + } + } + } + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countTransactionsStarted()).isEqualTo(2); + } + + @Test + public void testAsyncTransactionManagerInlinedBeginTxWithOnlyMutations() + throws InterruptedException, ExecutionException { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + try (AsyncTransactionManager txMgr = client.transactionManagerAsync()) { + TransactionContextFuture txn = txMgr.beginAsync(); + while (true) { + try { + txn.then( + new AsyncTransactionFunction() { + @Override + public ApiFuture apply(TransactionContext txn, Void input) + throws Exception { + txn.buffer(Mutation.delete("FOO", Key.of(1L))); + return ApiFutures.immediateFuture(null); + } + }, + executor) + .commitAsync() + .get(); + break; + } catch (AbortedException e) { + txn = txMgr.resetForRetryAsync(); + } + } + } + // There should be 1 call to BeginTransaction because there is no statement that we can use to + // inline the BeginTransaction call with. + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(1); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + + @Test + public void testAsyncTransactionManagerInlinedBeginTxWithError() + throws InterruptedException, ExecutionException { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + try (AsyncTransactionManager txMgr = client.transactionManagerAsync()) { + TransactionContextFuture txn = txMgr.beginAsync(); + while (true) { + try { + AsyncTransactionStep updateCount = + txn.then( + new AsyncTransactionFunction() { + @Override + public ApiFuture apply(TransactionContext txn, Void input) + throws Exception { + return txn.executeUpdateAsync(INVALID_UPDATE_STATEMENT); + } + }, + executor) + .then( + new AsyncTransactionFunction() { + @Override + public ApiFuture apply(TransactionContext txn, Long input) + throws Exception { + return txn.executeUpdateAsync(UPDATE_STATEMENT); + } + }, + executor); + try { + updateCount.commitAsync().get(); + fail("missing expected exception"); + } catch (ExecutionException e) { + assertThat(e.getCause()).isInstanceOf(SpannerException.class); + SpannerException se = (SpannerException) e.getCause(); + assertThat(se.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + } + break; + } catch (AbortedException e) { + txn = txMgr.resetForRetryAsync(); + } + } + } + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + private int countRequests(Class requestType) { int count = 0; for (AbstractMessage msg : mockSpanner.getRequests()) { diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionManagerAsyncTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionManagerAsyncTest.java index c802493dec..f51ac4b480 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionManagerAsyncTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionManagerAsyncTest.java @@ -35,6 +35,7 @@ import com.google.cloud.spanner.Key; import com.google.cloud.spanner.KeySet; import com.google.cloud.spanner.Mutation; +import com.google.cloud.spanner.Spanner; import com.google.cloud.spanner.SpannerException; import com.google.cloud.spanner.Struct; import com.google.cloud.spanner.TransactionContext; @@ -46,6 +47,7 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.Executors; +import org.junit.After; import org.junit.Before; import org.junit.BeforeClass; import org.junit.ClassRule; @@ -58,21 +60,29 @@ @RunWith(Parameterized.class) public class ITTransactionManagerAsyncTest { - @Parameter public Executor executor; + @Parameter(0) + public Executor executor; - @Parameters(name = "executor = {0}") + @Parameter(1) + public boolean inlineBegin; + + @Parameters(name = "executor = {0}, inlineBegin = {1}") public static Collection data() { return Arrays.asList( new Object[][] { - {MoreExecutors.directExecutor()}, - {Executors.newSingleThreadExecutor()}, - {Executors.newFixedThreadPool(4)} + {MoreExecutors.directExecutor(), false}, + {MoreExecutors.directExecutor(), true}, + {Executors.newSingleThreadExecutor(), false}, + {Executors.newSingleThreadExecutor(), true}, + {Executors.newFixedThreadPool(4), false}, + {Executors.newFixedThreadPool(4), true} }); } @ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv(); private static Database db; - private static DatabaseClient client; + private Spanner spanner; + private DatabaseClient client; @BeforeClass public static void setUpDatabase() { @@ -84,14 +94,26 @@ public static void setUpDatabase() { + " K STRING(MAX) NOT NULL," + " BoolValue BOOL," + ") PRIMARY KEY (K)"); - client = env.getTestHelper().getDatabaseClient(db); } @Before public void clearTable() { + spanner = + env.getTestHelper() + .getOptions() + .toBuilder() + .setInlineBeginForReadWriteTransaction(inlineBegin) + .build() + .getService(); + client = spanner.getDatabaseClient(db.getId()); client.write(ImmutableList.of(Mutation.delete("T", KeySet.all()))); } + @After + public void closeSpanner() { + spanner.close(); + } + @Test public void testSimpleInsert() throws ExecutionException, InterruptedException { try (AsyncTransactionManager manager = client.transactionManagerAsync()) { diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionManagerTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionManagerTest.java index 281977af6a..f912c0d710 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionManagerTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionManagerTest.java @@ -26,28 +26,45 @@ import com.google.cloud.spanner.DatabaseClient; 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.ParallelIntegrationTest; +import com.google.cloud.spanner.Spanner; import com.google.cloud.spanner.SpannerException; import com.google.cloud.spanner.Struct; import com.google.cloud.spanner.TransactionContext; import com.google.cloud.spanner.TransactionManager; import com.google.cloud.spanner.TransactionManager.TransactionState; +import com.google.common.collect.ImmutableList; import java.util.Arrays; +import java.util.Collection; +import org.junit.After; +import org.junit.Before; import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Test; import org.junit.experimental.categories.Category; import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; @Category(ParallelIntegrationTest.class) -@RunWith(JUnit4.class) +@RunWith(Parameterized.class) public class ITTransactionManagerTest { + @Parameter(0) + public boolean inlineBegin; + + @Parameters(name = "inlineBegin = {0}") + public static Collection data() { + return Arrays.asList(new Object[][] {{false}, {true}}); + } + @ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv(); private static Database db; - private static DatabaseClient client; + private Spanner spanner; + private DatabaseClient client; @BeforeClass public static void setUpDatabase() { @@ -59,7 +76,24 @@ public static void setUpDatabase() { + " K STRING(MAX) NOT NULL," + " BoolValue BOOL," + ") PRIMARY KEY (K)"); - client = env.getTestHelper().getDatabaseClient(db); + } + + @Before + public void setupClient() { + spanner = + env.getTestHelper() + .getOptions() + .toBuilder() + .setInlineBeginForReadWriteTransaction(inlineBegin) + .build() + .getService(); + client = spanner.getDatabaseClient(db.getId()); + client.write(ImmutableList.of(Mutation.delete("T", KeySet.all()))); + } + + @After + public void closeClient() { + spanner.close(); } @SuppressWarnings("resource") @@ -200,6 +234,7 @@ public void abortAndRetry() throws InterruptedException { Struct row = client.singleUse().readRow("T", Key.of("Key3"), Arrays.asList("K", "BoolValue")); assertThat(row.getString(0)).isEqualTo("Key3"); assertThat(row.getBoolean(1)).isTrue(); + manager2.close(); } } } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionTest.java index 5e3c1483e7..672a9e27f3 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionTest.java @@ -36,7 +36,9 @@ import com.google.cloud.spanner.ParallelIntegrationTest; import com.google.cloud.spanner.PartitionOptions; import com.google.cloud.spanner.ReadContext; +import com.google.cloud.spanner.ReadOnlyTransaction; import com.google.cloud.spanner.ResultSet; +import com.google.cloud.spanner.Spanner; import com.google.cloud.spanner.SpannerException; import com.google.cloud.spanner.Statement; import com.google.cloud.spanner.Struct; @@ -48,24 +50,39 @@ import com.google.common.util.concurrent.Uninterruptibles; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.Vector; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import org.junit.After; +import org.junit.Before; import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Test; import org.junit.experimental.categories.Category; import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; /** Integration tests for read-write transactions. */ @Category(ParallelIntegrationTest.class) -@RunWith(JUnit4.class) +@RunWith(Parameterized.class) public class ITTransactionTest { + + @Parameter(0) + public boolean inlineBegin; + + @Parameters(name = "inlineBegin = {0}") + public static Collection data() { + return Arrays.asList(new Object[][] {{false}, {true}}); + } + @ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv(); private static Database db; - private static DatabaseClient client; + private Spanner spanner; + private DatabaseClient client; /** Sequence for assigning unique keys to test cases. */ private static int seq; @@ -78,7 +95,23 @@ public static void setUpDatabase() { + " K STRING(MAX) NOT NULL," + " V INT64," + ") PRIMARY KEY (K)"); - client = env.getTestHelper().getDatabaseClient(db); + } + + @Before + public void setupClient() { + spanner = + env.getTestHelper() + .getOptions() + .toBuilder() + .setInlineBeginForReadWriteTransaction(inlineBegin) + .build() + .getService(); + client = spanner.getDatabaseClient(db.getId()); + } + + @After + public void closeClient() { + spanner.close(); } private static String uniqueKey() { @@ -427,7 +460,9 @@ public void nestedReadOnlyTxnThrows() { new TransactionCallable() { @Override public Void run(TransactionContext transaction) throws SpannerException { - client.readOnlyTransaction().getReadTimestamp(); + try (ReadOnlyTransaction tx = client.readOnlyTransaction()) { + tx.getReadTimestamp(); + } return null; } From af5066978406e106c72a113d2547f6b9309d12ad Mon Sep 17 00:00:00 2001 From: Olav Loite Date: Thu, 30 Jul 2020 16:27:38 +0200 Subject: [PATCH 07/19] test: add tests for error during tx --- .../spanner/InlineBeginTransactionTest.java | 60 ++++++++++++++++ .../cloud/spanner/it/ITTransactionTest.java | 68 +++++++++++++++++++ 2 files changed, 128 insertions(+) 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 9759f15361..57299ce1f2 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 @@ -38,7 +38,10 @@ 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.ExecuteSqlRequest; import com.google.spanner.v1.ResultSetMetadata; +import com.google.spanner.v1.RollbackRequest; import com.google.spanner.v1.StructType; import com.google.spanner.v1.StructType.Field; import com.google.spanner.v1.TypeCode; @@ -284,6 +287,63 @@ public Long run(TransactionContext transaction) throws Exception { assertThat(countTransactionsStarted()).isEqualTo(2); } + @Test + public void testInlinedBeginTxWithUncaughtError() { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + try { + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Long run(TransactionContext transaction) throws Exception { + return transaction.executeUpdate(INVALID_UPDATE_STATEMENT); + } + }); + fail("missing expected exception"); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + } + // The first update will start a transaction, but then fail the update statement. This will + // start a transaction on the mock server, but that transaction will never be returned to the + // client. + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countRequests(CommitRequest.class)).isEqualTo(0); + assertThat(countRequests(ExecuteSqlRequest.class)).isEqualTo(1); + // No rollback request will be initiated because the client does not receive any transaction id. + assertThat(countRequests(RollbackRequest.class)).isEqualTo(0); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + + @Test + public void testInlinedBeginTxWithUncaughtErrorAfterSuccessfulBegin() { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + try { + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Long run(TransactionContext transaction) throws Exception { + // This statement will start a transaction. + transaction.executeUpdate(UPDATE_STATEMENT); + // This statement will fail and cause a rollback as the exception is not caught. + return transaction.executeUpdate(INVALID_UPDATE_STATEMENT); + } + }); + fail("missing expected exception"); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + } + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countRequests(CommitRequest.class)).isEqualTo(0); + assertThat(countRequests(ExecuteSqlRequest.class)).isEqualTo(2); + assertThat(countRequests(RollbackRequest.class)).isEqualTo(1); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + @Test public void testInlinedBeginTxWithParallelQueries() { final int numQueries = 100; diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionTest.java index 672a9e27f3..fc563f0ac7 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionTest.java @@ -28,6 +28,7 @@ import com.google.cloud.spanner.BatchReadOnlyTransaction; import com.google.cloud.spanner.Database; import com.google.cloud.spanner.DatabaseClient; +import com.google.cloud.spanner.DatabaseId; import com.google.cloud.spanner.ErrorCode; import com.google.cloud.spanner.IntegrationTestEnv; import com.google.cloud.spanner.Key; @@ -45,6 +46,8 @@ import com.google.cloud.spanner.TimestampBound; import com.google.cloud.spanner.TransactionContext; import com.google.cloud.spanner.TransactionRunner; +import com.google.cloud.spanner.TransactionRunner.TransactionCallable; +import com.google.common.collect.ImmutableList; import com.google.common.collect.Sets; import com.google.common.util.concurrent.SettableFuture; import com.google.common.util.concurrent.Uninterruptibles; @@ -111,6 +114,7 @@ public void setupClient() { @After public void closeClient() { + client.writeAtLeastOnce(ImmutableList.of(Mutation.delete("T", KeySet.all()))); spanner.close(); } @@ -548,4 +552,68 @@ public Void run(TransactionContext transaction) throws SpannerException { } }); } + + @Test + public void testTxWithCaughtError() { + long updateCount = + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Long run(TransactionContext transaction) throws Exception { + try { + transaction.executeUpdate( + Statement.of("UPDATE NonExistingTable SET Foo=1 WHERE Bar=2")); + fail("missing expected exception"); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + } + return transaction.executeUpdate( + Statement.of("INSERT INTO T (K, V) VALUES ('One', 1)")); + } + }); + assertThat(updateCount).isEqualTo(1L); + } + + @Test + public void testTxWithUncaughtError() { + try { + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Long run(TransactionContext transaction) throws Exception { + return transaction.executeUpdate( + Statement.of("UPDATE NonExistingTable SET Foo=1 WHERE Bar=2")); + } + }); + fail("missing expected exception"); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + } + } + + @Test + public void testTxWithUncaughtErrorAfterSuccessfulBegin() { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + try { + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Long run(TransactionContext transaction) throws Exception { + transaction.executeUpdate(Statement.of("INSERT INTO T (K, V) VALUES ('One', 1)")); + return transaction.executeUpdate( + Statement.of("UPDATE NonExistingTable SET Foo=1 WHERE Bar=2")); + } + }); + fail("missing expected exception"); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + } + } } From f30334e839baaf556279e948bb632562627a6461 Mon Sep 17 00:00:00 2001 From: Olav Loite Date: Thu, 30 Jul 2020 16:43:58 +0200 Subject: [PATCH 08/19] test: use statement with same error code on emulator --- .../com/google/cloud/spanner/it/ITTransactionTest.java | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionTest.java index fc563f0ac7..8993e9cba6 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionTest.java @@ -563,8 +563,7 @@ public void testTxWithCaughtError() { @Override public Long run(TransactionContext transaction) throws Exception { try { - transaction.executeUpdate( - Statement.of("UPDATE NonExistingTable SET Foo=1 WHERE Bar=2")); + transaction.executeUpdate(Statement.of("UPDATE T SET V=2 WHERE")); fail("missing expected exception"); } catch (SpannerException e) { assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); @@ -585,8 +584,7 @@ public void testTxWithUncaughtError() { new TransactionCallable() { @Override public Long run(TransactionContext transaction) throws Exception { - return transaction.executeUpdate( - Statement.of("UPDATE NonExistingTable SET Foo=1 WHERE Bar=2")); + return transaction.executeUpdate(Statement.of("UPDATE T SET V=2 WHERE")); } }); fail("missing expected exception"); @@ -607,8 +605,7 @@ public void testTxWithUncaughtErrorAfterSuccessfulBegin() { @Override public Long run(TransactionContext transaction) throws Exception { transaction.executeUpdate(Statement.of("INSERT INTO T (K, V) VALUES ('One', 1)")); - return transaction.executeUpdate( - Statement.of("UPDATE NonExistingTable SET Foo=1 WHERE Bar=2")); + return transaction.executeUpdate(Statement.of("UPDATE T SET V=2 WHERE")); } }); fail("missing expected exception"); From a4d2e767239e563af25be5f42a4922b5ef43d410 Mon Sep 17 00:00:00 2001 From: Olav Loite Date: Thu, 30 Jul 2020 16:54:49 +0200 Subject: [PATCH 09/19] test: skip test on emulator --- .../com/google/cloud/spanner/it/ITTransactionTest.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionTest.java index 8993e9cba6..6073aad39a 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionTest.java @@ -28,7 +28,6 @@ import com.google.cloud.spanner.BatchReadOnlyTransaction; import com.google.cloud.spanner.Database; import com.google.cloud.spanner.DatabaseClient; -import com.google.cloud.spanner.DatabaseId; import com.google.cloud.spanner.ErrorCode; import com.google.cloud.spanner.IntegrationTestEnv; import com.google.cloud.spanner.Key; @@ -555,6 +554,10 @@ public Void run(TransactionContext transaction) throws SpannerException { @Test public void testTxWithCaughtError() { + assumeFalse( + "Emulator does not recover from an error within a transaction", + env.getTestHelper().isEmulator()); + long updateCount = client .readWriteTransaction() @@ -595,8 +598,6 @@ public Long run(TransactionContext transaction) throws Exception { @Test public void testTxWithUncaughtErrorAfterSuccessfulBegin() { - DatabaseClient client = - spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); try { client .readWriteTransaction() From 1817afade2005d3a3dbf61e25d096767b2ebfcb3 Mon Sep 17 00:00:00 2001 From: Olav Loite Date: Thu, 30 Jul 2020 20:57:02 +0200 Subject: [PATCH 10/19] test: constraint error causes transaction to be invalidated --- .../cloud/spanner/it/ITTransactionTest.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionTest.java index 6073aad39a..57d8e085c6 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionTest.java @@ -578,6 +578,45 @@ public Long run(TransactionContext transaction) throws Exception { assertThat(updateCount).isEqualTo(1L); } + @Test + public void testTxWithConstraintError() { + assumeFalse( + "Emulator does not recover from an error within a transaction", + env.getTestHelper().isEmulator()); + + // First insert a single row. + client.writeAtLeastOnce( + ImmutableList.of( + Mutation.newInsertOrUpdateBuilder("T").set("K").to("One").set("V").to(1L).build())); + + long updateCount = + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Long run(TransactionContext transaction) throws Exception { + try { + // Try to insert a duplicate row. This statement will fail. When the statement + // is executed against an already existing transaction (i.e. + // inlineBegin=false), the entire transaction will remain invalid and cannot + // be committed. When it is executed as the first statement of a transaction + // that also tries to start a transaction, then no transaction will be started + // and the next statement will start the transaction. This will cause the + // transaction to succeed. + transaction.executeUpdate( + Statement.of("INSERT INTO T (K, V) VALUES ('One', 1)")); + fail("missing expected exception"); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.ALREADY_EXISTS); + } + return transaction.executeUpdate( + Statement.of("INSERT INTO T (K, V) VALUES ('Two', 2)")); + } + }); + assertThat(updateCount).isEqualTo(1L); + } + @Test public void testTxWithUncaughtError() { try { From 61cc2072724f908d9eb21bc3cd9ea378d02e9d51 Mon Sep 17 00:00:00 2001 From: Olav Loite Date: Fri, 31 Jul 2020 15:50:40 +0200 Subject: [PATCH 11/19] fix: retry transaction if first statements fails and had BeginTransaction option --- .../cloud/spanner/TransactionManagerImpl.java | 3 +- .../cloud/spanner/TransactionRunnerImpl.java | 86 +++++----- .../spanner/InlineBeginTransactionTest.java | 35 +++- ...adWriteTransactionWithInlineBeginTest.java | 156 ++++++++++++------ .../cloud/spanner/it/ITTransactionTest.java | 54 +++--- 5 files changed, 204 insertions(+), 130 deletions(-) diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionManagerImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionManagerImpl.java index f5c505e6c5..3aa7a008ab 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionManagerImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionManagerImpl.java @@ -105,8 +105,9 @@ public TransactionContext resetForRetry() { "resetForRetry can only be called if the previous attempt" + " aborted"); } try (Scope s = tracer.withSpan(span)) { + boolean useInlinedBegin = inlineBegin && txn.transactionId != null; txn = session.newTransaction(); - if (!inlineBegin) { + if (!useInlinedBegin) { txn.ensureTxn(); } txnState = TransactionState.STARTED; 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 4187b5358a..88c8de2383 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 @@ -55,7 +55,6 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.Callable; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicInteger; @@ -153,15 +152,13 @@ public void removeListener(Runnable listener) { private long retryDelayInMillis = -1L; /** - * transactionLock guards that only one request can be beginning the transaction at any time. We - * only hold on to this lock while a request is creating a transaction. After a transaction has - * been created, the lock is released and concurrent requests can be executed on the + * transactionIdFuture will return the transaction id returned by the first statement in the + * transaction if the BeginTransaction option is included with the first statement of the * transaction. */ - // private final ReentrantLock transactionLock = new ReentrantLock(); - private volatile CountDownLatch transactionLatch = new CountDownLatch(0); + private volatile SettableApiFuture transactionIdFuture = null; - private volatile ByteString transactionId; + volatile ByteString transactionId; private Timestamp commitTimestamp; private TransactionContextImpl(Builder builder) { @@ -402,39 +399,30 @@ TransactionSelector getTransactionSelector() { try { // Wait if another request is already beginning, committing or rolling back the // transaction. - - // transactionLock.lockInterruptibly(); - while (true) { - CountDownLatch latch; - synchronized (lock) { - latch = transactionLatch; + ApiFuture tx = null; + synchronized (lock) { + if (transactionIdFuture == null) { + transactionIdFuture = SettableApiFuture.create(); + } else { + tx = transactionIdFuture; } - latch.await(); - + } + if (tx == null) { + return TransactionSelector.newBuilder() + .setBegin( + TransactionOptions.newBuilder() + .setReadWrite(TransactionOptions.ReadWrite.getDefaultInstance())) + .build(); + } else { + TransactionSelector.newBuilder().setId(tx.get()).build(); + } + } catch (ExecutionException e) { + if (e.getCause() instanceof AbortedException) { synchronized (lock) { - if (transactionLatch.getCount() > 0L) { - continue; - } - // Check again if a transactionId is now available. It could be that the thread that - // was - // holding the lock and that had sent a statement with a BeginTransaction request - // caused - // an error and did not return a transaction. - if (transactionId == null) { - transactionLatch = new CountDownLatch(1); - // Return a TransactionSelector that will start a new transaction as part of the - // statement that is being executed. - return TransactionSelector.newBuilder() - .setBegin( - TransactionOptions.newBuilder() - .setReadWrite(TransactionOptions.ReadWrite.getDefaultInstance())) - .build(); - } else { - // transactionLock.unlock(); - break; - } + aborted = true; } } + throw SpannerExceptionFactory.newSpannerException(e.getCause()); } catch (InterruptedException e) { throw SpannerExceptionFactory.newSpannerExceptionForCancellation(null, e); } @@ -449,8 +437,7 @@ public void onTransactionMetadata(Transaction transaction) { // transaction on this instance and release the lock to allow other statements to proceed. if (this.transactionId == null && transaction != null && transaction.getId() != null) { this.transactionId = transaction.getId(); - transactionLatch.countDown(); - // transactionLock.unlock(); + this.transactionIdFuture.set(transaction.getId()); } } @@ -459,12 +446,15 @@ public void onError(SpannerException e, boolean withBeginTransaction) { // Release the transactionLock if that is being held by this thread. That would mean that the // statement that was trying to start a transaction caused an error. The next statement should // in that case also include a BeginTransaction option. - - // if (transactionLock.isHeldByCurrentThread()) { - // transactionLock.unlock(); - // } if (withBeginTransaction) { - transactionLatch.countDown(); + // Simulate an aborted transaction to force a retry with a new transaction. + this.transactionIdFuture.setException( + SpannerExceptionFactory.newSpannerException( + ErrorCode.ABORTED, "Aborted due to failed initial statement", e)); + // synchronized (lock) { + // retryDelayInMillis = 0; + // aborted = true; + // } } if (e.getErrorCode() == ErrorCode.ABORTED) { @@ -758,21 +748,25 @@ private T runInternal(final TransactionCallable txCallable) { new Callable() { @Override public T call() { + boolean useInlinedBegin = inlineBegin; if (attempt.get() > 0) { + if (useInlinedBegin) { + // Do not inline the BeginTransaction during a retry if the initial attempt did not + // actually start a transaction. + useInlinedBegin = txn.transactionId != null; + } txn = session.newTransaction(); } checkState( isValid, "TransactionRunner has been invalidated by a new operation on the session"); attempt.incrementAndGet(); - // TODO(user): When using streaming reads, consider using the first read to begin - // the txn. span.addAnnotation( "Starting Transaction Attempt", ImmutableMap.of("Attempt", AttributeValue.longAttributeValue(attempt.longValue()))); // Only ensure that there is a transaction if we should not inline the beginTransaction // with the first statement. - if (!inlineBegin) { + if (!useInlinedBegin) { txn.ensureTxn(); } 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 57299ce1f2..45762baced 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 @@ -280,7 +280,10 @@ public Long run(TransactionContext transaction) throws Exception { } }); assertThat(updateCount).isEqualTo(UPDATE_COUNT); - assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + // The transaction will be retried because the first statement that also tried to include the + // BeginTransaction statement failed and did not return a transaction. That forces a retry of + // the entire transaction with an explicit BeginTransaction RPC. + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(1); // The first update will start a transaction, but then fail the update statement. This will // start a transaction on the mock server, but that transaction will never be returned to the // client. @@ -498,12 +501,36 @@ public void testTransactionManagerInlinedBeginTxWithError() { } } } - assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + // The first statement will fail and not return a transaction id. This will trigger a retry of + // the entire transaction, and the retry will do an explicit BeginTransaction RPC. + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(1); // The first statement will start a transaction, but it will never be returned to the client as // the update statement fails. assertThat(countTransactionsStarted()).isEqualTo(2); } + @SuppressWarnings("resource") + @Test + public void testTransactionManagerInlinedBeginTxWithUncaughtError() { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + try (TransactionManager txMgr = client.transactionManager()) { + TransactionContext txn = txMgr.begin(); + while (true) { + try { + txn.executeUpdate(INVALID_UPDATE_STATEMENT); + fail("missing expected exception"); + } catch (AbortedException e) { + txn = txMgr.resetForRetry(); + } + } + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + } + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + @Test public void testInlinedBeginAsyncTx() throws InterruptedException, ExecutionException { DatabaseClient client = @@ -631,7 +658,9 @@ public ApiFuture doWorkAsync(TransactionContext transaction) { }, executor); assertThat(updateCount.get()).isEqualTo(UPDATE_COUNT); - assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + // The first statement will fail and not return a transaction id. This will trigger a retry of + // the entire transaction, and the retry will do an explicit BeginTransaction RPC. + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(1); // The first update will start a transaction, but then fail the update statement. This will // start a transaction on the mock server, but that transaction will never be returned to the // client. 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 6d8893b664..c6ff7cdbf2 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 @@ -21,10 +21,11 @@ import com.google.api.gax.grpc.testing.LocalChannelProvider; import com.google.cloud.NoCredentials; -import com.google.cloud.spanner.MockSpannerServiceImpl.SimulatedExecutionTime; import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult; import com.google.cloud.spanner.TransactionRunner.TransactionCallable; +import com.google.protobuf.AbstractMessage; import com.google.protobuf.ListValue; +import com.google.spanner.v1.BeginTransactionRequest; import com.google.spanner.v1.ResultSetMetadata; import com.google.spanner.v1.StructType; import com.google.spanner.v1.StructType.Field; @@ -46,16 +47,12 @@ import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public class ReadWriteTransactionWithInlineBeginTest { - @Rule public ExpectedException exception = ExpectedException.none(); - private static MockSpannerServiceImpl mockSpanner; private static Server server; private static LocalChannelProvider channelProvider; @@ -139,9 +136,6 @@ public void setUp() throws IOException { .setCredentials(NoCredentials.getInstance()) .build() .getService(); - // Make sure that calling BeginTransaction would lead to an error. - mockSpanner.setBeginTransactionExecutionTime( - SimulatedExecutionTime.ofException(Status.PERMISSION_DENIED.asRuntimeException())); client = spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); } @@ -163,6 +157,8 @@ public Long run(TransactionContext transaction) throws Exception { } }); assertThat(updateCount).isEqualTo(UPDATE_COUNT); + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countTransactionsStarted()).isEqualTo(1); } @Test @@ -179,6 +175,8 @@ public long[] run(TransactionContext transaction) throws Exception { } }); assertThat(updateCounts).isEqualTo(new long[] {UPDATE_COUNT, UPDATE_COUNT}); + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countTransactionsStarted()).isEqualTo(1); } @Test @@ -199,6 +197,8 @@ public Long run(TransactionContext transaction) throws Exception { } }); assertThat(value).isEqualTo(1L); + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countTransactionsStarted()).isEqualTo(1); } @Test @@ -221,6 +221,8 @@ public long[] run(TransactionContext transaction) throws Exception { } }); assertThat(res).isEqualTo(new long[] {UPDATE_COUNT, 1L}); + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countTransactionsStarted()).isEqualTo(1); } @Test @@ -253,6 +255,8 @@ public Long call() throws Exception { } }); assertThat(updateCount).isEqualTo(UPDATE_COUNT * updates); + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countTransactionsStarted()).isEqualTo(1); } @Test @@ -288,6 +292,8 @@ public long[] call() throws Exception { } }); assertThat(updateCount).isEqualTo(UPDATE_COUNT * updates * 2); + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countTransactionsStarted()).isEqualTo(1); } @Test @@ -325,52 +331,72 @@ public Long call() throws Exception { } }); assertThat(selectedTotal).isEqualTo(queries); + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countTransactionsStarted()).isEqualTo(1); } @Test public void failedUpdate() { - exception.expect(SpannerMatchers.isSpannerException(ErrorCode.INVALID_ARGUMENT)); - client - .readWriteTransaction() - .run( - new TransactionCallable() { - @Override - public Long run(TransactionContext transaction) throws Exception { - return transaction.executeUpdate(INVALID_UPDATE_STATEMENT); - } - }); + try { + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Long run(TransactionContext transaction) throws Exception { + return transaction.executeUpdate(INVALID_UPDATE_STATEMENT); + } + }); + fail("missing expected exception"); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + } + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countTransactionsStarted()).isEqualTo(1); } @Test public void failedBatchUpdate() { - exception.expect(SpannerMatchers.isSpannerException(ErrorCode.INVALID_ARGUMENT)); - client - .readWriteTransaction() - .run( - new TransactionCallable() { - @Override - public long[] run(TransactionContext transaction) throws Exception { - return transaction.batchUpdate( - Arrays.asList(INVALID_UPDATE_STATEMENT, UPDATE_STATEMENT)); - } - }); + try { + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public long[] run(TransactionContext transaction) throws Exception { + return transaction.batchUpdate( + Arrays.asList(INVALID_UPDATE_STATEMENT, UPDATE_STATEMENT)); + } + }); + fail("missing expected exception"); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + } + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countTransactionsStarted()).isEqualTo(1); } @Test public void failedQuery() { - exception.expect(SpannerMatchers.isSpannerException(ErrorCode.INVALID_ARGUMENT)); - client - .readWriteTransaction() - .run( - new TransactionCallable() { - @Override - public Void run(TransactionContext transaction) throws Exception { - try (ResultSet rs = transaction.executeQuery(INVALID_SELECT_STATEMENT)) { - rs.next(); + try { + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Void run(TransactionContext transaction) throws Exception { + try (ResultSet rs = transaction.executeQuery(INVALID_SELECT_STATEMENT)) { + rs.next(); + } + return null; } - return null; - } - }); + }); + fail("missing expected exception"); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + } + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countTransactionsStarted()).isEqualTo(1); } @Test @@ -383,19 +409,21 @@ public void failedUpdateAndThenUpdate() { @Override public Long run(TransactionContext transaction) throws Exception { try { - // This update statement carries the BeginTransaction, but fails. The - // BeginTransaction will then be carried by the subsequent statement. + // This update statement carries the BeginTransaction, but fails. This will + // cause the entire transaction to be retried with an explicit + // BeginTransaction RPC to ensure all statements in the transaction are + // actually executed against the same transaction. transaction.executeUpdate(INVALID_UPDATE_STATEMENT); fail("Missing expected exception"); } catch (SpannerException e) { - if (e.getErrorCode() != ErrorCode.INVALID_ARGUMENT) { - fail("Error mismatch, expected INVALID_ARGUMENT"); - } + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); } return transaction.executeUpdate(UPDATE_STATEMENT); } }); assertThat(updateCount).isEqualTo(1L); + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(1); + assertThat(countTransactionsStarted()).isEqualTo(2); } @Test @@ -408,20 +436,22 @@ public void failedBatchUpdateAndThenUpdate() { @Override public Long run(TransactionContext transaction) throws Exception { try { - // This update statement carries the BeginTransaction, but fails. The - // BeginTransaction will then be carried by the subsequent statement. + // This update statement carries the BeginTransaction, but fails. This will + // cause the entire transaction to be retried with an explicit + // BeginTransaction RPC to ensure all statements in the transaction are + // actually executed against the same transaction. transaction.batchUpdate( Arrays.asList(INVALID_UPDATE_STATEMENT, UPDATE_STATEMENT)); fail("Missing expected exception"); } catch (SpannerException e) { - if (e.getErrorCode() != ErrorCode.INVALID_ARGUMENT) { - fail("Error mismatch, expected INVALID_ARGUMENT"); - } + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); } return transaction.executeUpdate(UPDATE_STATEMENT); } }); assertThat(updateCount).isEqualTo(1L); + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(1); + assertThat(countTransactionsStarted()).isEqualTo(2); } @Test @@ -439,14 +469,14 @@ public Long run(TransactionContext transaction) throws Exception { rs.next(); fail("Missing expected exception"); } catch (SpannerException e) { - if (e.getErrorCode() != ErrorCode.INVALID_ARGUMENT) { - fail("Error mismatch, expected INVALID_ARGUMENT"); - } + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); } return transaction.executeUpdate(UPDATE_STATEMENT); } }); assertThat(updateCount).isEqualTo(1L); + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(1); + assertThat(countTransactionsStarted()).isEqualTo(2); } @Test @@ -469,6 +499,8 @@ public Long run(TransactionContext transaction) throws Exception { }); assertThat(updateCount).isEqualTo(UPDATE_COUNT); assertThat(attempt.get()).isEqualTo(2); + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countTransactionsStarted()).isEqualTo(2); } @Test @@ -492,5 +524,21 @@ public long[] run(TransactionContext transaction) throws Exception { }); assertThat(updateCounts).isEqualTo(new long[] {UPDATE_COUNT, UPDATE_COUNT}); assertThat(attempt.get()).isEqualTo(2); + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countTransactionsStarted()).isEqualTo(2); + } + + private int countRequests(Class requestType) { + int count = 0; + for (AbstractMessage msg : mockSpanner.getRequests()) { + if (msg.getClass().equals(requestType)) { + count++; + } + } + return count; + } + + private int countTransactionsStarted() { + return mockSpanner.getTransactionsStarted().size(); } } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionTest.java index 57d8e085c6..2d98f2c3f6 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionTest.java @@ -17,7 +17,6 @@ package com.google.cloud.spanner.it; import static com.google.cloud.spanner.SpannerExceptionFactory.newSpannerException; -import static com.google.cloud.spanner.TransactionRunner.TransactionCallable; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; import static org.junit.Assume.assumeFalse; @@ -589,32 +588,35 @@ public void testTxWithConstraintError() { ImmutableList.of( Mutation.newInsertOrUpdateBuilder("T").set("K").to("One").set("V").to(1L).build())); - long updateCount = - client - .readWriteTransaction() - .run( - new TransactionCallable() { - @Override - public Long run(TransactionContext transaction) throws Exception { - try { - // Try to insert a duplicate row. This statement will fail. When the statement - // is executed against an already existing transaction (i.e. - // inlineBegin=false), the entire transaction will remain invalid and cannot - // be committed. When it is executed as the first statement of a transaction - // that also tries to start a transaction, then no transaction will be started - // and the next statement will start the transaction. This will cause the - // transaction to succeed. - transaction.executeUpdate( - Statement.of("INSERT INTO T (K, V) VALUES ('One', 1)")); - fail("missing expected exception"); - } catch (SpannerException e) { - assertThat(e.getErrorCode()).isEqualTo(ErrorCode.ALREADY_EXISTS); - } - return transaction.executeUpdate( - Statement.of("INSERT INTO T (K, V) VALUES ('Two', 2)")); + try { + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Long run(TransactionContext transaction) throws Exception { + try { + // Try to insert a duplicate row. This statement will fail. When the statement + // is executed against an already existing transaction (i.e. + // inlineBegin=false), the entire transaction will remain invalid and cannot + // be committed. When it is executed as the first statement of a transaction + // that also tries to start a transaction, then no transaction will be started + // and the next statement will start the transaction. This will cause the + // transaction to succeed. + transaction.executeUpdate( + Statement.of("INSERT INTO T (K, V) VALUES ('One', 1)")); + fail("missing expected exception"); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.ALREADY_EXISTS); } - }); - assertThat(updateCount).isEqualTo(1L); + return transaction.executeUpdate( + Statement.of("INSERT INTO T (K, V) VALUES ('Two', 2)")); + } + }); + fail("missing expected ALREADY_EXISTS error"); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.ALREADY_EXISTS); + } } @Test From 3bdff48168636bb1160416e8a635980320a338a6 Mon Sep 17 00:00:00 2001 From: Olav Loite Date: Fri, 31 Jul 2020 16:34:47 +0200 Subject: [PATCH 12/19] fix: handle aborted exceptions --- .../com/google/cloud/spanner/it/ITTransactionTest.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionTest.java index 2d98f2c3f6..e23f4bacde 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionTest.java @@ -568,6 +568,10 @@ public Long run(TransactionContext transaction) throws Exception { transaction.executeUpdate(Statement.of("UPDATE T SET V=2 WHERE")); fail("missing expected exception"); } catch (SpannerException e) { + if (e.getErrorCode() == ErrorCode.ABORTED) { + // Aborted -> Let the transaction be retried + throw e; + } assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); } return transaction.executeUpdate( @@ -607,6 +611,10 @@ public Long run(TransactionContext transaction) throws Exception { Statement.of("INSERT INTO T (K, V) VALUES ('One', 1)")); fail("missing expected exception"); } catch (SpannerException e) { + if (e.getErrorCode() == ErrorCode.ABORTED) { + // Aborted -> Let the transaction be retried + throw e; + } assertThat(e.getErrorCode()).isEqualTo(ErrorCode.ALREADY_EXISTS); } return transaction.executeUpdate( From b3148a080e32e9b27c35c056638df865fa13497f Mon Sep 17 00:00:00 2001 From: Olav Loite Date: Wed, 16 Sep 2020 11:19:13 +0200 Subject: [PATCH 13/19] test: add additional tests for corner cases --- .../spanner/InlineBeginTransactionTest.java | 142 +++++++++++++++++- .../cloud/spanner/MockSpannerServiceImpl.java | 33 +++- 2 files changed, 168 insertions(+), 7 deletions(-) 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 45762baced..c973114297 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 @@ -32,8 +32,10 @@ import com.google.cloud.spanner.AsyncTransactionManager.AsyncTransactionStep; import com.google.cloud.spanner.AsyncTransactionManager.CommitTimestampFuture; 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.TransactionRunner.TransactionCallable; +import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.MoreExecutors; import com.google.protobuf.AbstractMessage; import com.google.protobuf.ListValue; @@ -117,6 +119,8 @@ public static Collection data() { .build()) .setMetadata(SELECT1_METADATA) .build(); + private static final Statement INVALID_SELECT = Statement.of("SELECT * FROM NON_EXISTING_TABLE"); + private Spanner spanner; @BeforeClass @@ -128,7 +132,15 @@ public static void startStaticServer() throws IOException { mockSpanner.putStatementResult( StatementResult.exception( INVALID_UPDATE_STATEMENT, - Status.INVALID_ARGUMENT.withDescription("invalid statement").asRuntimeException())); + Status.INVALID_ARGUMENT + .withDescription("invalid update statement") + .asRuntimeException())); + mockSpanner.putStatementResult( + StatementResult.exception( + INVALID_SELECT, + Status.INVALID_ARGUMENT + .withDescription("invalid select statement") + .asRuntimeException())); String uniqueName = InProcessServerBuilder.generateName(); server = @@ -347,6 +359,134 @@ public Long run(TransactionContext transaction) throws Exception { assertThat(countTransactionsStarted()).isEqualTo(1); } + @Test + public void testInlinedBeginTxBatchDmlWithErrorOnFirstStatement() { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + Void res = + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Void run(TransactionContext transaction) throws Exception { + try { + transaction.batchUpdate( + ImmutableList.of(INVALID_UPDATE_STATEMENT, UPDATE_STATEMENT)); + fail("missing expected exception"); + } catch (SpannerBatchUpdateException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + assertThat(e.getUpdateCounts()).hasLength(0); + } + return null; + } + }); + assertThat(res).isNull(); + // The first statement failed and could not return a transaction. The entire transaction is + // therefore retried with an explicit BeginTransaction RPC. + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(1); + assertThat(countTransactionsStarted()).isEqualTo(2); + } + + @Test + public void testInlinedBeginTxBatchDmlWithErrorOnSecondStatement() { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + long updateCount = + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Long run(TransactionContext transaction) throws Exception { + try { + transaction.batchUpdate( + ImmutableList.of(UPDATE_STATEMENT, INVALID_UPDATE_STATEMENT)); + fail("missing expected exception"); + // The following line is needed as the compiler does not know that this is + // unreachable. + return -1L; + } catch (SpannerBatchUpdateException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + assertThat(e.getUpdateCounts()).hasLength(1); + return e.getUpdateCounts()[0]; + } + } + }); + assertThat(updateCount).isEqualTo(UPDATE_COUNT); + // Although the batch DML returned an error, that error was for the second statement. That means + // that the transaction was started by the first statement. + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + + @Test + public void testInlinedBeginTxWithErrorOnStreamingQuery() { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + Void res = + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Void run(TransactionContext transaction) throws Exception { + try (ResultSet rs = transaction.executeQuery(INVALID_SELECT)) { + while (rs.next()) {} + fail("missing expected exception"); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + } + return null; + } + }); + assertThat(res).isNull(); + // The transaction will be retried because the first statement that also tried to include the + // BeginTransaction statement failed and did not return a transaction. That forces a retry of + // the entire transaction with an explicit BeginTransaction RPC. + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(1); + // The first update will start a transaction, but then fail the update statement. This will + // start a transaction on the mock server, but that transaction will never be returned to the + // client. + assertThat(countTransactionsStarted()).isEqualTo(2); + } + + @Test + public void testInlinedBeginTxWithErrorOnSecondPartialResultSet() { + final Statement statement = Statement.of("SELECT * FROM BROKEN_TABLE"); + RandomResultSetGenerator generator = new RandomResultSetGenerator(2); + mockSpanner.putStatementResult(StatementResult.query(statement, generator.generate())); + // First two null exceptions and then a DATA_LOSS exception. The first null is for the RPC + // itself, the second null means that the first PartialResultSet will be returned, and the + // DATA_LOSS exception will be returned for the second PartialResultSet. + mockSpanner.setExecuteStreamingSqlExecutionTime( + SimulatedExecutionTime.ofExceptions( + Arrays.asList(null, null, Status.DATA_LOSS.asRuntimeException()))); + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + Void res = + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Void run(TransactionContext transaction) throws Exception { + try (ResultSet rs = transaction.executeQuery(statement)) { + while (rs.next()) {} + fail("missing expected exception"); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.DATA_LOSS); + } + return null; + } + }); + assertThat(res).isNull(); + // The transaction will not be retried, as the first PartialResultSet returns the transaction + // ID, and the second fails with an error code. + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + @Test public void testInlinedBeginTxWithParallelQueries() { final int numQueries = 100; diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java index 6154c26f2f..5009698652 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java @@ -215,6 +215,7 @@ public PartialResultSet next() { recordCount++; currentRow++; } + builder.setResumeToken(ByteString.copyFromUtf8(String.format("%09d", currentRow))); hasNext = currentRow < resultSet.getRowsCount(); return builder.build(); } @@ -442,12 +443,14 @@ public static SimulatedExecutionTime stickyDatabaseNotFoundException(String name SpannerExceptionFactoryTest.newStatusDatabaseNotFoundException(name)); } - public static SimulatedExecutionTime ofExceptions(Collection exceptions) { + public static SimulatedExecutionTime ofExceptions(Collection exceptions) { return new SimulatedExecutionTime(0, 0, exceptions, false); } public static SimulatedExecutionTime ofMinimumAndRandomTimeAndExceptions( - int minimumExecutionTime, int randomExecutionTime, Collection exceptions) { + int minimumExecutionTime, + int randomExecutionTime, + Collection exceptions) { return new SimulatedExecutionTime( minimumExecutionTime, randomExecutionTime, exceptions, false); } @@ -457,7 +460,10 @@ private SimulatedExecutionTime(int minimum, int random) { } private SimulatedExecutionTime( - int minimum, int random, Collection exceptions, boolean stickyException) { + int minimum, + int random, + Collection exceptions, + boolean stickyException) { Preconditions.checkArgument(minimum >= 0, "Minimum execution time must be >= 0"); Preconditions.checkArgument(random >= 0, "Random execution time must be >= 0"); this.minimumExecutionTime = minimum; @@ -481,6 +487,10 @@ void simulateExecutionTime( } } + void simulateStreamExecutionTime() { + checkException(this.exceptions, stickyException); + } + private static void checkException(Queue exceptions, boolean keepException) { Exception e = keepException ? exceptions.peek() : exceptions.poll(); if (e != null) { @@ -1092,7 +1102,11 @@ public void executeStreamingSql( throw res.getException(); case RESULT_SET: returnPartialResultSet( - res.getResultSet(), transactionId, request.getTransaction(), responseObserver); + res.getResultSet(), + transactionId, + request.getTransaction(), + responseObserver, + executeStreamingSqlExecutionTime); break; case UPDATE_COUNT: if (isPartitioned) { @@ -1421,7 +1435,11 @@ public Iterator iterator() { .asRuntimeException(); } returnPartialResultSet( - res.getResultSet(), transactionId, request.getTransaction(), responseObserver); + res.getResultSet(), + transactionId, + request.getTransaction(), + responseObserver, + streamingReadExecutionTime); } catch (StatusRuntimeException e) { responseObserver.onError(e); } catch (Throwable t) { @@ -1433,7 +1451,8 @@ private void returnPartialResultSet( ResultSet resultSet, ByteString transactionId, TransactionSelector transactionSelector, - StreamObserver responseObserver) { + StreamObserver responseObserver, + SimulatedExecutionTime executionTime) { ResultSetMetadata metadata = resultSet.getMetadata(); if (transactionId == null) { Transaction transaction = getTemporaryTransactionOrNull(transactionSelector); @@ -1449,6 +1468,8 @@ private void returnPartialResultSet( PartialResultSetsIterator iterator = new PartialResultSetsIterator(resultSet); while (iterator.hasNext()) { responseObserver.onNext(iterator.next()); + // Uninterruptibles.sleepUninterruptibly(1L, TimeUnit.SECONDS); + executionTime.simulateStreamExecutionTime(); } responseObserver.onCompleted(); } From f508bdb182d0694021864ccd0107c9572039e36f Mon Sep 17 00:00:00 2001 From: Olav Loite Date: Wed, 16 Sep 2020 17:34:26 +0200 Subject: [PATCH 14/19] feat: use single-use tx for idem-potent mutations --- .../cloud/spanner/TransactionRunnerImpl.java | 64 +++-- .../spanner/InlineBeginTransactionTest.java | 221 ++++++++++++++++-- 2 files changed, 256 insertions(+), 29 deletions(-) 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 44c76aa122..a47bf6a4bd 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 @@ -26,6 +26,7 @@ import com.google.api.core.ApiFutures; import com.google.api.core.SettableApiFuture; import com.google.cloud.Timestamp; +import com.google.cloud.spanner.Mutation.Op; import com.google.cloud.spanner.Options.QueryOption; import com.google.cloud.spanner.Options.ReadOption; import com.google.cloud.spanner.SessionImpl.SessionTransaction; @@ -167,6 +168,28 @@ private TransactionContextImpl(Builder builder) { this.finishedAsyncOperations.set(null); } + boolean hasNonIdemPotentMutations() { + for (Mutation m : mutations) { + // INSERT is not idem-potent as it will return an ALREADY_EXISTS error in case it is + // retried. + if (m.getOperation() == Op.INSERT) { + return true; + } + // All other operations are idem-potent as the result of the last attempt will be consistent + // with the actual operation: + // UPDATE: Will fail if the row does not exist and on constraint violations. If the row + // exists at the first attempt, is then deleted by a different transaction, and the update + // is retried, it will return an error at the second attempt. This error is consistent with + // the outcome of the transaction. + // INSERT_OR_UPDATE: Will fail on constraint violations. The last returned error or success + // will be consistent with the actual operation. + // REPLACE: Same as INSERT_OR_UPDATE. + // DELETE: Will fail on constraint violations. The last returned error or success will be + // consistent with the actual operation. + } + return false; + } + private void increaseAsynOperations() { synchronized (lock) { if (runningAsyncOperations == 0) { @@ -259,45 +282,54 @@ void commit() { ApiFuture commitAsync() { final SettableApiFuture res = SettableApiFuture.create(); final SettableApiFuture finishOps; + CommitRequest.Builder builder = CommitRequest.newBuilder().setSession(session.getName()); synchronized (lock) { - if (finishedAsyncOperations.isDone() && transactionId == null) { + if (transactionIdFuture == null && transactionId == null && hasNonIdemPotentMutations()) { finishOps = SettableApiFuture.create(); createTxnAsync(finishOps); } else { finishOps = finishedAsyncOperations; } + if (!mutations.isEmpty()) { + List mutationsProto = new ArrayList<>(); + Mutation.toProto(mutations, mutationsProto); + builder.addAllMutations(mutationsProto); + } + // Ensure that no call to buffer mutations that would be lost can succeed. + mutations = null; } - finishOps.addListener(new CommitRunnable(res, finishOps), MoreExecutors.directExecutor()); + finishOps.addListener( + new CommitRunnable(res, finishOps, builder), MoreExecutors.directExecutor()); return res; } private final class CommitRunnable implements Runnable { private final SettableApiFuture res; private final ApiFuture prev; + private final CommitRequest.Builder requestBuilder; - CommitRunnable(SettableApiFuture res, ApiFuture prev) { + CommitRunnable( + SettableApiFuture res, + ApiFuture prev, + CommitRequest.Builder requestBuilder) { this.res = res; this.prev = prev; + this.requestBuilder = requestBuilder; } @Override public void run() { try { prev.get(); - CommitRequest.Builder builder = - CommitRequest.newBuilder() - .setSession(session.getName()) - .setTransactionId(transactionId); - synchronized (lock) { - if (!mutations.isEmpty()) { - List mutationsProto = new ArrayList<>(); - Mutation.toProto(mutations, mutationsProto); - builder.addAllMutations(mutationsProto); - } - // Ensure that no call to buffer mutations that would be lost can succeed. - mutations = null; + if (transactionId == null && transactionIdFuture == null) { + requestBuilder.setSingleUseTransaction( + TransactionOptions.newBuilder() + .setReadWrite(TransactionOptions.ReadWrite.getDefaultInstance())); + } else { + requestBuilder.setTransactionId( + transactionId == null ? transactionIdFuture.get() : transactionId); } - final CommitRequest commitRequest = builder.build(); + final CommitRequest commitRequest = requestBuilder.build(); span.addAnnotation("Starting Commit"); final Span opSpan = tracer.spanBuilderWithExplicitParent(SpannerImpl.COMMIT, span).startSpan(); 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 c973114297..3281cc1ce5 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 @@ -41,7 +41,9 @@ 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.RollbackRequest; import com.google.spanner.v1.StructType; @@ -120,6 +122,7 @@ public static Collection data() { .setMetadata(SELECT1_METADATA) .build(); private static final Statement INVALID_SELECT = Statement.of("SELECT * FROM NON_EXISTING_TABLE"); + private static final Statement READ_STATEMENT = Statement.of("SELECT ID FROM FOO WHERE 1=1"); private Spanner spanner; @@ -129,6 +132,7 @@ 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.query(READ_STATEMENT, SELECT1_RESULTSET)); mockSpanner.putStatementResult( StatementResult.exception( INVALID_UPDATE_STATEMENT, @@ -199,6 +203,8 @@ public Long run(TransactionContext transaction) throws Exception { }); assertThat(updateCount).isEqualTo(UPDATE_COUNT); assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countRequests(ExecuteSqlRequest.class)).isEqualTo(1); + assertThat(countRequests(CommitRequest.class)).isEqualTo(1); assertThat(countTransactionsStarted()).isEqualTo(1); } @@ -223,7 +229,9 @@ public Long run(TransactionContext transaction) throws Exception { }); assertThat(updateCount).isEqualTo(UPDATE_COUNT); assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); - // We have started 2 transactions, because the first transaction aborted. + assertThat(countRequests(ExecuteSqlRequest.class)).isEqualTo(2); + // We have started 2 transactions, because the first transaction aborted during the commit. + assertThat(countRequests(CommitRequest.class)).isEqualTo(2); assertThat(countTransactionsStarted()).isEqualTo(2); } @@ -248,6 +256,35 @@ public Long run(TransactionContext transaction) throws Exception { }); assertThat(updateCount).isEqualTo(1L); assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countRequests(ExecuteSqlRequest.class)).isEqualTo(1); + assertThat(countRequests(CommitRequest.class)).isEqualTo(1); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + + @Test + public void testInlinedBeginTxWithRead() { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + long updateCount = + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Long run(TransactionContext transaction) throws Exception { + try (ResultSet rs = + transaction.read("FOO", KeySet.all(), Arrays.asList("ID"))) { + while (rs.next()) { + return rs.getLong(0); + } + } + return 0L; + } + }); + assertThat(updateCount).isEqualTo(1L); + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countRequests(ReadRequest.class)).isEqualTo(1); + assertThat(countRequests(CommitRequest.class)).isEqualTo(1); assertThat(countTransactionsStarted()).isEqualTo(1); } @@ -268,6 +305,8 @@ public long[] run(TransactionContext transaction) throws Exception { }); assertThat(updateCounts).asList().containsExactly(UPDATE_COUNT, UPDATE_COUNT); assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countRequests(ExecuteBatchDmlRequest.class)).isEqualTo(1); + assertThat(countRequests(CommitRequest.class)).isEqualTo(1); assertThat(countTransactionsStarted()).isEqualTo(1); } @@ -296,12 +335,60 @@ public Long run(TransactionContext transaction) throws Exception { // BeginTransaction statement failed and did not return a transaction. That forces a retry of // the entire transaction with an explicit BeginTransaction RPC. assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(1); + // The update statement will be executed 3 times: + // 1. The invalid update statement will be executed during the first attempt and fail. The + // second update statement will not be executed, as the transaction runner sees that the initial + // statement failed and did not return a valid transaction id. + // 2. The invalid update statement is executed again during the retry. + // 3. The valid update statement is only executed after the first statement succeeded. + assertThat(countRequests(ExecuteSqlRequest.class)).isEqualTo(3); + assertThat(countRequests(CommitRequest.class)).isEqualTo(1); // The first update will start a transaction, but then fail the update statement. This will // start a transaction on the mock server, but that transaction will never be returned to the // client. assertThat(countTransactionsStarted()).isEqualTo(2); } + @Test + public void testInlinedBeginTxWithErrorOnFirstStatement_andThenErrorOnBeginTransaction() { + mockSpanner.setBeginTransactionExecutionTime( + SimulatedExecutionTime.ofException( + Status.INTERNAL + .withDescription("Begin transaction failed due to an internal error") + .asRuntimeException())); + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + try { + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Void run(TransactionContext transaction) throws Exception { + try { + transaction.executeUpdate(INVALID_UPDATE_STATEMENT); + fail("missing expected exception"); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + } + return null; + } + }); + fail("Missing expected exception"); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INTERNAL); + assertThat(e.getMessage()).contains("Begin transaction failed due to an internal error"); + } + // The transaction will be retried because the first statement that also tried to include the + // BeginTransaction statement failed and did not return a transaction. That forces a retry of + // the entire transaction with an explicit BeginTransaction RPC. + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(1); + assertThat(countRequests(ExecuteSqlRequest.class)).isEqualTo(1); + assertThat(countRequests(CommitRequest.class)).isEqualTo(0); + // The explicit BeginTransaction RPC failed, so only one transaction was started. + assertThat(countTransactionsStarted()).isEqualTo(1); + } + @Test public void testInlinedBeginTxWithUncaughtError() { DatabaseClient client = @@ -385,6 +472,8 @@ public Void run(TransactionContext transaction) throws Exception { // The first statement failed and could not return a transaction. The entire transaction is // therefore retried with an explicit BeginTransaction RPC. assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(1); + assertThat(countRequests(ExecuteBatchDmlRequest.class)).isEqualTo(2); + assertThat(countRequests(CommitRequest.class)).isEqualTo(1); assertThat(countTransactionsStarted()).isEqualTo(2); } @@ -417,11 +506,13 @@ public Long run(TransactionContext transaction) throws Exception { // Although the batch DML returned an error, that error was for the second statement. That means // that the transaction was started by the first statement. assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countRequests(ExecuteBatchDmlRequest.class)).isEqualTo(1); + assertThat(countRequests(CommitRequest.class)).isEqualTo(1); assertThat(countTransactionsStarted()).isEqualTo(1); } @Test - public void testInlinedBeginTxWithErrorOnStreamingQuery() { + public void testInlinedBeginTxWithErrorOnStreamingSql() { DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); Void res = @@ -445,6 +536,8 @@ public Void run(TransactionContext transaction) throws Exception { // BeginTransaction statement failed and did not return a transaction. That forces a retry of // the entire transaction with an explicit BeginTransaction RPC. assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(1); + assertThat(countRequests(ExecuteSqlRequest.class)).isEqualTo(2); + assertThat(countRequests(CommitRequest.class)).isEqualTo(1); // The first update will start a transaction, but then fail the update statement. This will // start a transaction on the mock server, but that transaction will never be returned to the // client. @@ -484,6 +577,8 @@ public Void run(TransactionContext transaction) throws Exception { // The transaction will not be retried, as the first PartialResultSet returns the transaction // ID, and the second fails with an error code. assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countRequests(ExecuteSqlRequest.class)).isEqualTo(1); + assertThat(countRequests(CommitRequest.class)).isEqualTo(1); assertThat(countTransactionsStarted()).isEqualTo(1); } @@ -529,7 +624,7 @@ public Long call() throws Exception { } @Test - public void testInlinedBeginTxWithOnlyMutations() { + public void testInlinedBeginTxWithNonIdemPotentMutations() { DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); client @@ -538,13 +633,38 @@ public void testInlinedBeginTxWithOnlyMutations() { new TransactionCallable() { @Override public Void run(TransactionContext transaction) throws Exception { - transaction.buffer(Mutation.delete("FOO", Key.of(1L))); + transaction.buffer( + Arrays.asList( + Mutation.newInsertBuilder("FOO").set("ID").to(1L).build(), + Mutation.delete("FOO", Key.of(1L)))); return null; } }); // There should be 1 call to BeginTransaction because there is no statement that we can use to // inline the BeginTransaction call with. assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(1); + assertThat(countRequests(CommitRequest.class)).isEqualTo(1); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + + @Test + public void testInlinedBeginTxWithOnlyIdemPotentMutations() { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + client + .readWriteTransaction() + .run( + new TransactionCallable() { + @Override + public Void run(TransactionContext transaction) throws Exception { + transaction.buffer(Mutation.delete("FOO", Key.of(1L))); + return null; + } + }); + // As all mutations are idem-potent, the transaction can use a single CommitRequest with a + // single-use transaction. + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countRequests(CommitRequest.class)).isEqualTo(1); assertThat(countTransactionsStarted()).isEqualTo(1); } @@ -597,7 +717,7 @@ public void testTransactionManagerInlinedBeginTxAborted() { @SuppressWarnings("resource") @Test - public void testTransactionManagerInlinedBeginTxWithOnlyMutations() { + public void testTransactionManagerInlinedBeginTxWithOnlyIdemPotentMutations() { DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); try (TransactionManager txMgr = client.transactionManager()) { @@ -612,9 +732,32 @@ public void testTransactionManagerInlinedBeginTxWithOnlyMutations() { } } } - // There should be 1 call to BeginTransaction because there is no statement that we can use to - // inline the BeginTransaction call with. + // As the transaction only contains idem-potent mutations it can use a single-use transaction + // that is triggered by the commit request. + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countRequests(CommitRequest.class)).isEqualTo(1); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + + @SuppressWarnings("resource") + @Test + public void testTransactionManagerInlinedBeginTxWithNonIdemPotentMutations() { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + try (TransactionManager txMgr = client.transactionManager()) { + TransactionContext txn = txMgr.begin(); + while (true) { + try { + txn.buffer(Mutation.newInsertBuilder("FOO").set("ID").to(1L).build()); + txMgr.commit(); + break; + } catch (AbortedException e) { + txn = txMgr.resetForRetry(); + } + } + } assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(1); + assertThat(countRequests(CommitRequest.class)).isEqualTo(1); assertThat(countTransactionsStarted()).isEqualTo(1); } @@ -867,7 +1010,7 @@ public ApiFuture apply(List input) throws Exception { } @Test - public void testInlinedBeginAsyncTxWithOnlyMutations() + public void testInlinedBeginAsyncTxWithOnlyIdemPotentMutations() throws InterruptedException, ExecutionException { DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); @@ -883,9 +1026,30 @@ public ApiFuture doWorkAsync(TransactionContext transaction) { }, executor) .get(); - // There should be 1 call to BeginTransaction because there is no statement that we can use to - // inline the BeginTransaction call with. + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countRequests(CommitRequest.class)).isEqualTo(1); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + + @Test + public void testInlinedBeginAsyncTxWithNonIdemPotentMutations() + throws InterruptedException, ExecutionException { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + client + .runAsync() + .runAsync( + new AsyncWork() { + @Override + public ApiFuture doWorkAsync(TransactionContext transaction) { + transaction.buffer(Mutation.newInsertBuilder("FOO").set("ID").to(1L).build()); + return ApiFutures.immediateFuture(null); + } + }, + executor) + .get(); assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(1); + assertThat(countRequests(CommitRequest.class)).isEqualTo(1); assertThat(countTransactionsStarted()).isEqualTo(1); } @@ -969,7 +1133,7 @@ public ApiFuture apply(TransactionContext txn, Long input) } @Test - public void testAsyncTransactionManagerInlinedBeginTxWithOnlyMutations() + public void testAsyncTransactionManagerInlinedBeginTxWithOnlyIdemPotentMutations() throws InterruptedException, ExecutionException { DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); @@ -995,9 +1159,40 @@ public ApiFuture apply(TransactionContext txn, Void input) } } } - // There should be 1 call to BeginTransaction because there is no statement that we can use to - // inline the BeginTransaction call with. + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + assertThat(countRequests(CommitRequest.class)).isEqualTo(1); + assertThat(countTransactionsStarted()).isEqualTo(1); + } + + @Test + public void testAsyncTransactionManagerInlinedBeginTxWithNonIdemPotentMutations() + throws InterruptedException, ExecutionException { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + try (AsyncTransactionManager txMgr = client.transactionManagerAsync()) { + TransactionContextFuture txn = txMgr.beginAsync(); + while (true) { + try { + txn.then( + new AsyncTransactionFunction() { + @Override + public ApiFuture apply(TransactionContext txn, Void input) + throws Exception { + txn.buffer(Mutation.newInsertBuilder("FOO").set("ID").to(1L).build()); + return ApiFutures.immediateFuture(null); + } + }, + executor) + .commitAsync() + .get(); + break; + } catch (AbortedException e) { + txn = txMgr.resetForRetryAsync(); + } + } + } assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(1); + assertThat(countRequests(CommitRequest.class)).isEqualTo(1); assertThat(countTransactionsStarted()).isEqualTo(1); } From d9e938fb9fc73a44e22e423e90b3ded6ba66148a Mon Sep 17 00:00:00 2001 From: Olav Loite Date: Thu, 17 Sep 2020 16:54:38 +0200 Subject: [PATCH 15/19] fix: remove check for idempotent mutations --- .../cloud/spanner/TransactionRunnerImpl.java | 25 +--- .../spanner/InlineBeginTransactionTest.java | 107 +----------------- 2 files changed, 5 insertions(+), 127 deletions(-) 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 a47bf6a4bd..b0d48b3e69 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 @@ -26,7 +26,6 @@ import com.google.api.core.ApiFutures; import com.google.api.core.SettableApiFuture; import com.google.cloud.Timestamp; -import com.google.cloud.spanner.Mutation.Op; import com.google.cloud.spanner.Options.QueryOption; import com.google.cloud.spanner.Options.ReadOption; import com.google.cloud.spanner.SessionImpl.SessionTransaction; @@ -168,28 +167,6 @@ private TransactionContextImpl(Builder builder) { this.finishedAsyncOperations.set(null); } - boolean hasNonIdemPotentMutations() { - for (Mutation m : mutations) { - // INSERT is not idem-potent as it will return an ALREADY_EXISTS error in case it is - // retried. - if (m.getOperation() == Op.INSERT) { - return true; - } - // All other operations are idem-potent as the result of the last attempt will be consistent - // with the actual operation: - // UPDATE: Will fail if the row does not exist and on constraint violations. If the row - // exists at the first attempt, is then deleted by a different transaction, and the update - // is retried, it will return an error at the second attempt. This error is consistent with - // the outcome of the transaction. - // INSERT_OR_UPDATE: Will fail on constraint violations. The last returned error or success - // will be consistent with the actual operation. - // REPLACE: Same as INSERT_OR_UPDATE. - // DELETE: Will fail on constraint violations. The last returned error or success will be - // consistent with the actual operation. - } - return false; - } - private void increaseAsynOperations() { synchronized (lock) { if (runningAsyncOperations == 0) { @@ -284,7 +261,7 @@ ApiFuture commitAsync() { final SettableApiFuture finishOps; CommitRequest.Builder builder = CommitRequest.newBuilder().setSession(session.getName()); synchronized (lock) { - if (transactionIdFuture == null && transactionId == null && hasNonIdemPotentMutations()) { + if (transactionIdFuture == null && transactionId == null) { finishOps = SettableApiFuture.create(); createTxnAsync(finishOps); } else { 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 3281cc1ce5..f40fed8615 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 @@ -624,7 +624,7 @@ public Long call() throws Exception { } @Test - public void testInlinedBeginTxWithNonIdemPotentMutations() { + public void testInlinedBeginTxWithOnlyMutations() { DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); client @@ -647,27 +647,6 @@ public Void run(TransactionContext transaction) throws Exception { assertThat(countTransactionsStarted()).isEqualTo(1); } - @Test - public void testInlinedBeginTxWithOnlyIdemPotentMutations() { - DatabaseClient client = - spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); - client - .readWriteTransaction() - .run( - new TransactionCallable() { - @Override - public Void run(TransactionContext transaction) throws Exception { - transaction.buffer(Mutation.delete("FOO", Key.of(1L))); - return null; - } - }); - // As all mutations are idem-potent, the transaction can use a single CommitRequest with a - // single-use transaction. - assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); - assertThat(countRequests(CommitRequest.class)).isEqualTo(1); - assertThat(countTransactionsStarted()).isEqualTo(1); - } - @SuppressWarnings("resource") @Test public void testTransactionManagerInlinedBeginTx() { @@ -717,31 +696,7 @@ public void testTransactionManagerInlinedBeginTxAborted() { @SuppressWarnings("resource") @Test - public void testTransactionManagerInlinedBeginTxWithOnlyIdemPotentMutations() { - DatabaseClient client = - spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); - try (TransactionManager txMgr = client.transactionManager()) { - TransactionContext txn = txMgr.begin(); - while (true) { - try { - txn.buffer(Mutation.delete("FOO", Key.of(1L))); - txMgr.commit(); - break; - } catch (AbortedException e) { - txn = txMgr.resetForRetry(); - } - } - } - // As the transaction only contains idem-potent mutations it can use a single-use transaction - // that is triggered by the commit request. - assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); - assertThat(countRequests(CommitRequest.class)).isEqualTo(1); - assertThat(countTransactionsStarted()).isEqualTo(1); - } - - @SuppressWarnings("resource") - @Test - public void testTransactionManagerInlinedBeginTxWithNonIdemPotentMutations() { + public void testTransactionManagerInlinedBeginTxWithOnlyMutations() { DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); try (TransactionManager txMgr = client.transactionManager()) { @@ -1010,29 +965,7 @@ public ApiFuture apply(List input) throws Exception { } @Test - public void testInlinedBeginAsyncTxWithOnlyIdemPotentMutations() - throws InterruptedException, ExecutionException { - DatabaseClient client = - spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); - client - .runAsync() - .runAsync( - new AsyncWork() { - @Override - public ApiFuture doWorkAsync(TransactionContext transaction) { - transaction.buffer(Mutation.delete("FOO", Key.of(1L))); - return ApiFutures.immediateFuture(null); - } - }, - executor) - .get(); - assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); - assertThat(countRequests(CommitRequest.class)).isEqualTo(1); - assertThat(countTransactionsStarted()).isEqualTo(1); - } - - @Test - public void testInlinedBeginAsyncTxWithNonIdemPotentMutations() + public void testInlinedBeginAsyncTxWithOnlyMutations() throws InterruptedException, ExecutionException { DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); @@ -1133,39 +1066,7 @@ public ApiFuture apply(TransactionContext txn, Long input) } @Test - public void testAsyncTransactionManagerInlinedBeginTxWithOnlyIdemPotentMutations() - throws InterruptedException, ExecutionException { - DatabaseClient client = - spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); - try (AsyncTransactionManager txMgr = client.transactionManagerAsync()) { - TransactionContextFuture txn = txMgr.beginAsync(); - while (true) { - try { - txn.then( - new AsyncTransactionFunction() { - @Override - public ApiFuture apply(TransactionContext txn, Void input) - throws Exception { - txn.buffer(Mutation.delete("FOO", Key.of(1L))); - return ApiFutures.immediateFuture(null); - } - }, - executor) - .commitAsync() - .get(); - break; - } catch (AbortedException e) { - txn = txMgr.resetForRetryAsync(); - } - } - } - assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); - assertThat(countRequests(CommitRequest.class)).isEqualTo(1); - assertThat(countTransactionsStarted()).isEqualTo(1); - } - - @Test - public void testAsyncTransactionManagerInlinedBeginTxWithNonIdemPotentMutations() + public void testAsyncTransactionManagerInlinedBeginTxWithOnlyMutations() throws InterruptedException, ExecutionException { DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); From 07346f02e1974a6800144c7ec03b24a61ed429bd Mon Sep 17 00:00:00 2001 From: Olav Loite Date: Tue, 6 Oct 2020 17:55:53 +0200 Subject: [PATCH 16/19] chore: remove commented code --- .../cloud/spanner/TransactionRunnerImpl.java | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) 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 b0d48b3e69..f885492a03 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 @@ -406,10 +406,11 @@ TransactionSelector getTransactionSelector() { // withInlineBegin and an earlier statement has already started a transaction. if (transactionId == null) { try { - // Wait if another request is already beginning, committing or rolling back the - // transaction. ApiFuture tx = null; synchronized (lock) { + // The first statement of a transaction that gets here will be the one that includes + // BeginTransaction with the statement. The others will be waiting on the + // transactionIdFuture until an actual transactionId is available. if (transactionIdFuture == null) { transactionIdFuture = SettableApiFuture.create(); } else { @@ -423,6 +424,10 @@ TransactionSelector getTransactionSelector() { .setReadWrite(TransactionOptions.ReadWrite.getDefaultInstance())) .build(); } else { + // Wait for the transaction to come available. The tx.get() call will fail with an + // Aborted error if the call that included the BeginTransaction option fails. The + // Aborted error will cause the entire transaction to be retried, and the retry will use + // a separate BeginTransaction RPC. TransactionSelector.newBuilder().setId(tx.get()).build(); } } catch (ExecutionException e) { @@ -452,18 +457,18 @@ public void onTransactionMetadata(Transaction transaction) { @Override public void onError(SpannerException e, boolean withBeginTransaction) { - // Release the transactionLock if that is being held by this thread. That would mean that the - // statement that was trying to start a transaction caused an error. The next statement should - // in that case also include a BeginTransaction option. + // If the statement that caused an error was the statement that included a BeginTransaction + // option, we simulate an aborted transaction to force a retry of the entire transaction. This + // will cause the retry to execute an explicit BeginTransaction RPC and then the actual + // statements of the transaction. This is needed as the first statement of the transaction + // must be included with the transaction to ensure that any locks that are taken by the + // statement are included in the transaction, even if the statement again causes an error + // during the retry. if (withBeginTransaction) { // Simulate an aborted transaction to force a retry with a new transaction. this.transactionIdFuture.setException( SpannerExceptionFactory.newSpannerException( ErrorCode.ABORTED, "Aborted due to failed initial statement", e)); - // synchronized (lock) { - // retryDelayInMillis = 0; - // aborted = true; - // } } if (e.getErrorCode() == ErrorCode.ABORTED) { From 2768f69db13c155bbe3ee24b3440c0f3d2b67b67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Wed, 21 Oct 2020 08:03:39 +0200 Subject: [PATCH 17/19] feat!: remove session pool preparing (#515) * feat: remove session pool preparing * fix: fix integration tests * test: fix malformed retry loop in test case * fix: review comments --- .../spanner/AsyncTransactionManagerImpl.java | 10 +- .../cloud/spanner/DatabaseClientImpl.java | 125 +- .../spanner/MetricRegistryConstants.java | 13 + .../com/google/cloud/spanner/SessionImpl.java | 18 +- .../com/google/cloud/spanner/SessionPool.java | 562 ++------- .../cloud/spanner/SessionPoolOptions.java | 35 +- .../com/google/cloud/spanner/SpannerImpl.java | 3 +- .../google/cloud/spanner/SpannerOptions.java | 36 - .../cloud/spanner/TransactionManagerImpl.java | 9 +- .../cloud/spanner/TransactionRunnerImpl.java | 15 +- .../google/cloud/spanner/AsyncRunnerTest.java | 31 +- .../spanner/AsyncTransactionManagerTest.java | 53 +- .../spanner/BatchCreateSessionsTest.java | 70 -- .../cloud/spanner/DatabaseClientImplTest.java | 145 +-- .../spanner/ITSessionPoolIntegrationTest.java | 22 +- .../cloud/spanner/InlineBeginBenchmark.java | 6 +- .../spanner/InlineBeginTransactionTest.java | 6 +- .../IntegrationTestWithClosedSessionsEnv.java | 26 +- ...adWriteTransactionWithInlineBeginTest.java | 4 - .../RetryOnInvalidatedSessionTest.java | 76 +- .../cloud/spanner/SessionPoolLeakTest.java | 27 +- .../spanner/SessionPoolMaintainerTest.java | 40 +- .../cloud/spanner/SessionPoolStressTest.java | 21 +- .../google/cloud/spanner/SessionPoolTest.java | 1025 ++++------------- .../com/google/cloud/spanner/SpanTest.java | 30 +- .../cloud/spanner/SpannerGaxRetryTest.java | 4 +- .../TransactionManagerAbortedTest.java | 16 +- .../spanner/TransactionManagerImplTest.java | 3 +- .../spanner/TransactionRunnerImplTest.java | 18 +- .../cloud/spanner/it/ITClosedSessionTest.java | 22 +- .../google/cloud/spanner/it/ITDMLTest.java | 35 +- .../it/ITTransactionManagerAsyncTest.java | 22 +- .../spanner/it/ITTransactionManagerTest.java | 36 +- .../cloud/spanner/it/ITTransactionTest.java | 41 +- 34 files changed, 543 insertions(+), 2062 deletions(-) diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManagerImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManagerImpl.java index f6085adf38..9230df5838 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManagerImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManagerImpl.java @@ -39,16 +39,14 @@ final class AsyncTransactionManagerImpl private final SessionImpl session; private Span span; - private final boolean inlineBegin; private TransactionRunnerImpl.TransactionContextImpl txn; private TransactionState txnState; private final SettableApiFuture commitTimestamp = SettableApiFuture.create(); - AsyncTransactionManagerImpl(SessionImpl session, Span span, boolean inlineBegin) { + AsyncTransactionManagerImpl(SessionImpl session, Span span) { this.session = session; this.span = span; - this.inlineBegin = inlineBegin; } @Override @@ -69,15 +67,15 @@ public TransactionContextFutureImpl beginAsync() { return begin; } - private ApiFuture internalBeginAsync(boolean setActive) { + private ApiFuture internalBeginAsync(boolean firstAttempt) { txnState = TransactionState.STARTED; txn = session.newTransaction(); - if (setActive) { + if (firstAttempt) { session.setActive(this); } final SettableApiFuture res = SettableApiFuture.create(); final ApiFuture fut; - if (inlineBegin) { + if (firstAttempt) { fut = ApiFutures.immediateFuture(null); } else { fut = txn.ensureTxnAsync(); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java index 3883134582..fa2fa9d971 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java @@ -29,40 +29,26 @@ class DatabaseClientImpl implements DatabaseClient { private static final String READ_WRITE_TRANSACTION = "CloudSpanner.ReadWriteTransaction"; - private static final String READ_WRITE_TRANSACTION_WITH_INLINE_BEGIN = - "CloudSpanner.ReadWriteTransactionWithInlineBegin"; private static final String READ_ONLY_TRANSACTION = "CloudSpanner.ReadOnlyTransaction"; private static final String PARTITION_DML_TRANSACTION = "CloudSpanner.PartitionDMLTransaction"; private static final Tracer tracer = Tracing.getTracer(); - private enum SessionMode { - READ, - READ_WRITE - } - @VisibleForTesting final String clientId; @VisibleForTesting final SessionPool pool; - private final boolean inlineBeginReadWriteTransactions; @VisibleForTesting - DatabaseClientImpl(SessionPool pool, boolean inlineBeginReadWriteTransactions) { - this("", pool, inlineBeginReadWriteTransactions); + DatabaseClientImpl(SessionPool pool) { + this("", pool); } - DatabaseClientImpl(String clientId, SessionPool pool, boolean inlineBeginReadWriteTransactions) { + DatabaseClientImpl(String clientId, SessionPool pool) { this.clientId = clientId; this.pool = pool; - this.inlineBeginReadWriteTransactions = inlineBeginReadWriteTransactions; - } - - @VisibleForTesting - PooledSessionFuture getReadSession() { - return pool.getReadSession(); } @VisibleForTesting - PooledSessionFuture getReadWriteSession() { - return pool.getReadWriteSession(); + PooledSessionFuture getSession() { + return pool.getSession(); } @Override @@ -70,7 +56,6 @@ public Timestamp write(final Iterable mutations) throws SpannerExcepti Span span = tracer.spanBuilder(READ_WRITE_TRANSACTION).startSpan(); try (Scope s = tracer.withSpan(span)) { return runWithSessionRetry( - SessionMode.READ_WRITE, new Function() { @Override public Timestamp apply(Session session) { @@ -90,7 +75,6 @@ public Timestamp writeAtLeastOnce(final Iterable mutations) throws Spa Span span = tracer.spanBuilder(READ_WRITE_TRANSACTION).startSpan(); try (Scope s = tracer.withSpan(span)) { return runWithSessionRetry( - SessionMode.READ_WRITE, new Function() { @Override public Timestamp apply(Session session) { @@ -109,7 +93,7 @@ public Timestamp apply(Session session) { public ReadContext singleUse() { Span span = tracer.spanBuilder(READ_ONLY_TRANSACTION).startSpan(); try (Scope s = tracer.withSpan(span)) { - return getReadSession().singleUse(); + return getSession().singleUse(); } catch (RuntimeException e) { TraceUtil.endSpanWithFailure(span, e); throw e; @@ -120,7 +104,7 @@ public ReadContext singleUse() { public ReadContext singleUse(TimestampBound bound) { Span span = tracer.spanBuilder(READ_ONLY_TRANSACTION).startSpan(); try (Scope s = tracer.withSpan(span)) { - return getReadSession().singleUse(bound); + return getSession().singleUse(bound); } catch (RuntimeException e) { TraceUtil.endSpanWithFailure(span, e); throw e; @@ -131,7 +115,7 @@ public ReadContext singleUse(TimestampBound bound) { public ReadOnlyTransaction singleUseReadOnlyTransaction() { Span span = tracer.spanBuilder(READ_ONLY_TRANSACTION).startSpan(); try (Scope s = tracer.withSpan(span)) { - return getReadSession().singleUseReadOnlyTransaction(); + return getSession().singleUseReadOnlyTransaction(); } catch (RuntimeException e) { TraceUtil.endSpanWithFailure(span, e); throw e; @@ -142,7 +126,7 @@ public ReadOnlyTransaction singleUseReadOnlyTransaction() { public ReadOnlyTransaction singleUseReadOnlyTransaction(TimestampBound bound) { Span span = tracer.spanBuilder(READ_ONLY_TRANSACTION).startSpan(); try (Scope s = tracer.withSpan(span)) { - return getReadSession().singleUseReadOnlyTransaction(bound); + return getSession().singleUseReadOnlyTransaction(bound); } catch (RuntimeException e) { TraceUtil.endSpanWithFailure(span, e); throw e; @@ -153,7 +137,7 @@ public ReadOnlyTransaction singleUseReadOnlyTransaction(TimestampBound bound) { public ReadOnlyTransaction readOnlyTransaction() { Span span = tracer.spanBuilder(READ_ONLY_TRANSACTION).startSpan(); try (Scope s = tracer.withSpan(span)) { - return getReadSession().readOnlyTransaction(); + return getSession().readOnlyTransaction(); } catch (RuntimeException e) { TraceUtil.endSpanWithFailure(span, e); throw e; @@ -164,7 +148,7 @@ public ReadOnlyTransaction readOnlyTransaction() { public ReadOnlyTransaction readOnlyTransaction(TimestampBound bound) { Span span = tracer.spanBuilder(READ_ONLY_TRANSACTION).startSpan(); try (Scope s = tracer.withSpan(span)) { - return getReadSession().readOnlyTransaction(bound); + return getSession().readOnlyTransaction(bound); } catch (RuntimeException e) { TraceUtil.endSpanWithFailure(span, e); throw e; @@ -173,56 +157,22 @@ public ReadOnlyTransaction readOnlyTransaction(TimestampBound bound) { @Override public TransactionRunner readWriteTransaction() { - return inlineBeginReadWriteTransactions - ? inlinedReadWriteTransaction() - : preparedReadWriteTransaction(); - } - - private TransactionRunner preparedReadWriteTransaction() { Span span = tracer.spanBuilder(READ_WRITE_TRANSACTION).startSpan(); try (Scope s = tracer.withSpan(span)) { - return getReadWriteSession().readWriteTransaction(); + return getSession().readWriteTransaction(); } catch (RuntimeException e) { - TraceUtil.setWithFailure(span, e); + TraceUtil.endSpanWithFailure(span, e); throw e; } finally { span.end(TraceUtil.END_SPAN_OPTIONS); } } - private TransactionRunner inlinedReadWriteTransaction() { - Span span = tracer.spanBuilder(READ_WRITE_TRANSACTION_WITH_INLINE_BEGIN).startSpan(); - try (Scope s = tracer.withSpan(span)) { - // An inlined read/write transaction does not need a write-prepared session. - return getReadSession().readWriteTransaction(); - } catch (RuntimeException e) { - TraceUtil.endSpanWithFailure(span, e); - throw e; - } - } - @Override public TransactionManager transactionManager() { - return inlineBeginReadWriteTransactions - ? inlinedTransactionManager() - : preparedTransactionManager(); - } - - private TransactionManager preparedTransactionManager() { Span span = tracer.spanBuilder(READ_WRITE_TRANSACTION).startSpan(); try (Scope s = tracer.withSpan(span)) { - return getReadWriteSession().transactionManager(); - } catch (RuntimeException e) { - TraceUtil.endSpanWithFailure(span, e); - throw e; - } - } - - private TransactionManager inlinedTransactionManager() { - Span span = tracer.spanBuilder(READ_WRITE_TRANSACTION_WITH_INLINE_BEGIN).startSpan(); - try (Scope s = tracer.withSpan(span)) { - // An inlined read/write transaction does not need a write-prepared session. - return getReadSession().transactionManager(); + return getSession().transactionManager(); } catch (RuntimeException e) { TraceUtil.endSpanWithFailure(span, e); throw e; @@ -231,23 +181,9 @@ private TransactionManager inlinedTransactionManager() { @Override public AsyncRunner runAsync() { - return inlineBeginReadWriteTransactions ? inlinedRunAsync() : preparedRunAsync(); - } - - private AsyncRunner preparedRunAsync() { Span span = tracer.spanBuilder(READ_WRITE_TRANSACTION).startSpan(); try (Scope s = tracer.withSpan(span)) { - return getReadWriteSession().runAsync(); - } catch (RuntimeException e) { - TraceUtil.endSpanWithFailure(span, e); - throw e; - } - } - - private AsyncRunner inlinedRunAsync() { - Span span = tracer.spanBuilder(READ_WRITE_TRANSACTION_WITH_INLINE_BEGIN).startSpan(); - try (Scope s = tracer.withSpan(span)) { - return getReadSession().runAsync(); + return getSession().runAsync(); } catch (RuntimeException e) { TraceUtil.endSpanWithFailure(span, e); throw e; @@ -256,25 +192,9 @@ private AsyncRunner inlinedRunAsync() { @Override public AsyncTransactionManager transactionManagerAsync() { - return inlineBeginReadWriteTransactions - ? inlinedTransactionManagerAsync() - : preparedTransactionManagerAsync(); - } - - private AsyncTransactionManager preparedTransactionManagerAsync() { Span span = tracer.spanBuilder(READ_WRITE_TRANSACTION).startSpan(); try (Scope s = tracer.withSpan(span)) { - return getReadWriteSession().transactionManagerAsync(); - } catch (RuntimeException e) { - TraceUtil.endSpanWithFailure(span, e); - throw e; - } - } - - private AsyncTransactionManager inlinedTransactionManagerAsync() { - Span span = tracer.spanBuilder(READ_WRITE_TRANSACTION_WITH_INLINE_BEGIN).startSpan(); - try (Scope s = tracer.withSpan(span)) { - return getReadSession().transactionManagerAsync(); + return getSession().transactionManagerAsync(); } catch (RuntimeException e) { TraceUtil.endSpanWithFailure(span, e); throw e; @@ -285,10 +205,7 @@ private AsyncTransactionManager inlinedTransactionManagerAsync() { public long executePartitionedUpdate(final Statement stmt) { Span span = tracer.spanBuilder(PARTITION_DML_TRANSACTION).startSpan(); try (Scope s = tracer.withSpan(span)) { - // A partitioned update transaction does not need a prepared write session, as the transaction - // object will start a new transaction with specific options anyway. return runWithSessionRetry( - SessionMode.READ, new Function() { @Override public Long apply(Session session) { @@ -301,17 +218,13 @@ public Long apply(Session session) { } } - private T runWithSessionRetry(SessionMode mode, Function callable) { - PooledSessionFuture session = - mode == SessionMode.READ_WRITE ? getReadWriteSession() : getReadSession(); + private T runWithSessionRetry(Function callable) { + PooledSessionFuture session = getSession(); while (true) { try { return callable.apply(session); } catch (SessionNotFoundException e) { - session = - mode == SessionMode.READ_WRITE - ? pool.replaceReadWriteSession(e, session) - : pool.replaceReadSession(e, session); + session = pool.replaceSession(e, session); } } } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/MetricRegistryConstants.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/MetricRegistryConstants.java index 8da8ee1506..3512a75732 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/MetricRegistryConstants.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/MetricRegistryConstants.java @@ -36,9 +36,22 @@ class MetricRegistryConstants { private static final LabelValue UNSET_LABEL = LabelValue.create(null); static final LabelValue NUM_IN_USE_SESSIONS = LabelValue.create("num_in_use_sessions"); + + /** + * The session pool no longer prepares a fraction of the sessions with a read/write transaction. + * This metric will therefore always be zero and may be removed in the future. + */ + @Deprecated static final LabelValue NUM_SESSIONS_BEING_PREPARED = LabelValue.create("num_sessions_being_prepared"); + static final LabelValue NUM_READ_SESSIONS = LabelValue.create("num_read_sessions"); + + /** + * The session pool no longer prepares a fraction of the sessions with a read/write transaction. + * This metric will therefore always be zero and may be removed in the future. + */ + @Deprecated static final LabelValue NUM_WRITE_SESSIONS = LabelValue.create("num_write_prepared_sessions"); static final ImmutableList SPANNER_LABEL_KEYS = 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 d707cf0ef6..6a91d85fef 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 @@ -228,34 +228,24 @@ public ReadOnlyTransaction readOnlyTransaction(TimestampBound bound) { @Override public TransactionRunner readWriteTransaction() { return setActive( - new TransactionRunnerImpl( - this, - spanner.getRpc(), - spanner.getDefaultPrefetchChunks(), - spanner.getOptions().isInlineBeginForReadWriteTransaction())); + new TransactionRunnerImpl(this, spanner.getRpc(), spanner.getDefaultPrefetchChunks())); } @Override public AsyncRunner runAsync() { return new AsyncRunnerImpl( setActive( - new TransactionRunnerImpl( - this, - spanner.getRpc(), - spanner.getDefaultPrefetchChunks(), - spanner.getOptions().isInlineBeginForReadWriteTransaction()))); + new TransactionRunnerImpl(this, spanner.getRpc(), spanner.getDefaultPrefetchChunks()))); } @Override public TransactionManager transactionManager() { - return new TransactionManagerImpl( - this, currentSpan, spanner.getOptions().isInlineBeginForReadWriteTransaction()); + return new TransactionManagerImpl(this, currentSpan); } @Override public AsyncTransactionManagerImpl transactionManagerAsync() { - return new AsyncTransactionManagerImpl( - this, currentSpan, spanner.getOptions().isInlineBeginForReadWriteTransaction()); + return new AsyncTransactionManagerImpl(this, currentSpan); } @Override diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPool.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPool.java index 2512024117..5e8788e166 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPool.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPool.java @@ -64,7 +64,6 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.SettableFuture; -import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.google.protobuf.Empty; import io.opencensus.common.Scope; import io.opencensus.common.ToLongFunction; @@ -81,7 +80,6 @@ import io.opencensus.trace.Tracer; import io.opencensus.trace.Tracing; import java.util.ArrayList; -import java.util.Arrays; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; @@ -92,10 +90,8 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; -import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; @@ -109,10 +105,8 @@ import org.threeten.bp.Instant; /** - * Maintains a pool of sessions some of which might be prepared for write by invoking - * BeginTransaction rpc. It maintains two queues of sessions(read and write prepared) and two queues - * of waiters who are waiting for a session to become available. This class itself is thread safe - * and is meant to be used concurrently across multiple threads. + * Maintains a pool of sessions. This class itself is thread safe and is meant to be used + * concurrently across multiple threads. */ final class SessionPool { @@ -319,7 +313,7 @@ private void replaceSessionIfPossible(SessionNotFoundException notFound) { if (isSingleUse || !sessionUsedForQuery) { // This class is only used by read-only transactions, so we know that we only need a // read-only session. - session = sessionPool.replaceReadSession(notFound, session); + session = sessionPool.replaceSession(notFound, session); readContextDelegate = readContextDelegateSupplier.apply(session); } else { throw notFound; @@ -735,7 +729,7 @@ public TransactionContext begin() { try { return internalBegin(); } catch (SessionNotFoundException e) { - session = sessionPool.replaceReadWriteSession(e, session); + session = sessionPool.replaceSession(e, session); delegate = session.get().delegate.transactionManager(); } } @@ -748,7 +742,7 @@ private TransactionContext internalBegin() { } private SpannerException handleSessionNotFound(SessionNotFoundException notFound) { - session = sessionPool.replaceReadWriteSession(notFound, session); + session = sessionPool.replaceSession(notFound, session); delegate = session.get().delegate.transactionManager(); restartedAfterSessionNotFound = true; return SpannerExceptionFactory.newSpannerException( @@ -789,7 +783,7 @@ public TransactionContext resetForRetry() { return new SessionPoolTransactionContext(delegate.resetForRetry()); } } catch (SessionNotFoundException e) { - session = sessionPool.replaceReadWriteSession(e, session); + session = sessionPool.replaceSession(e, session); delegate = session.get().delegate.transactionManager(); restartedAfterSessionNotFound = true; } @@ -828,7 +822,7 @@ public TransactionState getState() { /** * {@link TransactionRunner} that automatically handles {@link SessionNotFoundException}s by - * replacing the underlying read/write session and then restarts the transaction. + * replacing the underlying session and then restarts the transaction. */ private static final class SessionPoolTransactionRunner implements TransactionRunner { private final SessionPool sessionPool; @@ -857,7 +851,7 @@ public T run(TransactionCallable callable) { result = getRunner().run(callable); break; } catch (SessionNotFoundException e) { - session = sessionPool.replaceReadWriteSession(e, session); + session = sessionPool.replaceSession(e, session); runner = session.get().delegate.readWriteTransaction(); } } @@ -915,8 +909,7 @@ public void run() { se = SpannerExceptionFactory.newSpannerException(t); } finally { if (se != null && se instanceof SessionNotFoundException) { - session = - sessionPool.replaceReadWriteSession((SessionNotFoundException) se, session); + session = sessionPool.replaceSession((SessionNotFoundException) se, session); } else { break; } @@ -965,109 +958,6 @@ private enum SessionState { CLOSING, } - /** - * Forwarding future that will return a {@link PooledSession}. If {@link #inProcessPrepare} has - * been set to true, the returned session will be prepared with a read/write session using the - * thread of the caller to {@link #get()}. This ensures that the executor that is responsible for - * background preparing of read/write transactions is not overwhelmed by requests in case of a - * large burst of write requests. Instead of filling up the queue of the background executor, the - * caller threads will be used for the BeginTransaction call. - */ - private final class ForwardingListenablePooledSessionFuture - extends SimpleForwardingListenableFuture { - private final boolean inProcessPrepare; - private final Span span; - private volatile boolean initialized = false; - private final Object prepareLock = new Object(); - private volatile PooledSession result; - private volatile SpannerException error; - - private ForwardingListenablePooledSessionFuture( - ListenableFuture delegate, boolean inProcessPrepare, Span span) { - super(delegate); - this.inProcessPrepare = inProcessPrepare; - this.span = span; - } - - @Override - public PooledSession get() throws InterruptedException, ExecutionException { - try { - return initialize(super.get()); - } catch (ExecutionException e) { - throw SpannerExceptionFactory.newSpannerException(e.getCause()); - } catch (InterruptedException e) { - throw SpannerExceptionFactory.propagateInterrupt(e); - } - } - - @Override - public PooledSession get(long timeout, TimeUnit unit) - throws InterruptedException, ExecutionException, TimeoutException { - try { - return initialize(super.get(timeout, unit)); - } catch (ExecutionException e) { - throw SpannerExceptionFactory.newSpannerException(e.getCause()); - } catch (InterruptedException e) { - throw SpannerExceptionFactory.propagateInterrupt(e); - } catch (TimeoutException e) { - throw SpannerExceptionFactory.propagateTimeout(e); - } - } - - private PooledSession initialize(PooledSession sess) { - if (!initialized) { - synchronized (prepareLock) { - if (!initialized) { - try { - result = prepare(sess); - } catch (Throwable t) { - error = SpannerExceptionFactory.newSpannerException(t); - } finally { - initialized = true; - } - } - } - } - if (error != null) { - throw error; - } - return result; - } - - private PooledSession prepare(PooledSession sess) { - if (inProcessPrepare && !sess.delegate.hasReadyTransaction()) { - while (true) { - try { - sess.prepareReadWriteTransaction(); - synchronized (lock) { - stopAutomaticPrepare = false; - } - break; - } catch (Throwable t) { - if (isClosed()) { - span.addAnnotation("Pool has been closed"); - throw new IllegalStateException("Pool has been closed"); - } - SpannerException e = newSpannerException(t); - WaiterFuture waiter = new WaiterFuture(); - synchronized (lock) { - handlePrepareSessionFailure(e, sess, false); - if (!isSessionNotFound(e)) { - throw e; - } - readWaiters.add(waiter); - } - sess = waiter.get(); - if (sess.delegate.hasReadyTransaction()) { - break; - } - } - } - } - return sess; - } - } - private PooledSessionFuture createPooledSessionFuture( ListenableFuture future, Span span) { return new PooledSessionFuture(future, span); @@ -1634,18 +1524,15 @@ private void removeIdleSessions(Instant currTime) { synchronized (lock) { // Determine the minimum last use time for a session to be deemed to still be alive. Remove // all sessions that have a lastUseTime before that time, unless it would cause us to go - // below MinSessions. Prefer to remove read sessions above write-prepared sessions. + // below MinSessions. Instant minLastUseTime = currTime.minus(options.getRemoveInactiveSessionAfter()); - for (Iterator iterator : - Arrays.asList( - readSessions.descendingIterator(), writePreparedSessions.descendingIterator())) { - while (iterator.hasNext()) { - PooledSession session = iterator.next(); - if (session.lastUseTime.isBefore(minLastUseTime)) { - if (session.state != SessionState.CLOSING) { - removeFromPool(session); - iterator.remove(); - } + Iterator iterator = sessions.descendingIterator(); + while (iterator.hasNext()) { + PooledSession session = iterator.next(); + if (session.lastUseTime.isBefore(minLastUseTime)) { + if (session.state != SessionState.CLOSING) { + removeFromPool(session); + iterator.remove(); } } } @@ -1675,12 +1562,7 @@ private void keepAliveSessions(Instant currTime) { while (numSessionsToKeepAlive > 0) { PooledSession sessionToKeepAlive = null; synchronized (lock) { - sessionToKeepAlive = findSessionToKeepAlive(readSessions, keepAliveThreshold, 0); - if (sessionToKeepAlive == null) { - sessionToKeepAlive = - findSessionToKeepAlive( - writePreparedSessions, keepAliveThreshold, readSessions.size()); - } + sessionToKeepAlive = findSessionToKeepAlive(sessions, keepAliveThreshold, 0); } if (sessionToKeepAlive == null) { break; @@ -1716,9 +1598,7 @@ private static enum Position { private final SessionClient sessionClient; private final ScheduledExecutorService executor; private final ExecutorFactory executorFactory; - private final ScheduledExecutorService prepareExecutor; - private final int prepareThreadPoolSize; final PoolMaintainer poolMaintainer; private final Clock clock; private final Object lock = new Object(); @@ -1740,19 +1620,10 @@ private static enum Position { private boolean stopAutomaticPrepare; @GuardedBy("lock") - private final LinkedList readSessions = new LinkedList<>(); - - @GuardedBy("lock") - private final LinkedList writePreparedSessions = new LinkedList<>(); - - @GuardedBy("lock") - private final Queue readWaiters = new LinkedList<>(); + private final LinkedList sessions = new LinkedList<>(); @GuardedBy("lock") - private final Queue readWriteWaiters = new LinkedList<>(); - - @GuardedBy("lock") - private int numSessionsBeingPrepared = 0; + private final Queue waiters = new LinkedList<>(); @GuardedBy("lock") private int numSessionsBeingCreated = 0; @@ -1769,12 +1640,6 @@ private static enum Position { @GuardedBy("lock") private long numSessionsReleased = 0; - @GuardedBy("lock") - private long numSessionsInProcessPrepared = 0; - - @GuardedBy("lock") - private long numSessionsAsyncPrepared = 0; - @GuardedBy("lock") private long numIdleSessionsRemoved = 0; @@ -1859,18 +1724,6 @@ private SessionPool( this.options = options; this.executorFactory = executorFactory; this.executor = executor; - if (executor instanceof ThreadPoolExecutor) { - prepareThreadPoolSize = Math.max(((ThreadPoolExecutor) executor).getCorePoolSize(), 1); - } else { - prepareThreadPoolSize = 8; - } - this.prepareExecutor = - Executors.newScheduledThreadPool( - prepareThreadPoolSize, - new ThreadFactoryBuilder() - .setDaemon(true) - .setNameFormat("session-pool-prepare-%d") - .build()); this.sessionClient = sessionClient; this.clock = clock; this.poolMaintainer = new PoolMaintainer(); @@ -1884,19 +1737,6 @@ int getNumberOfSessionsInUse() { } } - long getNumberOfSessionsInProcessPrepared() { - synchronized (lock) { - return numSessionsInProcessPrepared; - } - } - - @VisibleForTesting - long getNumberOfSessionsAsyncPrepared() { - synchronized (lock) { - return numSessionsAsyncPrepared; - } - } - void removeFromPool(PooledSession session) { synchronized (lock) { if (isClosed()) { @@ -1918,24 +1758,10 @@ long numIdleSessionsRemoved() { } } - @VisibleForTesting - int getNumberOfAvailableWritePreparedSessions() { - synchronized (lock) { - return writePreparedSessions.size(); - } - } - @VisibleForTesting int getNumberOfSessionsInPool() { synchronized (lock) { - return readSessions.size() + writePreparedSessions.size() + numSessionsBeingPrepared; - } - } - - @VisibleForTesting - int getNumberOfWriteSessionsInPool() { - synchronized (lock) { - return writePreparedSessions.size() + numSessionsBeingPrepared; + return sessions.size(); } } @@ -1946,13 +1772,6 @@ int getNumberOfSessionsBeingCreated() { } } - @VisibleForTesting - int getNumberOfSessionsBeingPrepared() { - synchronized (lock) { - return numSessionsBeingPrepared; - } - } - @VisibleForTesting long getNumWaiterTimeouts() { return numWaiterTimeouts.get(); @@ -1989,11 +1808,6 @@ private boolean isDatabaseOrInstanceNotFound(SpannerException e) { return e instanceof DatabaseNotFoundException || e instanceof InstanceNotFoundException; } - private boolean shouldStopPrepareSessions(SpannerException e) { - return isDatabaseOrInstanceNotFound(e) - || SHOULD_STOP_PREPARE_SESSIONS_ERROR_CODES.contains(e.getErrorCode()); - } - private void invalidateSession(PooledSession session) { synchronized (lock) { if (isClosed()) { @@ -2031,8 +1845,8 @@ boolean isValid() { } /** - * Returns a session to be used for read requests to spanner. It will block if a session is not - * currently available. In case the pool is exhausted and {@link + * Returns a session to be used for requests to spanner. This method is always non-blocking and + * returns a {@link PooledSessionFuture}. In case the pool is exhausted and {@link * SessionPoolOptions#isFailIfPoolExhausted()} has been set, it will throw an exception. Returned * session must be closed by calling {@link Session#close()}. * @@ -2040,13 +1854,12 @@ boolean isValid() { * *
    *
  1. If a read session is available, return that. - *
  2. Otherwise if a writePreparedSession is available, return that. *
  3. Otherwise if a session can be created, fire a creation request. *
  4. Wait for a session to become available. Note that this can be unblocked either by a * session being returned to the pool or a new session being created. *
*/ - PooledSessionFuture getReadSession() throws SpannerException { + PooledSessionFuture getSession() throws SpannerException { Span span = Tracing.getTracer().getCurrentSpan(); span.addAnnotation("Acquiring session"); WaiterFuture waiter = null; @@ -2065,151 +1878,39 @@ PooledSessionFuture getReadSession() throws SpannerException { resourceNotFoundException.getMessage()), resourceNotFoundException); } - sess = readSessions.poll(); + sess = sessions.poll(); if (sess == null) { - sess = writePreparedSessions.poll(); - if (sess == null) { - span.addAnnotation("No session available"); - maybeCreateSession(); - waiter = new WaiterFuture(); - readWaiters.add(waiter); - } else { - span.addAnnotation("Acquired read write session"); - } + span.addAnnotation("No session available"); + maybeCreateSession(); + waiter = new WaiterFuture(); + waiters.add(waiter); } else { - span.addAnnotation("Acquired read only session"); + span.addAnnotation("Acquired session"); } - return checkoutSession(span, sess, waiter, false, false); - } - } - - /** - * Returns a session which has been prepared for writes by invoking BeginTransaction rpc. It will - * block if such a session is not currently available.In case the pool is exhausted and {@link - * SessionPoolOptions#isFailIfPoolExhausted()} has been set, it will throw an exception. Returned - * session must closed by invoking {@link Session#close()}. - * - *

Implementation strategy: - * - *

    - *
  1. If a writePreparedSession is available, return that. - *
  2. Otherwise if we have an extra session being prepared for write, wait for that. - *
  3. Otherwise, if there is a read session available, start preparing that for write and wait. - *
  4. Otherwise start creating a new session and wait. - *
  5. Wait for write prepared session to become available. This can be unblocked either by the - * session create/prepare request we fired in above request or by a session being released - * to the pool which is then write prepared. - *
- */ - PooledSessionFuture getReadWriteSession() { - Span span = Tracing.getTracer().getCurrentSpan(); - span.addAnnotation("Acquiring read write session"); - PooledSession sess = null; - WaiterFuture waiter = null; - boolean inProcessPrepare = stopAutomaticPrepare; - synchronized (lock) { - if (closureFuture != null) { - span.addAnnotation("Pool has been closed"); - throw new IllegalStateException("Pool has been closed", closedException); - } - if (resourceNotFoundException != null) { - span.addAnnotation("Database has been deleted"); - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.NOT_FOUND, - String.format( - "The session pool has been invalidated because a previous RPC returned 'Database not found': %s", - resourceNotFoundException.getMessage()), - resourceNotFoundException); - } - sess = writePreparedSessions.poll(); - if (sess == null) { - if (!inProcessPrepare && numSessionsBeingPrepared <= prepareThreadPoolSize) { - if (numSessionsBeingPrepared <= readWriteWaiters.size()) { - PooledSession readSession = readSessions.poll(); - if (readSession != null) { - span.addAnnotation( - "Acquired read only session. Preparing for read write transaction"); - prepareSession(readSession); - } else { - span.addAnnotation("No session available"); - maybeCreateSession(); - } - } - } else { - inProcessPrepare = true; - numSessionsInProcessPrepared++; - PooledSession readSession = readSessions.poll(); - if (readSession != null) { - // Create a read/write transaction in-process if there is already a queue for prepared - // sessions. This is more efficient than doing it asynchronously, as it scales with - // the number of user threads. The thread pool for asynchronously preparing sessions - // is fixed. - span.addAnnotation( - "Acquired read only session. Preparing in-process for read write transaction"); - sess = readSession; - } else { - span.addAnnotation("No session available"); - maybeCreateSession(); - } - } - if (sess == null) { - waiter = new WaiterFuture(); - if (inProcessPrepare) { - // inProcessPrepare=true means that we have already determined that the queue for - // preparing read/write sessions is larger than the number of threads in the prepare - // thread pool, and that it's more efficient to do the prepare in-process. We will - // therefore create a waiter for a read-only session, even though a read/write session - // has been requested. - readWaiters.add(waiter); - } else { - readWriteWaiters.add(waiter); - } - } - } else { - span.addAnnotation("Acquired read write session"); - } - return checkoutSession(span, sess, waiter, true, inProcessPrepare); + return checkoutSession(span, sess, waiter); } } private PooledSessionFuture checkoutSession( - final Span span, - final PooledSession readySession, - WaiterFuture waiter, - boolean write, - final boolean inProcessPrepare) { + final Span span, final PooledSession readySession, WaiterFuture waiter) { ListenableFuture sessionFuture; if (waiter != null) { logger.log( Level.FINE, "No session available in the pool. Blocking for one to become available/created"); - span.addAnnotation( - String.format( - "Waiting for %s session to be available", write ? "read write" : "read only")); + span.addAnnotation(String.format("Waiting for a session to come available")); sessionFuture = waiter; } else { SettableFuture fut = SettableFuture.create(); fut.set(readySession); sessionFuture = fut; } - ForwardingListenablePooledSessionFuture forwardingFuture = - new ForwardingListenablePooledSessionFuture(sessionFuture, inProcessPrepare, span); - PooledSessionFuture res = createPooledSessionFuture(forwardingFuture, span); + PooledSessionFuture res = createPooledSessionFuture(sessionFuture, span); res.markCheckedOut(); return res; } - PooledSessionFuture replaceReadSession(SessionNotFoundException e, PooledSessionFuture session) { - return replaceSession(e, session, false); - } - - PooledSessionFuture replaceReadWriteSession( - SessionNotFoundException e, PooledSessionFuture session) { - return replaceSession(e, session, true); - } - - private PooledSessionFuture replaceSession( - SessionNotFoundException e, PooledSessionFuture session, boolean write) { + PooledSessionFuture replaceSession(SessionNotFoundException e, PooledSessionFuture session) { if (!options.isFailIfSessionNotFound() && session.get().allowReplacing) { synchronized (lock) { numSessionsInUse--; @@ -2218,7 +1919,7 @@ private PooledSessionFuture replaceSession( } session.leakedException = null; invalidateSession(session.get()); - return write ? getReadWriteSession() : getReadSession(); + return getSession(); } else { throw e; } @@ -2258,47 +1959,29 @@ private void maybeCreateSession() { } } } - /** - * Releases a session back to the pool. This might cause one of the waiters to be unblocked. - * - *

Implementation note: - * - *

    - *
  1. If there are no pending waiters, either add to the read sessions queue or start preparing - * for write depending on what fraction of sessions are already prepared for writes. - *
  2. Otherwise either unblock a waiting reader or start preparing for a write. Exact strategy - * on which option we chose, in case there are both waiting readers and writers, is - * implemented in {@link #shouldUnblockReader} - *
- */ + /** Releases a session back to the pool. This might cause one of the waiters to be unblocked. */ private void releaseSession(PooledSession session, Position position) { Preconditions.checkNotNull(session); synchronized (lock) { if (closureFuture != null) { return; } - if (readWaiters.size() == 0 && numSessionsBeingPrepared >= readWriteWaiters.size()) { + if (waiters.size() == 0) { // No pending waiters - if (shouldPrepareSession()) { - prepareSession(session); - } else { - switch (position) { - case RANDOM: - if (!readSessions.isEmpty()) { - int pos = random.nextInt(readSessions.size() + 1); - readSessions.add(pos, session); - break; - } - // fallthrough - case FIRST: - default: - readSessions.addFirst(session); - } + switch (position) { + case RANDOM: + if (!sessions.isEmpty()) { + int pos = random.nextInt(sessions.size() + 1); + sessions.add(pos, session); + break; + } + // fallthrough + case FIRST: + default: + sessions.addFirst(session); } - } else if (shouldUnblockReader()) { - readWaiters.poll().put(session); } else { - prepareSession(session); + waiters.poll().put(session); } } } @@ -2306,10 +1989,8 @@ private void releaseSession(PooledSession session, Position position) { private void handleCreateSessionsFailure(SpannerException e, int count) { synchronized (lock) { for (int i = 0; i < count; i++) { - if (readWaiters.size() > 0) { - readWaiters.poll().put(e); - } else if (readWriteWaiters.size() > 0) { - readWriteWaiters.poll().put(e); + if (waiters.size() > 0) { + waiters.poll().put(e); } else { break; } @@ -2320,42 +2001,6 @@ private void handleCreateSessionsFailure(SpannerException e, int count) { } } - private void handlePrepareSessionFailure( - SpannerException e, PooledSession session, boolean informFirstWaiter) { - synchronized (lock) { - if (isSessionNotFound(e)) { - invalidateSession(session); - } else if (shouldStopPrepareSessions(e)) { - // Database has been deleted or the user has no permission to write to this database, or - // there is some other semi-permanent error. We should stop trying to prepare any - // transactions. Also propagate the error to all waiters if the database or instance has - // been deleted, as any further waiting is pointless. - stopAutomaticPrepare = true; - while (readWriteWaiters.size() > 0) { - readWriteWaiters.poll().put(e); - } - while (readWaiters.size() > 0) { - readWaiters.poll().put(e); - } - if (isDatabaseOrInstanceNotFound(e)) { - // Remove the session from the pool. - if (isClosed()) { - decrementPendingClosures(1); - } - allSessions.remove(session); - setResourceNotFoundException((ResourceNotFoundException) e); - } else { - releaseSession(session, Position.FIRST); - } - } else if (informFirstWaiter && readWriteWaiters.size() > 0) { - releaseSession(session, Position.FIRST); - readWriteWaiters.poll().put(e); - } else { - releaseSession(session, Position.FIRST); - } - } - } - void setResourceNotFoundException(ResourceNotFoundException e) { this.resourceNotFoundException = MoreObjects.firstNonNull(this.resourceNotFoundException, e); } @@ -2368,9 +2013,9 @@ private void decrementPendingClosures(int count) { } /** - * Close all the sessions. Once this method is invoked {@link #getReadSession()} and {@link - * #getReadWriteSession()} will start throwing {@code IllegalStateException}. The returned future - * blocks till all the sessions created in this pool have been closed. + * Close all the sessions. Once this method is invoked {@link #getSession()} will start throwing + * {@code IllegalStateException}. The returned future blocks till all the sessions created in this + * pool have been closed. */ ListenableFuture closeAsync(ClosedException closedException) { ListenableFuture retFuture = null; @@ -2380,40 +2025,18 @@ ListenableFuture closeAsync(ClosedException closedException) { } this.closedException = closedException; // Fail all pending waiters. - WaiterFuture waiter = readWaiters.poll(); - while (waiter != null) { - waiter.put(newSpannerException(ErrorCode.INTERNAL, "Client has been closed")); - waiter = readWaiters.poll(); - } - waiter = readWriteWaiters.poll(); + WaiterFuture waiter = waiters.poll(); while (waiter != null) { waiter.put(newSpannerException(ErrorCode.INTERNAL, "Client has been closed")); - waiter = readWriteWaiters.poll(); + waiter = waiters.poll(); } closureFuture = SettableFuture.create(); retFuture = closureFuture; pendingClosure = - totalSessions() - + numSessionsBeingCreated - + 2 /* For pool maintenance thread + prepareExecutor */; + totalSessions() + numSessionsBeingCreated + 1 /* For pool maintenance thread */; poolMaintainer.close(); - readSessions.clear(); - writePreparedSessions.clear(); - prepareExecutor.shutdown(); - executor.submit( - new Runnable() { - @Override - public void run() { - try { - prepareExecutor.awaitTermination(5L, TimeUnit.SECONDS); - } catch (Throwable t) { - } - synchronized (lock) { - decrementPendingClosures(1); - } - } - }); + sessions.clear(); for (PooledSessionFuture session : checkedOutSessions) { if (session.leakedException != null) { if (options.isFailOnSessionLeak()) { @@ -2440,29 +2063,9 @@ public void run() { return retFuture; } - private boolean shouldUnblockReader() { - // This might not be the best strategy since a continuous burst of read requests can starve - // a write request. Maybe maintain a timestamp in the queue and unblock according to that - // or just flip a weighted coin. - synchronized (lock) { - int numWriteWaiters = readWriteWaiters.size() - numSessionsBeingPrepared; - return readWaiters.size() > numWriteWaiters; - } - } - - private boolean shouldPrepareSession() { - synchronized (lock) { - if (stopAutomaticPrepare) { - return false; - } - int preparedSessions = writePreparedSessions.size() + numSessionsBeingPrepared; - return preparedSessions < Math.floor(options.getWriteSessionsFraction() * totalSessions()); - } - } - private int numWaiters() { synchronized (lock) { - return readWaiters.size() + readWriteWaiters.size(); + return waiters.size(); } } @@ -2497,43 +2100,6 @@ public void run() { return res; } - private void prepareSession(final PooledSession sess) { - synchronized (lock) { - numSessionsBeingPrepared++; - } - prepareExecutor.submit( - new Runnable() { - @Override - public void run() { - try { - logger.log(Level.FINE, "Preparing session"); - sess.prepareReadWriteTransaction(); - logger.log(Level.FINE, "Session prepared"); - synchronized (lock) { - numSessionsAsyncPrepared++; - numSessionsBeingPrepared--; - if (!isClosed()) { - if (readWriteWaiters.size() > 0) { - readWriteWaiters.poll().put(sess); - } else if (readWaiters.size() > 0) { - readWaiters.poll().put(sess); - } else { - writePreparedSessions.add(sess); - } - } - } - } catch (Throwable t) { - synchronized (lock) { - numSessionsBeingPrepared--; - if (!isClosed()) { - handlePrepareSessionFailure(newSpannerException(t), sess, true); - } - } - } - } - }); - } - /** * Returns the minimum of the wanted number of sessions that the caller wants to create and the * actual max number that may be created at this moment. @@ -2742,7 +2308,8 @@ public long applyAsLong(SessionPool sessionPool) { new ToLongFunction() { @Override public long applyAsLong(SessionPool sessionPool) { - return sessionPool.numSessionsBeingPrepared; + // TODO: Remove metric. + return 0L; } }); @@ -2766,7 +2333,7 @@ public long applyAsLong(SessionPool sessionPool) { new ToLongFunction() { @Override public long applyAsLong(SessionPool sessionPool) { - return sessionPool.readSessions.size(); + return sessionPool.sessions.size(); } }); @@ -2778,7 +2345,8 @@ public long applyAsLong(SessionPool sessionPool) { new ToLongFunction() { @Override public long applyAsLong(SessionPool sessionPool) { - return sessionPool.writePreparedSessions.size(); + // TODO: Remove metric. + return 0L; } }); } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPoolOptions.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPoolOptions.java index 57dbd4debd..2c68fd317e 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPoolOptions.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPoolOptions.java @@ -31,7 +31,12 @@ public class SessionPoolOptions { private final int maxSessions; private final int incStep; private final int maxIdleSessions; - private final float writeSessionsFraction; + /** + * The session pool no longer prepares a fraction of the sessions with a read/write transaction. + * This setting therefore does not have any meaning anymore, and may be removed in the future. + */ + @Deprecated private final float writeSessionsFraction; + private final ActionOnExhaustion actionOnExhaustion; private final long loopFrequency; private final int keepAliveIntervalMinutes; @@ -74,6 +79,13 @@ public int getMaxIdleSessions() { return maxIdleSessions; } + /** + * @deprecated This value is no longer used. The session pool does not prepare any sessions for + * read/write transactions. Instead, a transaction will be started by including a + * BeginTransaction option with the first statement of a transaction. This method may be + * removed in a future release. + */ + @Deprecated public float getWriteSessionsFraction() { return writeSessionsFraction; } @@ -139,7 +151,12 @@ public static class Builder { private int maxSessions = DEFAULT_MAX_SESSIONS; private int incStep = DEFAULT_INC_STEP; private int maxIdleSessions; - private float writeSessionsFraction = 0.2f; + /** + * The session pool no longer prepares a fraction of the sessions with a read/write transaction. + * This setting therefore does not have any meaning anymore, and may be removed in the future. + */ + @Deprecated private float writeSessionsFraction = 0.2f; + private ActionOnExhaustion actionOnExhaustion = DEFAULT_ACTION; private long initialWaitForSessionTimeoutMillis = 30_000L; private ActionOnSessionNotFound actionOnSessionNotFound = ActionOnSessionNotFound.RETRY; @@ -260,12 +277,11 @@ Builder setFailOnSessionLeak() { } /** - * Fraction of sessions to be kept prepared for write transactions. This is an optimisation to - * avoid the cost of sending a BeginTransaction() rpc. If all such sessions are in use and a - * write request comes, we will make the BeginTransaction() rpc inline. It must be between 0 and - * 1(inclusive). - * - *

Default value is 0.2. + * @deprecated This configuration value is no longer in use. The session pool does not prepare + * any sessions for read/write transactions. Instead, a transaction will automatically be + * started by the first statement that is executed by a transaction by including a + * BeginTransaction option with that statement. + *

This method may be removed in a future release. */ public Builder setWriteSessionsFraction(float writeSessionsFraction) { this.writeSessionsFraction = writeSessionsFraction; @@ -288,9 +304,6 @@ private void validate() { } Preconditions.checkArgument( keepAliveIntervalMinutes < 60, "Keep alive interval should be less than" + "60 minutes"); - Preconditions.checkArgument( - writeSessionsFraction >= 0 && writeSessionsFraction <= 1, - "Fraction of write sessions must be between 0 and 1 (inclusive)"); } } } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java index 7df8105c8f..2d034eda88 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java @@ -233,8 +233,7 @@ public DatabaseClient getDatabaseClient(DatabaseId db) { @VisibleForTesting DatabaseClientImpl createDatabaseClient(String clientId, SessionPool pool) { - return new DatabaseClientImpl( - clientId, pool, getOptions().isInlineBeginForReadWriteTransaction()); + return new DatabaseClientImpl(clientId, pool); } @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 ba702dc093..6bd3d0f90c 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 @@ -97,7 +97,6 @@ public class SpannerOptions extends ServiceOptions { private final int prefetchChunks; private final int numChannels; private final ImmutableMap sessionLabels; - private final boolean inlineBeginForReadWriteTransaction; private final SpannerStubSettings spannerStubSettings; private final InstanceAdminStubSettings instanceAdminStubSettings; private final DatabaseAdminStubSettings databaseAdminStubSettings; @@ -547,7 +546,6 @@ private SpannerOptions(Builder builder) { : SessionPoolOptions.newBuilder().build(); prefetchChunks = builder.prefetchChunks; sessionLabels = builder.sessionLabels; - inlineBeginForReadWriteTransaction = builder.inlineBeginForReadWriteTransaction; try { spannerStubSettings = builder.spannerStubSettingsBuilder.build(); instanceAdminStubSettings = builder.instanceAdminStubSettingsBuilder.build(); @@ -626,7 +624,6 @@ public static class Builder private int prefetchChunks = DEFAULT_PREFETCH_CHUNKS; private SessionPoolOptions sessionPoolOptions; private ImmutableMap sessionLabels; - private boolean inlineBeginForReadWriteTransaction; private SpannerStubSettings.Builder spannerStubSettingsBuilder = SpannerStubSettings.newBuilder(); private InstanceAdminStubSettings.Builder instanceAdminStubSettingsBuilder = @@ -676,7 +673,6 @@ private Builder() { this.sessionPoolOptions = options.sessionPoolOptions; this.prefetchChunks = options.prefetchChunks; this.sessionLabels = options.sessionLabels; - this.inlineBeginForReadWriteTransaction = options.inlineBeginForReadWriteTransaction; this.spannerStubSettingsBuilder = options.spannerStubSettings.toBuilder(); this.instanceAdminStubSettingsBuilder = options.instanceAdminStubSettings.toBuilder(); this.databaseAdminStubSettingsBuilder = options.databaseAdminStubSettings.toBuilder(); @@ -769,34 +765,6 @@ public Builder setSessionLabels(Map sessionLabels) { return this; } - /** - * Sets whether {@link DatabaseClient}s should inline the BeginTransaction option with the first - * query or update statement that is executed by a read/write transaction instead of using a - * write-prepared session from the session pool. Enabling this option can improve execution - * times for read/write transactions in the following scenarios: - * - *

- * - *

    - *
  • Applications with a very high rate of read/write transactions where the session pool is - * not able to prepare new read/write transactions at the same rate as the application is - * requesting read/write transactions. - *
  • Applications with a very low rate of read/write transactions where sessions with a - * prepared read/write transaction are kept in the session pool for a long time without - * being used. - *
- * - * If you enable this option, you should also re-evaluate the value for {@link - * SessionPoolOptions.Builder#setWriteSessionsFraction(float)}. When this option is enabled, - * write-prepared sessions are only used for calls to {@link DatabaseClient#write(Iterable)}. If - * your application does not use this method, you should set the write fraction for the session - * pool to zero. - */ - public Builder setInlineBeginForReadWriteTransaction(boolean inlineBegin) { - this.inlineBeginForReadWriteTransaction = inlineBegin; - return this; - } - /** * {@link SpannerOptions.Builder} does not support global retry settings, as it creates three * different gRPC clients: {@link Spanner}, {@link DatabaseAdminClient} and {@link @@ -1093,10 +1061,6 @@ public Map getSessionLabels() { return sessionLabels; } - public boolean isInlineBeginForReadWriteTransaction() { - return inlineBeginForReadWriteTransaction; - } - public SpannerStubSettings getSpannerStubSettings() { return spannerStubSettings; } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionManagerImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionManagerImpl.java index 3aa7a008ab..b18e2f25d9 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionManagerImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionManagerImpl.java @@ -30,15 +30,13 @@ final class TransactionManagerImpl implements TransactionManager, SessionTransac private final SessionImpl session; private Span span; - private final boolean inlineBegin; private TransactionRunnerImpl.TransactionContextImpl txn; private TransactionState txnState; - TransactionManagerImpl(SessionImpl session, Span span, boolean inlineBegin) { + TransactionManagerImpl(SessionImpl session, Span span) { this.session = session; this.span = span; - this.inlineBegin = inlineBegin; } Span getSpan() { @@ -56,9 +54,6 @@ public TransactionContext begin() { try (Scope s = tracer.withSpan(span)) { txn = session.newTransaction(); session.setActive(this); - if (!inlineBegin) { - txn.ensureTxn(); - } txnState = TransactionState.STARTED; return txn; } @@ -105,7 +100,7 @@ public TransactionContext resetForRetry() { "resetForRetry can only be called if the previous attempt" + " aborted"); } try (Scope s = tracer.withSpan(span)) { - boolean useInlinedBegin = inlineBegin && txn.transactionId != null; + boolean useInlinedBegin = txn.transactionId != null; txn = session.newTransaction(); if (!useInlinedBegin) { txn.ensureTxn(); 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 f885492a03..b825ea7a03 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 @@ -718,7 +718,6 @@ public ListenableAsyncResultSet executeQueryAsync( private boolean blockNestedTxn = true; private final SessionImpl session; private Span span; - private final boolean inlineBegin; private TransactionContextImpl txn; private volatile boolean isValid = true; @@ -728,10 +727,8 @@ public TransactionRunner allowNestedTransaction() { return this; } - TransactionRunnerImpl( - SessionImpl session, SpannerRpc rpc, int defaultPrefetchChunks, boolean inlineBegin) { + TransactionRunnerImpl(SessionImpl session, SpannerRpc rpc, int defaultPrefetchChunks) { this.session = session; - this.inlineBegin = inlineBegin; this.txn = session.newTransaction(); } @@ -765,13 +762,11 @@ private T runInternal(final TransactionCallable txCallable) { new Callable() { @Override public T call() { - boolean useInlinedBegin = inlineBegin; + boolean useInlinedBegin = true; if (attempt.get() > 0) { - if (useInlinedBegin) { - // Do not inline the BeginTransaction during a retry if the initial attempt did not - // actually start a transaction. - useInlinedBegin = txn.transactionId != null; - } + // Do not inline the BeginTransaction during a retry if the initial attempt did not + // actually start a transaction. + useInlinedBegin = txn.transactionId != null; txn = session.newTransaction(); } checkState( diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncRunnerTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncRunnerTest.java index 3869dbdfcf..2af185ae14 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncRunnerTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncRunnerTest.java @@ -140,7 +140,7 @@ public void asyncRunnerUpdateAborted() throws Exception { @Override public ApiFuture doWorkAsync(TransactionContext txn) { if (attempt.incrementAndGet() == 1) { - mockSpanner.abortTransaction(txn); + mockSpanner.abortNextStatement(); } else { // Set the result of the update statement back to 1 row. mockSpanner.putStatementResult( @@ -199,7 +199,7 @@ public void asyncRunnerUpdateAbortedWithoutGettingResult() throws Exception { @Override public ApiFuture doWorkAsync(TransactionContext txn) { if (attempt.incrementAndGet() == 1) { - mockSpanner.abortTransaction(txn); + mockSpanner.abortNextStatement(); } // This update statement will be aborted, but the error will not propagated to the // transaction runner and cause the transaction to retry. Instead, the commit call @@ -217,9 +217,9 @@ public ApiFuture doWorkAsync(TransactionContext txn) { assertThat(mockSpanner.getRequestTypes()) .containsExactly( BatchCreateSessionsRequest.class, - BeginTransactionRequest.class, ExecuteSqlRequest.class, - CommitRequest.class, + // The retry will use an explicit BeginTransaction RPC because the first statement of + // the transaction did not return a transaction id during the initial attempt. BeginTransactionRequest.class, ExecuteSqlRequest.class, CommitRequest.class); @@ -272,10 +272,7 @@ public ApiFuture doWorkAsync(TransactionContext txn) { res.get(); assertThat(mockSpanner.getRequestTypes()) .containsExactly( - BatchCreateSessionsRequest.class, - BeginTransactionRequest.class, - ExecuteSqlRequest.class, - CommitRequest.class); + BatchCreateSessionsRequest.class, ExecuteSqlRequest.class, CommitRequest.class); } @Test @@ -418,9 +415,14 @@ public void asyncRunnerBatchUpdateAbortedWithoutGettingResult() throws Exception @Override public ApiFuture doWorkAsync(TransactionContext txn) { if (attempt.incrementAndGet() == 1) { - mockSpanner.abortTransaction(txn); + mockSpanner.abortNextTransaction(); } - // This update statement will be aborted, but the error will not propagated to the + // This statement will succeed and return a transaction id. The transaction will be + // marked as aborted on the mock server. + txn.executeUpdate(UPDATE_STATEMENT); + + // This batch update statement will be aborted, but the error will not propagated to + // the // transaction runner and cause the transaction to retry. Instead, the commit call // will do that. txn.batchUpdateAsync(ImmutableList.of(UPDATE_STATEMENT, UPDATE_STATEMENT)); @@ -436,10 +438,10 @@ public ApiFuture doWorkAsync(TransactionContext txn) { assertThat(mockSpanner.getRequestTypes()) .containsExactly( BatchCreateSessionsRequest.class, - BeginTransactionRequest.class, + ExecuteSqlRequest.class, ExecuteBatchDmlRequest.class, CommitRequest.class, - BeginTransactionRequest.class, + ExecuteSqlRequest.class, ExecuteBatchDmlRequest.class, CommitRequest.class); } @@ -491,10 +493,7 @@ public ApiFuture doWorkAsync(TransactionContext txn) { res.get(); assertThat(mockSpanner.getRequestTypes()) .containsExactly( - BatchCreateSessionsRequest.class, - BeginTransactionRequest.class, - ExecuteBatchDmlRequest.class, - CommitRequest.class); + BatchCreateSessionsRequest.class, ExecuteBatchDmlRequest.class, CommitRequest.class); } @Test diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncTransactionManagerTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncTransactionManagerTest.java index c7b95f33f6..09523bc7b2 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncTransactionManagerTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncTransactionManagerTest.java @@ -301,7 +301,7 @@ public ApiFuture apply(TransactionContext txn, Long input) public void asyncTransactionManagerFireAndForgetInvalidUpdate() throws Exception { final SettableApiFuture updateCount = SettableApiFuture.create(); - try (AsyncTransactionManager mgr = client().transactionManagerAsync()) { + try (AsyncTransactionManager mgr = clientWithEmptySessionPool().transactionManagerAsync()) { TransactionContextFuture txn = mgr.beginAsync(); while (true) { try { @@ -312,6 +312,8 @@ public void asyncTransactionManagerFireAndForgetInvalidUpdate() throws Exception public ApiFuture apply(TransactionContext txn, Void input) throws Exception { // This fire-and-forget update statement should not fail the transaction. + // The exception will however cause the transaction to be retried, as the + // statement will not return a transaction id. txn.executeUpdateAsync(INVALID_UPDATE_STATEMENT); ApiFutures.addCallback( txn.executeUpdateAsync(UPDATE_STATEMENT), @@ -332,14 +334,26 @@ public void onSuccess(Long result) { }, executor) .commitAsync(); - assertThat(updateCount.get()).isEqualTo(UPDATE_COUNT); assertThat(ts.get()).isNotNull(); + assertThat(updateCount.get()).isEqualTo(UPDATE_COUNT); break; } catch (AbortedException e) { txn = mgr.resetForRetryAsync(); } } } + assertThat(mockSpanner.getRequestTypes()) + .containsExactly( + BatchCreateSessionsRequest.class, + // The first update that fails. This will cause a transaction retry. + ExecuteSqlRequest.class, + // The retry will use an explicit BeginTransaction call. + BeginTransactionRequest.class, + // The first update will again fail, but now there is a transaction id, so the + // transaction can continue. + ExecuteSqlRequest.class, + ExecuteSqlRequest.class, + CommitRequest.class); } @Test @@ -439,7 +453,7 @@ public ApiFuture apply(TransactionContext txn, Void input) throws Exception { if (attempt.incrementAndGet() == 1) { // Abort the first attempt. - mockSpanner.abortTransaction(txn); + mockSpanner.abortNextStatement(); } else { // Set the result of the update statement back to 1 row. mockSpanner.putStatementResult( @@ -479,7 +493,7 @@ public void asyncTransactionManagerUpdateAbortedWithoutGettingResult() throws Ex public ApiFuture apply(TransactionContext txn, Void input) throws Exception { if (attempt.incrementAndGet() == 1) { - mockSpanner.abortTransaction(txn); + mockSpanner.abortNextStatement(); } // This update statement will be aborted, but the error will not // propagated to the transaction runner and cause the transaction to @@ -501,8 +515,8 @@ public ApiFuture apply(TransactionContext txn, Void input) assertThat(mockSpanner.getRequestTypes()) .containsAtLeast( BatchCreateSessionsRequest.class, - BeginTransactionRequest.class, ExecuteSqlRequest.class, + // The retry will use a BeginTransaction RPC. BeginTransactionRequest.class, ExecuteSqlRequest.class, CommitRequest.class); @@ -566,10 +580,7 @@ public ApiFuture apply(TransactionContext txn, Void input) .get(); assertThat(mockSpanner.getRequestTypes()) .containsExactly( - BatchCreateSessionsRequest.class, - BeginTransactionRequest.class, - ExecuteSqlRequest.class, - CommitRequest.class); + BatchCreateSessionsRequest.class, ExecuteSqlRequest.class, CommitRequest.class); break; } catch (AbortedException e) { txn = mgr.resetForRetryAsync(); @@ -685,7 +696,6 @@ public ApiFuture apply(TransactionContext txn, Void input) assertThat(mockSpanner.getRequestTypes()) .containsExactly( BatchCreateSessionsRequest.class, - BeginTransactionRequest.class, ExecuteBatchDmlRequest.class, ExecuteBatchDmlRequest.class, CommitRequest.class); @@ -727,7 +737,6 @@ public ApiFuture apply(TransactionContext txn, Void input) assertThat(mockSpanner.getRequestTypes()) .containsExactly( BatchCreateSessionsRequest.class, - BeginTransactionRequest.class, ExecuteBatchDmlRequest.class, BeginTransactionRequest.class, ExecuteBatchDmlRequest.class, @@ -747,7 +756,7 @@ public void asyncTransactionManagerBatchUpdateAbortedBeforeFirstStatement() thro public ApiFuture apply(TransactionContext txn, Void input) throws Exception { if (attempt.incrementAndGet() == 1) { - mockSpanner.abortTransaction(txn); + mockSpanner.abortNextStatement(); } return txn.batchUpdateAsync( ImmutableList.of(UPDATE_STATEMENT, UPDATE_STATEMENT)); @@ -768,7 +777,6 @@ public ApiFuture apply(TransactionContext txn, Void input) assertThat(mockSpanner.getRequestTypes()) .containsExactly( BatchCreateSessionsRequest.class, - BeginTransactionRequest.class, ExecuteBatchDmlRequest.class, BeginTransactionRequest.class, ExecuteBatchDmlRequest.class, @@ -830,7 +838,6 @@ public ApiFuture apply(TransactionContext txn, long[] input) assertThat(mockSpanner.getRequestTypes()) .containsExactly( BatchCreateSessionsRequest.class, - BeginTransactionRequest.class, ExecuteBatchDmlRequest.class, CommitRequest.class, BeginTransactionRequest.class, @@ -851,7 +858,7 @@ public void asyncTransactionManagerBatchUpdateAbortedWithoutGettingResult() thro public ApiFuture apply(TransactionContext txn, Void input) throws Exception { if (attempt.incrementAndGet() == 1) { - mockSpanner.abortTransaction(txn); + mockSpanner.abortNextStatement(); } // This update statement will be aborted, but the error will not propagated to // the transaction manager and cause the transaction to retry. Instead, the @@ -875,12 +882,11 @@ public ApiFuture apply(TransactionContext txn, Void input) assertThat(attempt.get()).isEqualTo(2); Iterable> requests = mockSpanner.getRequestTypes(); int size = Iterables.size(requests); - assertThat(size).isIn(Range.closed(6, 7)); - if (size == 6) { + assertThat(size).isIn(Range.closed(5, 6)); + if (size == 5) { assertThat(requests) .containsExactly( BatchCreateSessionsRequest.class, - BeginTransactionRequest.class, ExecuteBatchDmlRequest.class, BeginTransactionRequest.class, ExecuteBatchDmlRequest.class, @@ -889,7 +895,6 @@ public ApiFuture apply(TransactionContext txn, Void input) assertThat(requests) .containsExactly( BatchCreateSessionsRequest.class, - BeginTransactionRequest.class, ExecuteBatchDmlRequest.class, CommitRequest.class, BeginTransactionRequest.class, @@ -929,10 +934,7 @@ public void asyncTransactionManagerWithBatchUpdateCommitFails() throws Exception } assertThat(mockSpanner.getRequestTypes()) .containsExactly( - BatchCreateSessionsRequest.class, - BeginTransactionRequest.class, - ExecuteBatchDmlRequest.class, - CommitRequest.class); + BatchCreateSessionsRequest.class, ExecuteBatchDmlRequest.class, CommitRequest.class); } @Test @@ -961,10 +963,7 @@ public ApiFuture apply(TransactionContext txn, Void input) } assertThat(mockSpanner.getRequestTypes()) .containsExactly( - BatchCreateSessionsRequest.class, - BeginTransactionRequest.class, - ExecuteBatchDmlRequest.class, - CommitRequest.class); + BatchCreateSessionsRequest.class, ExecuteBatchDmlRequest.class, CommitRequest.class); } @Test diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BatchCreateSessionsTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BatchCreateSessionsTest.java index abac3bd134..7dac8c8bfe 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BatchCreateSessionsTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BatchCreateSessionsTest.java @@ -19,13 +19,11 @@ import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.Assert.fail; import com.google.api.gax.grpc.testing.LocalChannelProvider; import com.google.cloud.NoCredentials; import com.google.cloud.spanner.MockSpannerServiceImpl.SimulatedExecutionTime; import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult; -import com.google.cloud.spanner.TransactionRunner.TransactionCallable; import com.google.common.base.Stopwatch; import com.google.protobuf.ListValue; import com.google.spanner.v1.ResultSetMetadata; @@ -235,72 +233,4 @@ public void testSpannerReturnsResourceExhausted() throws InterruptedException { // Verify that all sessions have been deleted. assertThat(client.pool.totalSessions(), is(equalTo(0))); } - - @Test - public void testPrepareSessionFailPropagatesToUser() { - // Do not create any sessions by default. - // This also means that when a read/write session is requested, the session pool - // will start preparing a read session at that time. Any errors that might occur - // during the BeginTransaction call will be propagated to the user. - int minSessions = 0; - int maxSessions = 1000; - DatabaseClientImpl client = null; - mockSpanner.setBeginTransactionExecutionTime( - SimulatedExecutionTime.ofStickyException( - Status.ABORTED.withDescription("BeginTransaction failed").asRuntimeException())); - try (Spanner spanner = createSpanner(minSessions, maxSessions)) { - client = - (DatabaseClientImpl) - spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); - TransactionRunner runner = client.readWriteTransaction(); - runner.run( - new TransactionCallable() { - @Override - public Void run(TransactionContext transaction) { - return null; - } - }); - fail("missing expected exception"); - } catch (SpannerException e) { - assertThat(e.getErrorCode(), is(equalTo(ErrorCode.ABORTED))); - assertThat(e.getMessage().endsWith("BeginTransaction failed"), is(true)); - } - } - - @Test - public void testPrepareSessionFailDoesNotPropagateToUser() throws InterruptedException { - // Create 5 sessions and 20% write prepared sessions. - // That should prepare exactly 1 session for r/w. - int minSessions = 5; - int maxSessions = 1000; - DatabaseClientImpl client = null; - // The first prepare should fail. - // The prepare will then be retried and should succeed. - mockSpanner.setBeginTransactionExecutionTime( - SimulatedExecutionTime.ofException( - Status.ABORTED.withDescription("BeginTransaction failed").asRuntimeException())); - try (Spanner spanner = createSpanner(minSessions, maxSessions)) { - client = - (DatabaseClientImpl) - spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); - // Wait until the session pool has initialized and a session has been prepared. - Stopwatch watch = Stopwatch.createStarted(); - while ((client.pool.totalSessions() < minSessions - || client.pool.getNumberOfAvailableWritePreparedSessions() != 1) - && watch.elapsed(TimeUnit.SECONDS) < 10) { - Thread.sleep(10L); - } - - // There should be 1 prepared session and a r/w transaction should succeed. - assertThat(client.pool.getNumberOfAvailableWritePreparedSessions(), is(equalTo(1))); - TransactionRunner runner = client.readWriteTransaction(); - runner.run( - new TransactionCallable() { - @Override - public Void run(TransactionContext transaction) { - return null; - } - }); - } - } } 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 8775fb1b18..2747dc314f 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 @@ -809,65 +809,6 @@ public void testPartitionedDmlRetriesOnUnavailable() { } } - @Test - public void testDatabaseOrInstanceDoesNotExistOnPrepareSession() throws Exception { - StatusRuntimeException[] exceptions = - new StatusRuntimeException[] { - SpannerExceptionFactoryTest.newStatusResourceNotFoundException( - "Database", SpannerExceptionFactory.DATABASE_RESOURCE_TYPE, DATABASE_NAME), - SpannerExceptionFactoryTest.newStatusResourceNotFoundException( - "Instance", SpannerExceptionFactory.INSTANCE_RESOURCE_TYPE, INSTANCE_NAME) - }; - for (StatusRuntimeException exception : exceptions) { - try (Spanner spanner = - SpannerOptions.newBuilder() - .setProjectId(TEST_PROJECT) - .setChannelProvider(channelProvider) - .setCredentials(NoCredentials.getInstance()) - .build() - .getService()) { - mockSpanner.setBeginTransactionExecutionTime( - SimulatedExecutionTime.ofStickyException(exception)); - DatabaseClientImpl dbClient = - (DatabaseClientImpl) - spanner.getDatabaseClient( - DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); - // Wait until all sessions have been created. - Stopwatch watch = Stopwatch.createStarted(); - while (watch.elapsed(TimeUnit.SECONDS) < 5 - && dbClient.pool.getNumberOfSessionsBeingCreated() > 0) { - Thread.sleep(1L); - } - // Ensure that no sessions could be prepared and that the session pool gives up trying to - // prepare sessions. - watch = watch.reset().start(); - while (watch.elapsed(TimeUnit.SECONDS) < 5 - && dbClient.pool.getNumberOfSessionsBeingPrepared() > 0) { - Thread.sleep(1L); - } - assertThat(dbClient.pool.getNumberOfSessionsBeingPrepared()).isEqualTo(0); - assertThat(dbClient.pool.getNumberOfAvailableWritePreparedSessions()).isEqualTo(0); - int currentNumRequest = mockSpanner.getRequests().size(); - try { - dbClient - .readWriteTransaction() - .run( - new TransactionCallable() { - @Override - public Void run(TransactionContext transaction) { - return null; - } - }); - fail("missing expected exception"); - } catch (DatabaseNotFoundException | InstanceNotFoundException e) { - } - assertThat(mockSpanner.getRequests()).hasSize(currentNumRequest); - mockSpanner.reset(); - mockSpanner.removeAllExecutionTimes(); - } - } - } - @Test public void testDatabaseOrInstanceDoesNotExistOnInitialization() throws Exception { StatusRuntimeException[] exceptions = @@ -1001,89 +942,6 @@ public void testDatabaseOrInstanceDoesNotExistOnReplenish() throws Exception { } } - @Test - public void testPermissionDeniedOnPrepareSession() throws Exception { - testExceptionOnPrepareSession( - Status.PERMISSION_DENIED - .withDescription( - "Caller is missing IAM permission spanner.databases.beginOrRollbackReadWriteTransaction on resource") - .asRuntimeException()); - } - - @Test - public void testFailedPreconditionOnPrepareSession() throws Exception { - testExceptionOnPrepareSession( - Status.FAILED_PRECONDITION - .withDescription("FAILED_PRECONDITION: Database is in read-only mode") - .asRuntimeException()); - } - - private void testExceptionOnPrepareSession(StatusRuntimeException exception) - throws InterruptedException { - mockSpanner.setBeginTransactionExecutionTime( - SimulatedExecutionTime.ofStickyException(exception)); - DatabaseClientImpl dbClient = - (DatabaseClientImpl) - spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); - // Wait until all sessions have been created. - Stopwatch watch = Stopwatch.createStarted(); - while (watch.elapsed(TimeUnit.SECONDS) < 5 - && dbClient.pool.getNumberOfSessionsBeingCreated() > 0) { - Thread.sleep(1L); - } - // Ensure that no sessions could be prepared and that the session pool gives up trying to - // prepare sessions. - watch = watch.reset().start(); - while (watch.elapsed(TimeUnit.SECONDS) < 5 - && dbClient.pool.getNumberOfSessionsBeingPrepared() > 0) { - Thread.sleep(1L); - } - assertThat(dbClient.pool.getNumberOfSessionsBeingPrepared()).isEqualTo(0); - assertThat(dbClient.pool.getNumberOfAvailableWritePreparedSessions()).isEqualTo(0); - try { - dbClient - .readWriteTransaction() - .run( - new TransactionCallable() { - @Override - public Void run(TransactionContext transaction) { - return null; - } - }); - fail(String.format("missing expected %s exception", exception.getStatus().getCode().name())); - } catch (SpannerException e) { - assertThat(e.getErrorCode()).isEqualTo(ErrorCode.fromGrpcStatus(exception.getStatus())); - } - // Remove the semi-permanent error condition. Getting a read/write transaction should now - // succeed, and the automatic preparing of sessions should be restarted. - mockSpanner.setBeginTransactionExecutionTime(SimulatedExecutionTime.none()); - dbClient - .readWriteTransaction() - .run( - new TransactionCallable() { - @Override - public Void run(TransactionContext transaction) { - return null; - } - }); - for (int i = 0; i < spanner.getOptions().getSessionPoolOptions().getMinSessions(); i++) { - dbClient.pool.getReadSession().close(); - } - int expectedPreparedSessions = - (int) - Math.ceil( - dbClient.pool.getNumberOfSessionsInPool() - * spanner.getOptions().getSessionPoolOptions().getWriteSessionsFraction()); - watch = watch.reset().start(); - while (watch.elapsed(TimeUnit.SECONDS) < 5 - && dbClient.pool.getNumberOfAvailableWritePreparedSessions() < expectedPreparedSessions) { - Thread.sleep(1L); - } - assertThat(dbClient.pool.getNumberOfSessionsBeingPrepared()).isEqualTo(0); - assertThat(dbClient.pool.getNumberOfAvailableWritePreparedSessions()) - .isEqualTo(expectedPreparedSessions); - } - /** * Test showing that when a database is deleted while it is in use by a database client and then * re-created with the same name, will continue to return {@link DatabaseNotFoundException}s until @@ -1113,8 +971,7 @@ public void testDatabaseOrInstanceIsDeletedAndThenRecreated() throws Exception { // Wait until all sessions have been created and prepared. Stopwatch watch = Stopwatch.createStarted(); while (watch.elapsed(TimeUnit.SECONDS) < 5 - && (dbClient.pool.getNumberOfSessionsBeingCreated() > 0 - || dbClient.pool.getNumberOfSessionsBeingPrepared() > 0)) { + && (dbClient.pool.getNumberOfSessionsBeingCreated() > 0)) { Thread.sleep(1L); } // Simulate that the database or instance has been deleted. diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ITSessionPoolIntegrationTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ITSessionPoolIntegrationTest.java index 66256489e8..548f88172f 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ITSessionPoolIntegrationTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ITSessionPoolIntegrationTest.java @@ -98,12 +98,12 @@ public ScheduledExecutorService get() { @Test public void sessionCreation() { - try (PooledSessionFuture session = pool.getReadSession()) { + try (PooledSessionFuture session = pool.getSession()) { assertThat(session.get()).isNotNull(); } - try (PooledSessionFuture session = pool.getReadSession(); - PooledSessionFuture session2 = pool.getReadSession()) { + try (PooledSessionFuture session = pool.getSession(); + PooledSessionFuture session2 = pool.getSession()) { assertThat(session.get()).isNotNull(); assertThat(session2.get()).isNotNull(); } @@ -111,14 +111,14 @@ public void sessionCreation() { @Test public void poolExhaustion() throws Exception { - Session session1 = pool.getReadSession().get(); - Session session2 = pool.getReadSession().get(); + Session session1 = pool.getSession().get(); + Session session2 = pool.getSession().get(); final CountDownLatch latch = new CountDownLatch(1); new Thread( new Runnable() { @Override public void run() { - try (Session session3 = pool.getReadSession().get()) { + try (Session session3 = pool.getSession().get()) { latch.countDown(); } } @@ -132,8 +132,8 @@ public void run() { @Test public void multipleWaiters() throws Exception { - Session session1 = pool.getReadSession().get(); - Session session2 = pool.getReadSession().get(); + Session session1 = pool.getSession().get(); + Session session2 = pool.getSession().get(); int numSessions = 5; final CountDownLatch latch = new CountDownLatch(numSessions); for (int i = 0; i < numSessions; i++) { @@ -141,7 +141,7 @@ public void multipleWaiters() throws Exception { new Runnable() { @Override public void run() { - try (Session session = pool.getReadSession().get()) { + try (Session session = pool.getSession().get()) { latch.countDown(); } } @@ -161,13 +161,13 @@ public void closeQuicklyDoesNotBlockIndefinitely() throws Exception { @Test public void closeAfterInitialCreateDoesNotBlockIndefinitely() throws Exception { - pool.getReadSession().close(); + pool.getSession().close(); pool.closeAsync(new SpannerImpl.ClosedException()).get(); } @Test public void closeWhenSessionsActiveFinishes() throws Exception { - pool.getReadSession().get(); + pool.getSession().get(); // This will log a warning that a session has been leaked, as the session that we retrieved in // the previous statement was never returned to the pool. pool.closeAsync(new SpannerImpl.ClosedException()).get(); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InlineBeginBenchmark.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InlineBeginBenchmark.java index 3e08f0f633..ecd8f4410d 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InlineBeginBenchmark.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InlineBeginBenchmark.java @@ -77,10 +77,10 @@ public static class BenchmarkState { private Spanner spanner; private DatabaseClientImpl client; - @Param({"false", "true"}) + @Param({"true"}) boolean inlineBegin; - @Param({"0.0", "0.2"}) + @Param({"0.2"}) float writeFraction; @Setup(Level.Invocation) @@ -122,7 +122,6 @@ SpannerOptions createBenchmarkServerOptions(TransportChannelProvider channelProv .setCredentials(NoCredentials.getInstance()) .setSessionPoolOption( SessionPoolOptions.newBuilder().setWriteSessionsFraction(writeFraction).build()) - .setInlineBeginForReadWriteTransaction(inlineBegin) .build(); } @@ -130,7 +129,6 @@ SpannerOptions createRealServerOptions() throws IOException { return SpannerOptions.newBuilder() .setSessionPoolOption( SessionPoolOptions.newBuilder().setWriteSessionsFraction(writeFraction).build()) - .setInlineBeginForReadWriteTransaction(inlineBegin) .build(); } 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 f40fed8615..74b047ecf9 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 @@ -174,9 +174,6 @@ public void setUp() throws IOException { .setProjectId("[PROJECT]") .setChannelProvider(channelProvider) .setCredentials(NoCredentials.getInstance()) - .setInlineBeginForReadWriteTransaction(true) - .setSessionPoolOption( - SessionPoolOptions.newBuilder().setWriteSessionsFraction(0.0f).build()) .build() .getService(); } @@ -1061,7 +1058,8 @@ public ApiFuture apply(TransactionContext txn, Long input) } } } - assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(0); + // The retry will use a BeginTransaction RPC. + assertThat(countRequests(BeginTransactionRequest.class)).isEqualTo(1); assertThat(countTransactionsStarted()).isEqualTo(2); } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/IntegrationTestWithClosedSessionsEnv.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/IntegrationTestWithClosedSessionsEnv.java index 4c50667a39..84c7185e1f 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/IntegrationTestWithClosedSessionsEnv.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/IntegrationTestWithClosedSessionsEnv.java @@ -47,8 +47,7 @@ private static class SpannerWithClosedSessionsImpl extends SpannerImpl { @Override DatabaseClientImpl createDatabaseClient(String clientId, SessionPool pool) { - return new DatabaseClientWithClosedSessionImpl( - clientId, pool, getOptions().isInlineBeginForReadWriteTransaction()); + return new DatabaseClientWithClosedSessionImpl(clientId, pool); } } @@ -60,9 +59,8 @@ public static class DatabaseClientWithClosedSessionImpl extends DatabaseClientIm private boolean invalidateNextSession = false; private boolean allowReplacing = true; - DatabaseClientWithClosedSessionImpl( - String clientId, SessionPool pool, boolean inlineBeginReadWriteTransactions) { - super(clientId, pool, inlineBeginReadWriteTransactions); + DatabaseClientWithClosedSessionImpl(String clientId, SessionPool pool) { + super(clientId, pool); } /** Invalidate the next session that is checked out from the pool. */ @@ -76,22 +74,8 @@ public void setAllowSessionReplacing(boolean allow) { } @Override - PooledSessionFuture getReadSession() { - PooledSessionFuture session = super.getReadSession(); - if (invalidateNextSession) { - session.get().delegate.close(); - session.get().setAllowReplacing(false); - awaitDeleted(session.get().delegate); - session.get().setAllowReplacing(allowReplacing); - invalidateNextSession = false; - } - session.get().setAllowReplacing(allowReplacing); - return session; - } - - @Override - PooledSessionFuture getReadWriteSession() { - PooledSessionFuture session = super.getReadWriteSession(); + PooledSessionFuture getSession() { + PooledSessionFuture session = super.getSession(); if (invalidateNextSession) { session.get().delegate.close(); session.get().setAllowReplacing(false); 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 c6ff7cdbf2..4690a30aa7 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 @@ -125,13 +125,9 @@ public static void stopServer() throws InterruptedException { public void setUp() throws IOException { mockSpanner.reset(); mockSpanner.removeAllExecutionTimes(); - // Create a Spanner instance with no read/write prepared sessions in the pool. spanner = SpannerOptions.newBuilder() .setProjectId("[PROJECT]") - .setInlineBeginForReadWriteTransaction(true) - .setSessionPoolOption( - SessionPoolOptions.newBuilder().setWriteSessionsFraction(0.0f).build()) .setChannelProvider(channelProvider) .setCredentials(NoCredentials.getInstance()) .build() diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RetryOnInvalidatedSessionTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RetryOnInvalidatedSessionTest.java index fcf1c6e35b..5e732c1eab 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RetryOnInvalidatedSessionTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RetryOnInvalidatedSessionTest.java @@ -217,17 +217,6 @@ public void tearDown() { spanner.close(); } - private static void initReadWriteSessionPool() throws InterruptedException { - // Wait for at least one read/write session to be ready. - Stopwatch watch = Stopwatch.createStarted(); - while (((DatabaseClientImpl) client).pool.getNumberOfAvailableWritePreparedSessions() == 0) { - if (watch.elapsed(TimeUnit.SECONDS) > 5L) { - fail("No read/write sessions prepared"); - } - Thread.sleep(5L); - } - } - private static void invalidateSessionPool() throws InterruptedException { invalidateSessionPool(client, spanner.getOptions().getSessionPoolOptions().getMinSessions()); } @@ -576,16 +565,10 @@ public void readOnlyTransactionReadRowUsingIndexNonRecoverable() throws Interrup } } - /** - * Test with one read-only session in the pool that is invalidated. The session pool will try to - * prepare this session for read/write, which will fail with a {@link SessionNotFoundException}. - * That again will trigger the creation of a new session. This will always succeed. - */ @Test public void readWriteTransactionReadOnlySessionInPool() throws InterruptedException { // Create a session pool with only read sessions. - SessionPoolOptions.Builder builder = - SessionPoolOptions.newBuilder().setWriteSessionsFraction(0.0f); + SessionPoolOptions.Builder builder = SessionPoolOptions.newBuilder(); if (failOnInvalidatedSession) { builder.setFailIfSessionNotFound(); } @@ -600,27 +583,31 @@ public void readWriteTransactionReadOnlySessionInPool() throws InterruptedExcept DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); invalidateSessionPool(client, spanner.getOptions().getSessionPoolOptions().getMinSessions()); - TransactionRunner runner = client.readWriteTransaction(); - int count = - runner.run( - new TransactionCallable() { - @Override - public Integer run(TransactionContext transaction) { - int count = 0; - try (ResultSet rs = transaction.executeQuery(SELECT1AND2)) { - while (rs.next()) { - count++; + try { + TransactionRunner runner = client.readWriteTransaction(); + int count = + runner.run( + new TransactionCallable() { + @Override + public Integer run(TransactionContext transaction) { + int count = 0; + try (ResultSet rs = transaction.executeQuery(SELECT1AND2)) { + while (rs.next()) { + count++; + } } + return count; } - return count; - } - }); - assertThat(count).isEqualTo(2); + }); + assertThat(count).isEqualTo(2); + assertThat(failOnInvalidatedSession).isFalse(); + } catch (SessionNotFoundException e) { + assertThat(failOnInvalidatedSession).isTrue(); + } } @Test public void readWriteTransactionSelect() throws InterruptedException { - initReadWriteSessionPool(); invalidateSessionPool(); try { TransactionRunner runner = client.readWriteTransaction(); @@ -647,7 +634,6 @@ public Integer run(TransactionContext transaction) { @Test public void readWriteTransactionRead() throws InterruptedException { - initReadWriteSessionPool(); invalidateSessionPool(); try { TransactionRunner runner = client.readWriteTransaction(); @@ -674,7 +660,6 @@ public Integer run(TransactionContext transaction) { @Test public void readWriteTransactionReadUsingIndex() throws InterruptedException { - initReadWriteSessionPool(); invalidateSessionPool(); try { TransactionRunner runner = client.readWriteTransaction(); @@ -703,7 +688,6 @@ public Integer run(TransactionContext transaction) { @Test public void readWriteTransactionReadRow() throws InterruptedException { - initReadWriteSessionPool(); invalidateSessionPool(); try { TransactionRunner runner = client.readWriteTransaction(); @@ -724,7 +708,6 @@ public Struct run(TransactionContext transaction) { @Test public void readWriteTransactionReadRowUsingIndex() throws InterruptedException { - initReadWriteSessionPool(); invalidateSessionPool(); try { TransactionRunner runner = client.readWriteTransaction(); @@ -746,7 +729,6 @@ public Struct run(TransactionContext transaction) { @Test public void readWriteTransactionUpdate() throws InterruptedException { - initReadWriteSessionPool(); invalidateSessionPool(); try { TransactionRunner runner = client.readWriteTransaction(); @@ -767,7 +749,6 @@ public Long run(TransactionContext transaction) { @Test public void readWriteTransactionBatchUpdate() throws InterruptedException { - initReadWriteSessionPool(); invalidateSessionPool(); try { TransactionRunner runner = client.readWriteTransaction(); @@ -789,7 +770,6 @@ public long[] run(TransactionContext transaction) { @Test public void readWriteTransactionBuffer() throws InterruptedException { - initReadWriteSessionPool(); invalidateSessionPool(); try { TransactionRunner runner = client.readWriteTransaction(); @@ -1022,14 +1002,16 @@ public void transactionManagerReadOnlySessionInPool() throws InterruptedExceptio transaction = manager.resetForRetry(); } } + assertThat(count).isEqualTo(2); + assertThat(failOnInvalidatedSession).isFalse(); + } catch (SessionNotFoundException e) { + assertThat(failOnInvalidatedSession).isTrue(); } - assertThat(count).isEqualTo(2); } @SuppressWarnings("resource") @Test public void transactionManagerSelect() throws InterruptedException { - initReadWriteSessionPool(); invalidateSessionPool(); try (TransactionManager manager = client.transactionManager()) { int count = 0; @@ -1058,7 +1040,6 @@ public void transactionManagerSelect() throws InterruptedException { @SuppressWarnings("resource") @Test public void transactionManagerRead() throws InterruptedException { - initReadWriteSessionPool(); invalidateSessionPool(); try (TransactionManager manager = client.transactionManager()) { int count = 0; @@ -1087,7 +1068,6 @@ public void transactionManagerRead() throws InterruptedException { @SuppressWarnings("resource") @Test public void transactionManagerReadUsingIndex() throws InterruptedException { - initReadWriteSessionPool(); invalidateSessionPool(); try (TransactionManager manager = client.transactionManager()) { int count = 0; @@ -1117,7 +1097,6 @@ public void transactionManagerReadUsingIndex() throws InterruptedException { @SuppressWarnings("resource") @Test public void transactionManagerReadRow() throws InterruptedException { - initReadWriteSessionPool(); invalidateSessionPool(); try (TransactionManager manager = client.transactionManager()) { Struct row; @@ -1142,7 +1121,6 @@ public void transactionManagerReadRow() throws InterruptedException { @SuppressWarnings("resource") @Test public void transactionManagerReadRowUsingIndex() throws InterruptedException { - initReadWriteSessionPool(); invalidateSessionPool(); try (TransactionManager manager = client.transactionManager()) { Struct row; @@ -1167,7 +1145,6 @@ public void transactionManagerReadRowUsingIndex() throws InterruptedException { @SuppressWarnings("resource") @Test public void transactionManagerUpdate() throws InterruptedException { - initReadWriteSessionPool(); invalidateSessionPool(); try (TransactionManager manager = client.transactionManager()) { long count; @@ -1192,7 +1169,6 @@ public void transactionManagerUpdate() throws InterruptedException { @SuppressWarnings("resource") @Test public void transactionManagerBatchUpdate() throws InterruptedException { - initReadWriteSessionPool(); invalidateSessionPool(); try (TransactionManager manager = client.transactionManager()) { long[] count; @@ -1218,7 +1194,6 @@ public void transactionManagerBatchUpdate() throws InterruptedException { @SuppressWarnings("resource") @Test public void transactionManagerBuffer() throws InterruptedException { - initReadWriteSessionPool(); invalidateSessionPool(); try (TransactionManager manager = client.transactionManager()) { TransactionContext transaction = manager.begin(); @@ -1417,7 +1392,6 @@ public void transactionManagerReadRowUsingIndexInvalidatedDuringTransaction() @Test public void partitionedDml() throws InterruptedException { - initReadWriteSessionPool(); invalidateSessionPool(); try { assertThat(client.executePartitionedUpdate(UPDATE_STATEMENT)).isEqualTo(UPDATE_COUNT); @@ -1429,7 +1403,6 @@ public void partitionedDml() throws InterruptedException { @Test public void write() throws InterruptedException { - initReadWriteSessionPool(); invalidateSessionPool(); try { Timestamp timestamp = client.write(Arrays.asList(Mutation.delete("FOO", KeySet.all()))); @@ -1442,7 +1415,6 @@ public void write() throws InterruptedException { @Test public void writeAtLeastOnce() throws InterruptedException { - initReadWriteSessionPool(); invalidateSessionPool(); try { Timestamp timestamp = diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolLeakTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolLeakTest.java index 2dc31bb28a..f559a04b94 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolLeakTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolLeakTest.java @@ -80,15 +80,10 @@ public void setUp() { .setProjectId("[PROJECT]") .setChannelProvider(channelProvider) .setCredentials(NoCredentials.getInstance()); - // Make sure the session pool is empty by default, does not contain any write-prepared sessions, + // Make sure the session pool is empty by default, does not contain any sessions, // contains at most 2 sessions, and creates sessions in steps of 1. builder.setSessionPoolOption( - SessionPoolOptions.newBuilder() - .setMinSessions(0) - .setMaxSessions(2) - .setIncStep(1) - .setWriteSessionsFraction(0.0f) - .build()); + SessionPoolOptions.newBuilder().setMinSessions(0).setMaxSessions(2).setIncStep(1).build()); spanner = builder.build().getService(); client = spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); pool = ((DatabaseClientImpl) client).pool; @@ -162,15 +157,15 @@ public void run() { @Test public void testTransactionManagerExceptionOnBegin() { - transactionManagerTest( - new Runnable() { - @Override - public void run() { - mockSpanner.setBeginTransactionExecutionTime( - SimulatedExecutionTime.ofException(FAILED_PRECONDITION)); - } - }, - 1); + assertThat(pool.getNumberOfSessionsInPool(), is(equalTo(0))); + mockSpanner.setBeginTransactionExecutionTime( + SimulatedExecutionTime.ofException(FAILED_PRECONDITION)); + try (TransactionManager txManager = client.transactionManager()) { + // This should not cause an error, as the actual BeginTransaction will be included with the + // first statement of the transaction. + txManager.begin(); + } + assertThat(pool.getNumberOfSessionsInPool(), is(equalTo(1))); } private void transactionManagerTest(Runnable setup, int expectedNumberOfSessionsAfterExecution) { diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolMaintainerTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolMaintainerTest.java index 0e72b2b9bc..0c965a5573 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolMaintainerTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolMaintainerTest.java @@ -150,8 +150,8 @@ public void testKeepAlive() throws Exception { // Checkout two sessions and do a maintenance loop. Still no sessions should be getting any // pings. - Session session1 = pool.getReadSession(); - Session session2 = pool.getReadSession(); + Session session1 = pool.getSession(); + Session session2 = pool.getSession(); runMaintainanceLoop(clock, pool, 1); assertThat(pingedSessions).isEmpty(); @@ -173,9 +173,9 @@ public void testKeepAlive() throws Exception { // Now check out three sessions so the pool will create an additional session. The pool will // only keep 2 sessions alive, as that is the setting for MinSessions. - Session session3 = pool.getReadSession(); - Session session4 = pool.getReadSession(); - Session session5 = pool.getReadSession(); + Session session3 = pool.getSession(); + Session session4 = pool.getSession(); + Session session5 = pool.getSession(); // Note that session2 was now the first session in the pool as it was the last to receive a // ping. assertThat(session3.getName()).isEqualTo(session2.getName()); @@ -192,7 +192,7 @@ public void testKeepAlive() throws Exception { // should cause only one session to get a ping. clock.currentTimeMillis += TimeUnit.MINUTES.toMillis(options.getKeepAliveIntervalMinutes()) + 1; // We are now checking out session2 because - Session session6 = pool.getReadSession(); + Session session6 = pool.getSession(); // The session that was first in the pool now is equal to the initial first session as each full // round of pings will swap the order of the first MinSessions sessions in the pool. assertThat(session6.getName()).isEqualTo(session1.getName()); @@ -208,9 +208,9 @@ public void testKeepAlive() throws Exception { // Now check out 3 sessions again and make sure the 'extra' session is checked in last. That // will make it eligible for pings. - Session session7 = pool.getReadSession(); - Session session8 = pool.getReadSession(); - Session session9 = pool.getReadSession(); + Session session7 = pool.getSession(); + Session session8 = pool.getSession(); + Session session9 = pool.getSession(); assertThat(session7.getName()).isEqualTo(session1.getName()); assertThat(session8.getName()).isEqualTo(session2.getName()); @@ -244,8 +244,8 @@ public void testIdleSessions() throws Exception { assertThat(idledSessions).isEmpty(); // Checkout two sessions and do a maintenance loop. Still no sessions should be removed. - Session session1 = pool.getReadSession(); - Session session2 = pool.getReadSession(); + Session session1 = pool.getSession(); + Session session2 = pool.getSession(); runMaintainanceLoop(clock, pool, 1); assertThat(idledSessions).isEmpty(); @@ -262,9 +262,9 @@ public void testIdleSessions() throws Exception { // Now check out three sessions so the pool will create an additional session. The pool will // only keep 2 sessions alive, as that is the setting for MinSessions. - Session session3 = pool.getReadSession().get(); - Session session4 = pool.getReadSession().get(); - Session session5 = pool.getReadSession().get(); + Session session3 = pool.getSession().get(); + Session session4 = pool.getSession().get(); + Session session5 = pool.getSession().get(); // Note that session2 was now the first session in the pool as it was the last to receive a // ping. assertThat(session3.getName()).isEqualTo(session2.getName()); @@ -279,9 +279,9 @@ public void testIdleSessions() throws Exception { assertThat(pool.totalSessions()).isEqualTo(2); // Check out three sessions again and keep one session checked out. - Session session6 = pool.getReadSession().get(); - Session session7 = pool.getReadSession().get(); - Session session8 = pool.getReadSession().get(); + Session session6 = pool.getSession().get(); + Session session7 = pool.getSession().get(); + Session session8 = pool.getSession().get(); session8.close(); session7.close(); // Now advance the clock to idle sessions. This should remove session8 from the pool. @@ -293,9 +293,9 @@ public void testIdleSessions() throws Exception { // Check out three sessions and keep them all checked out. No sessions should be removed from // the pool. - Session session9 = pool.getReadSession().get(); - Session session10 = pool.getReadSession().get(); - Session session11 = pool.getReadSession().get(); + Session session9 = pool.getSession().get(); + Session session10 = pool.getSession().get(); + Session session11 = pool.getSession().get(); runMaintainanceLoop(clock, pool, loopsToIdleSessions); assertThat(idledSessions).containsExactly(session5, session8); assertThat(pool.totalSessions()).isEqualTo(3); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolStressTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolStressTest.java index b806f5fad6..a3b2a3c542 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolStressTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolStressTest.java @@ -205,15 +205,6 @@ private void expireSession(Session session) { } } - private void assertWritePrepared(Session session) { - String name = session.getName(); - synchronized (lock) { - if (!sessions.containsKey(name) || !sessions.get(name)) { - setFailed(); - } - } - } - private void resetTransaction(SessionImpl session) { String name = session.getName(); synchronized (lock) { @@ -242,7 +233,6 @@ public void stressTest() throws Exception { final int numOperationsPerThread = 1000; final CountDownLatch releaseThreads = new CountDownLatch(1); final CountDownLatch threadsDone = new CountDownLatch(concurrentThreads); - final int writeOperationFraction = 5; setupSpanner(db); int minSessions = 2; int maxSessions = concurrentThreads / 2; @@ -280,15 +270,8 @@ public void run() { Uninterruptibles.awaitUninterruptibly(releaseThreads); for (int j = 0; j < numOperationsPerThread; j++) { try { - PooledSessionFuture session = null; - if (random.nextInt(10) < writeOperationFraction) { - session = pool.getReadWriteSession(); - PooledSession sess = session.get(); - assertWritePrepared(sess); - } else { - session = pool.getReadSession(); - session.get(); - } + PooledSessionFuture session = pool.getSession(); + session.get(); Uninterruptibles.sleepUninterruptibly( random.nextInt(5), TimeUnit.MILLISECONDS); resetTransaction(session.get().delegate); 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 8c7e920f78..0620bfb0e9 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 @@ -54,7 +54,6 @@ import com.google.cloud.spanner.TransactionRunnerImpl.TransactionContextImpl; import com.google.cloud.spanner.spi.v1.SpannerRpc; import com.google.cloud.spanner.spi.v1.SpannerRpc.ResultStreamConsumer; -import com.google.common.base.Stopwatch; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.Uninterruptibles; import com.google.protobuf.ByteString; @@ -180,7 +179,7 @@ public void testClosedPoolIncludesClosedException() { assertThat(pool.isValid()).isTrue(); closePoolWithStacktrace(); try { - pool.getReadSession(); + pool.getSession(); fail("missing expected exception"); } catch (IllegalStateException e) { assertThat(e.getCause()).isInstanceOf(ClosedException.class); @@ -198,7 +197,7 @@ private void closePoolWithStacktrace() { public void sessionCreation() { setupMockSessionCreation(); pool = createPool(); - try (Session session = pool.getReadSession()) { + try (Session session = pool.getSession()) { assertThat(session).isNotNull(); } } @@ -207,25 +206,18 @@ public void sessionCreation() { public void poolLifo() { setupMockSessionCreation(); pool = createPool(); - Session session1 = pool.getReadSession().get(); - Session session2 = pool.getReadSession().get(); + Session session1 = pool.getSession().get(); + Session session2 = pool.getSession().get(); assertThat(session1).isNotEqualTo(session2); session2.close(); session1.close(); - Session session3 = pool.getReadSession().get(); - Session session4 = pool.getReadSession().get(); + Session session3 = pool.getSession().get(); + Session session4 = pool.getSession().get(); assertThat(session3).isEqualTo(session1); assertThat(session4).isEqualTo(session2); session3.close(); session4.close(); - - Session session5 = pool.getReadWriteSession().get(); - Session session6 = pool.getReadWriteSession().get(); - assertThat(session5).isEqualTo(session4); - assertThat(session6).isEqualTo(session3); - session6.close(); - session5.close(); } @Test @@ -260,9 +252,9 @@ public void run() { .when(sessionClient) .asyncBatchCreateSessions(Mockito.eq(1), Mockito.anyBoolean(), any(SessionConsumer.class)); pool = createPool(); - Session session1 = pool.getReadSession(); + Session session1 = pool.getSession(); // Leaked sessions - PooledSessionFuture leakedSession = pool.getReadSession(); + PooledSessionFuture leakedSession = pool.getSession(); // Clear the leaked exception to suppress logging of expected exceptions. leakedSession.clearLeakedException(); session1.close(); @@ -338,7 +330,7 @@ public Void call() throws Exception { .asyncBatchCreateSessions(Mockito.eq(1), Mockito.anyBoolean(), any(SessionConsumer.class)); pool = createPool(); - PooledSessionFuture leakedSession = pool.getReadSession(); + PooledSessionFuture leakedSession = pool.getSession(); // Suppress expected leakedSession warning. leakedSession.clearLeakedException(); AtomicBoolean failed = new AtomicBoolean(false); @@ -396,12 +388,12 @@ public Void call() throws Exception { .asyncBatchCreateSessions(Mockito.eq(1), Mockito.anyBoolean(), any(SessionConsumer.class)); pool = createPool(); - PooledSessionFuture leakedSession = pool.getReadSession(); + PooledSessionFuture leakedSession = pool.getSession(); // Suppress expected leakedSession warning. leakedSession.clearLeakedException(); AtomicBoolean failed = new AtomicBoolean(false); CountDownLatch latch = new CountDownLatch(1); - getReadWriteSessionAsync(latch, failed); + getSessionAsync(latch, failed); insideCreation.await(); pool.closeAsync(new SpannerImpl.ClosedException()); releaseCreation.countDown(); @@ -446,51 +438,6 @@ public Void call() throws Exception { assertThat(f.isDone()).isTrue(); } - @Test - public void poolClosesEvenIfPreparationFails() throws Exception { - final SessionImpl session = mockSession(); - doAnswer( - new Answer() { - @Override - public Void answer(final InvocationOnMock invocation) { - executor.submit( - new Runnable() { - @Override - public void run() { - SessionConsumerImpl consumer = - invocation.getArgumentAt(2, SessionConsumerImpl.class); - consumer.onSessionReady(session); - } - }); - return null; - } - }) - .when(sessionClient) - .asyncBatchCreateSessions(Mockito.eq(1), Mockito.anyBoolean(), any(SessionConsumer.class)); - final CountDownLatch insidePrepare = new CountDownLatch(1); - final CountDownLatch releasePrepare = new CountDownLatch(1); - doAnswer( - new Answer() { - @Override - public Session answer(InvocationOnMock invocation) throws Throwable { - insidePrepare.countDown(); - releasePrepare.await(); - throw SpannerExceptionFactory.newSpannerException(new RuntimeException()); - } - }) - .when(session) - .prepareReadWriteTransaction(); - pool = createPool(); - AtomicBoolean failed = new AtomicBoolean(false); - CountDownLatch latch = new CountDownLatch(1); - getReadWriteSessionAsync(latch, failed); - insidePrepare.await(); - ListenableFuture f = pool.closeAsync(new SpannerImpl.ClosedException()); - releasePrepare.countDown(); - f.get(); - assertThat(f.isDone()).isTrue(); - } - @Test public void poolClosureFailsNewRequests() { final SessionImpl session = mockSession(); @@ -513,13 +460,13 @@ public void run() { .when(sessionClient) .asyncBatchCreateSessions(Mockito.eq(1), Mockito.anyBoolean(), any(SessionConsumer.class)); pool = createPool(); - PooledSessionFuture leakedSession = pool.getReadSession(); + PooledSessionFuture leakedSession = pool.getSession(); leakedSession.get(); // Suppress expected leakedSession warning. leakedSession.clearLeakedException(); pool.closeAsync(new SpannerImpl.ClosedException()); try { - pool.getReadSession(); + pool.getSession(); fail("Expected exception"); } catch (IllegalStateException ex) { assertNotNull(ex.getMessage()); @@ -566,283 +513,13 @@ public Void call() { .asyncBatchCreateSessions(Mockito.eq(1), Mockito.anyBoolean(), any(SessionConsumer.class)); pool = createPool(); try { - pool.getReadSession().get(); - fail("Expected exception"); - } catch (SpannerException ex) { - assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.INTERNAL); - } - } - - @Test - public void creationExceptionPropagatesToReadWriteSession() { - doAnswer( - new Answer() { - @Override - public Void answer(final InvocationOnMock invocation) { - executor.submit( - new Callable() { - @Override - public Void call() { - SessionConsumerImpl consumer = - invocation.getArgumentAt(2, SessionConsumerImpl.class); - consumer.onSessionCreateFailure( - SpannerExceptionFactory.newSpannerException(ErrorCode.INTERNAL, ""), 1); - return null; - } - }); - return null; - } - }) - .when(sessionClient) - .asyncBatchCreateSessions(Mockito.eq(1), Mockito.anyBoolean(), any(SessionConsumer.class)); - pool = createPool(); - try { - pool.getReadWriteSession().get(); + pool.getSession().get(); fail("Expected exception"); } catch (SpannerException ex) { assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.INTERNAL); } } - @Test - public void prepareExceptionPropagatesToReadWriteSession() { - final SessionImpl session = mockSession(); - doAnswer( - new Answer() { - @Override - public Void answer(final InvocationOnMock invocation) { - executor.submit( - new Runnable() { - @Override - public void run() { - SessionConsumerImpl consumer = - invocation.getArgumentAt(2, SessionConsumerImpl.class); - consumer.onSessionReady(session); - } - }); - return null; - } - }) - .when(sessionClient) - .asyncBatchCreateSessions(Mockito.eq(1), Mockito.anyBoolean(), any(SessionConsumer.class)); - doThrow(SpannerExceptionFactory.newSpannerException(ErrorCode.INTERNAL, "")) - .when(session) - .prepareReadWriteTransaction(); - pool = createPool(); - try { - pool.getReadWriteSession().get(); - fail("Expected exception"); - } catch (SpannerException ex) { - assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.INTERNAL); - } - } - - @Test - public void getReadWriteSession() { - final SessionImpl mockSession = mockSession(); - doAnswer( - new Answer() { - @Override - public Void answer(final InvocationOnMock invocation) { - executor.submit( - new Runnable() { - @Override - public void run() { - SessionConsumerImpl consumer = - invocation.getArgumentAt(2, SessionConsumerImpl.class); - consumer.onSessionReady(mockSession); - } - }); - return null; - } - }) - .when(sessionClient) - .asyncBatchCreateSessions(Mockito.eq(1), Mockito.anyBoolean(), any(SessionConsumer.class)); - pool = createPool(); - try (PooledSessionFuture session = pool.getReadWriteSession()) { - assertThat(session).isNotNull(); - session.get(); - verify(mockSession).prepareReadWriteTransaction(); - } - } - - @Test - public void getMultipleReadWriteSessions() throws Exception { - SessionImpl mockSession1 = mockSession(); - SessionImpl mockSession2 = mockSession(); - final LinkedList sessions = - new LinkedList<>(Arrays.asList(mockSession1, mockSession2)); - doAnswer( - new Answer() { - @Override - public Void answer(final InvocationOnMock invocation) { - executor.submit( - new Runnable() { - @Override - public void run() { - SessionConsumerImpl consumer = - invocation.getArgumentAt(2, SessionConsumerImpl.class); - consumer.onSessionReady(sessions.pop()); - } - }); - return null; - } - }) - .when(sessionClient) - .asyncBatchCreateSessions(Mockito.eq(1), Mockito.anyBoolean(), any(SessionConsumer.class)); - pool = createPool(); - PooledSessionFuture session1 = pool.getReadWriteSession(); - PooledSessionFuture session2 = pool.getReadWriteSession(); - session1.get(); - session2.get(); - verify(mockSession1).prepareReadWriteTransaction(); - verify(mockSession2).prepareReadWriteTransaction(); - session1.close(); - session2.close(); - } - - @Test - public void getMultipleConcurrentReadWriteSessions() { - AtomicBoolean failed = new AtomicBoolean(false); - final SessionImpl session = mockSession(); - doAnswer( - new Answer() { - @Override - public Void answer(final InvocationOnMock invocation) { - executor.submit( - new Runnable() { - @Override - public void run() { - SessionConsumerImpl consumer = - invocation.getArgumentAt(2, SessionConsumerImpl.class); - consumer.onSessionReady(session); - } - }); - return null; - } - }) - .when(sessionClient) - .asyncBatchCreateSessions(Mockito.eq(1), Mockito.anyBoolean(), any(SessionConsumer.class)); - - pool = createPool(); - int numSessions = 5; - final CountDownLatch latch = new CountDownLatch(numSessions); - for (int i = 0; i < numSessions; i++) { - getReadWriteSessionAsync(latch, failed); - } - Uninterruptibles.awaitUninterruptibly(latch); - } - - @Test - public void sessionIsPrePrepared() { - final SessionImpl mockSession1 = mockSession(); - final SessionImpl mockSession2 = mockSession(); - final CountDownLatch prepareLatch = new CountDownLatch(1); - doAnswer( - new Answer() { - - @Override - public Void answer(InvocationOnMock arg0) { - prepareLatch.countDown(); - return null; - } - }) - .when(mockSession1) - .prepareReadWriteTransaction(); - doAnswer( - new Answer() { - - @Override - public Void answer(InvocationOnMock arg0) { - prepareLatch.countDown(); - return null; - } - }) - .when(mockSession2) - .prepareReadWriteTransaction(); - doAnswer( - new Answer() { - @Override - public Void answer(final InvocationOnMock invocation) { - executor.submit( - new Runnable() { - @Override - public void run() { - SessionConsumerImpl consumer = - invocation.getArgumentAt(2, SessionConsumerImpl.class); - consumer.onSessionReady(mockSession1); - consumer.onSessionReady(mockSession2); - } - }); - return null; - } - }) - .when(sessionClient) - .asyncBatchCreateSessions(Mockito.eq(2), Mockito.anyBoolean(), any(SessionConsumer.class)); - - options = - SessionPoolOptions.newBuilder() - .setMinSessions(2) - .setMaxSessions(2) - .setWriteSessionsFraction(0.5f) - .build(); - pool = createPool(); - // One of the sessions would be pre prepared. - Uninterruptibles.awaitUninterruptibly(prepareLatch); - PooledSession readSession = pool.getReadSession().get(); - PooledSession writeSession = pool.getReadWriteSession().get(); - verify(writeSession.delegate, times(1)).prepareReadWriteTransaction(); - verify(readSession.delegate, never()).prepareReadWriteTransaction(); - readSession.close(); - writeSession.close(); - } - - @Test - public void getReadSessionFallsBackToWritePreparedSession() throws Exception { - final SessionImpl mockSession1 = mockSession(); - final CountDownLatch prepareLatch = new CountDownLatch(2); - doAnswer( - new Answer() { - @Override - public Void answer(InvocationOnMock arg0) { - prepareLatch.countDown(); - return null; - } - }) - .when(mockSession1) - .prepareReadWriteTransaction(); - doAnswer( - new Answer() { - @Override - public Void answer(final InvocationOnMock invocation) { - executor.submit( - new Runnable() { - @Override - public void run() { - SessionConsumerImpl consumer = - invocation.getArgumentAt(2, SessionConsumerImpl.class); - consumer.onSessionReady(mockSession1); - } - }); - return null; - } - }) - .when(sessionClient) - .asyncBatchCreateSessions(Mockito.eq(1), Mockito.anyBoolean(), any(SessionConsumer.class)); - options = - SessionPoolOptions.newBuilder() - .setMinSessions(minSessions) - .setMaxSessions(1) - .setWriteSessionsFraction(1.0f) - .build(); - pool = createPool(); - pool.getReadWriteSession().close(); - prepareLatch.await(); - // This session should also be write prepared. - PooledSession readSession = pool.getReadSession().get(); - verify(readSession.delegate, times(2)).prepareReadWriteTransaction(); - } - @Test public void failOnPoolExhaustion() { options = @@ -870,50 +547,19 @@ public void run() { .when(sessionClient) .asyncBatchCreateSessions(Mockito.eq(1), Mockito.anyBoolean(), any(SessionConsumer.class)); pool = createPool(); - Session session1 = pool.getReadSession(); + Session session1 = pool.getSession(); try { - pool.getReadSession(); + pool.getSession(); fail("Expected exception"); } catch (SpannerException ex) { assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.RESOURCE_EXHAUSTED); } session1.close(); - session1 = pool.getReadSession(); + session1 = pool.getSession(); assertThat(session1).isNotNull(); session1.close(); } - @Test - public void poolWorksWhenSessionNotFound() { - SessionImpl mockSession1 = mockSession(); - SessionImpl mockSession2 = mockSession(); - final LinkedList sessions = - new LinkedList<>(Arrays.asList(mockSession1, mockSession2)); - doThrow(SpannerExceptionFactoryTest.newSessionNotFoundException(sessionName)) - .when(mockSession1) - .prepareReadWriteTransaction(); - doAnswer( - new Answer() { - @Override - public Void answer(final InvocationOnMock invocation) { - executor.submit( - new Runnable() { - @Override - public void run() { - SessionConsumerImpl consumer = - invocation.getArgumentAt(2, SessionConsumerImpl.class); - consumer.onSessionReady(sessions.pop()); - } - }); - return null; - } - }) - .when(sessionClient) - .asyncBatchCreateSessions(Mockito.eq(1), Mockito.anyBoolean(), any(SessionConsumer.class)); - pool = createPool(); - assertThat(pool.getReadWriteSession().get().delegate).isEqualTo(mockSession2); - } - @Test public void idleSessionCleanup() throws Exception { options = @@ -953,12 +599,12 @@ public void run() { clock.currentTimeMillis = System.currentTimeMillis(); pool = createPool(clock); // Make sure pool has been initialized - pool.getReadSession().close(); + pool.getSession().close(); runMaintainanceLoop(clock, pool, pool.poolMaintainer.numClosureCycles); assertThat(pool.numIdleSessionsRemoved()).isEqualTo(0L); - PooledSessionFuture readSession1 = pool.getReadSession(); - PooledSessionFuture readSession2 = pool.getReadSession(); - PooledSessionFuture readSession3 = pool.getReadSession(); + PooledSessionFuture readSession1 = pool.getSession(); + PooledSessionFuture readSession2 = pool.getSession(); + PooledSessionFuture readSession3 = pool.getSession(); // Wait until the sessions have actually been gotten in order to make sure they are in use in // parallel. readSession1.get(); @@ -973,9 +619,9 @@ public void run() { assertThat(pool.numIdleSessionsRemoved()).isEqualTo(0L); // Counters have now been reset // Use all 3 sessions sequentially - pool.getReadSession().close(); - pool.getReadSession().close(); - pool.getReadSession().close(); + pool.getSession().close(); + pool.getSession().close(); + pool.getSession().close(); // Advance the time by running the maintainer. This should cause // one session to be kept alive and two sessions to be removed. long cycles = @@ -1017,8 +663,8 @@ public void run() { FakeClock clock = new FakeClock(); clock.currentTimeMillis = System.currentTimeMillis(); pool = createPool(clock); - PooledSessionFuture session1 = pool.getReadSession(); - PooledSessionFuture session2 = pool.getReadSession(); + PooledSessionFuture session1 = pool.getSession(); + PooledSessionFuture session2 = pool.getSession(); session1.get(); session2.get(); session1.close(); @@ -1029,7 +675,7 @@ public void run() { verify(session, times(2)).singleUse(any(TimestampBound.class)); clock.currentTimeMillis += clock.currentTimeMillis + (options.getKeepAliveIntervalMinutes() + 5) * 60 * 1000; - session1 = pool.getReadSession(); + session1 = pool.getSession(); session1.writeAtLeastOnce(new ArrayList()); session1.close(); runMaintainanceLoop(clock, pool, pool.poolMaintainer.numKeepAliveCycles); @@ -1040,156 +686,53 @@ public void run() { } @Test - public void testMaintainerKeepsWriteProportion() throws Exception { + public void blockAndTimeoutOnPoolExhaustion() throws Exception { + // Create a session pool with max 1 session and a low timeout for waiting for a session. options = SessionPoolOptions.newBuilder() - .setMinSessions(10) - .setMaxSessions(20) - .setWriteSessionsFraction(0.5f) + .setMinSessions(minSessions) + .setMaxSessions(1) + .setInitialWaitForSessionTimeoutMillis(20L) .build(); - final SessionImpl session = mockSession(); - mockKeepAlive(session); - // This is cheating as we are returning the same session each but it makes the verification - // easier. - doAnswer( - new Answer() { + setupMockSessionCreation(); + pool = createPool(); + // Take the only session that can be in the pool. + PooledSessionFuture checkedOutSession = pool.getSession(); + checkedOutSession.get(); + ExecutorService executor = Executors.newFixedThreadPool(1); + final CountDownLatch latch = new CountDownLatch(1); + // Then try asynchronously to take another session. This attempt should time out. + Future fut = + executor.submit( + new Callable() { @Override - public Void answer(final InvocationOnMock invocation) { - executor.submit( - new Runnable() { - @Override - public void run() { - int sessionCount = invocation.getArgumentAt(0, Integer.class); - SessionConsumerImpl consumer = - invocation.getArgumentAt(2, SessionConsumerImpl.class); - for (int i = 0; i < sessionCount; i++) { - consumer.onSessionReady(session); - } - } - }); + public Void call() { + latch.countDown(); + PooledSessionFuture session = pool.getSession(); + session.close(); return null; } - }) - .when(sessionClient) - .asyncBatchCreateSessions(anyInt(), Mockito.anyBoolean(), any(SessionConsumer.class)); - FakeClock clock = new FakeClock(); - clock.currentTimeMillis = System.currentTimeMillis(); - pool = createPool(clock); - // Wait until all sessions have been created and prepared. - waitForExpectedSessionPool(options.getMinSessions(), options.getWriteSessionsFraction()); - assertThat(pool.getNumberOfSessionsInPool()).isEqualTo(options.getMinSessions()); - assertThat(pool.getNumberOfAvailableWritePreparedSessions()) - .isEqualTo((int) Math.ceil(options.getMinSessions() * options.getWriteSessionsFraction())); - - // Run maintainer numKeepAliveCycles. No pings should be executed during these. - runMaintainanceLoop(clock, pool, pool.poolMaintainer.numKeepAliveCycles); - verify(session, never()).singleUse(any(TimestampBound.class)); - // Run maintainer numKeepAliveCycles again. All sessions should now be pinged. - runMaintainanceLoop(clock, pool, pool.poolMaintainer.numKeepAliveCycles); - verify(session, times(options.getMinSessions())).singleUse(any(TimestampBound.class)); - // Verify that all sessions are still in the pool, and that the write fraction is maintained. - assertThat(pool.getNumberOfSessionsInPool()).isEqualTo(options.getMinSessions()); - assertThat(pool.getNumberOfWriteSessionsInPool()) - .isEqualTo( - (int) Math.ceil(pool.getNumberOfSessionsInPool() * options.getWriteSessionsFraction())); - - // Check out MaxSessions sessions to add additional sessions to the pool. - List sessions = new ArrayList<>(options.getMaxSessions()); - for (int i = 0; i < options.getMaxSessions(); i++) { - sessions.add(pool.getReadSession()); - } - for (Session s : sessions) { - s.close(); - } - // There should be MaxSessions in the pool and the writeFraction should be respected. - waitForExpectedSessionPool(options.getMaxSessions(), options.getWriteSessionsFraction()); - assertThat(pool.getNumberOfSessionsInPool()).isEqualTo(options.getMaxSessions()); - assertThat(pool.getNumberOfAvailableWritePreparedSessions()) - .isEqualTo((int) Math.ceil(options.getMaxSessions() * options.getWriteSessionsFraction())); - - // Advance the clock to allow the sessions to time out or be kept alive. - clock.currentTimeMillis += - clock.currentTimeMillis + (options.getKeepAliveIntervalMinutes() + 5) * 60 * 1000; - runMaintainanceLoop(clock, pool, pool.poolMaintainer.numKeepAliveCycles); - // The session pool only keeps MinSessions alive. - verify(session, times(options.getMinSessions())).singleUse(any(TimestampBound.class)); - // Verify that MinSessions and WriteFraction are respected. - waitForExpectedSessionPool(options.getMinSessions(), options.getWriteSessionsFraction()); - assertThat(pool.getNumberOfSessionsInPool()).isEqualTo(options.getMinSessions()); - assertThat(pool.getNumberOfAvailableWritePreparedSessions()) - .isEqualTo((int) Math.ceil(options.getMinSessions() * options.getWriteSessionsFraction())); - - pool.closeAsync(new SpannerImpl.ClosedException()).get(5L, TimeUnit.SECONDS); - } - - private void waitForExpectedSessionPool(int expectedSessions, float writeFraction) - throws InterruptedException { - Stopwatch watch = Stopwatch.createStarted(); - while ((pool.getNumberOfSessionsInPool() < expectedSessions - || pool.getNumberOfAvailableWritePreparedSessions() - < Math.ceil(expectedSessions * writeFraction)) - && watch.elapsed(TimeUnit.SECONDS) < 5) { - Thread.sleep(1L); + }); + // Wait until the background thread is actually waiting for a session. + latch.await(); + // Wait until the request has timed out. + int waitCount = 0; + while (pool.getNumWaiterTimeouts() == 0L && waitCount < 1000) { + Thread.sleep(5L); + waitCount++; } - } + // Return the checked out session to the pool so the async request will get a session and + // finish. + checkedOutSession.close(); + // Verify that the async request also succeeds. + fut.get(10L, TimeUnit.SECONDS); + executor.shutdown(); - @Test - public void blockAndTimeoutOnPoolExhaustion() throws Exception { - // Try to take a read or a read/write session. These requests should block. - for (Boolean write : new Boolean[] {true, false}) { - // Create a session pool with max 1 session and a low timeout for waiting for a session. - options = - SessionPoolOptions.newBuilder() - .setMinSessions(minSessions) - .setMaxSessions(1) - .setInitialWaitForSessionTimeoutMillis(20L) - .build(); - setupMockSessionCreation(); - pool = createPool(); - // Take the only session that can be in the pool. - PooledSessionFuture checkedOutSession = pool.getReadSession(); - checkedOutSession.get(); - final Boolean finWrite = write; - ExecutorService executor = Executors.newFixedThreadPool(1); - final CountDownLatch latch = new CountDownLatch(1); - // Then try asynchronously to take another session. This attempt should time out. - Future fut = - executor.submit( - new Callable() { - @Override - public Void call() { - PooledSessionFuture session; - latch.countDown(); - if (finWrite) { - session = pool.getReadWriteSession(); - } else { - session = pool.getReadSession(); - } - session.close(); - return null; - } - }); - // Wait until the background thread is actually waiting for a session. - latch.await(); - // Wait until the request has timed out. - int waitCount = 0; - while (pool.getNumWaiterTimeouts() == 0L && waitCount < 1000) { - Thread.sleep(5L); - waitCount++; - } - // Return the checked out session to the pool so the async request will get a session and - // finish. - checkedOutSession.close(); - // Verify that the async request also succeeds. - fut.get(10L, TimeUnit.SECONDS); - executor.shutdown(); - - // Verify that the session was returned to the pool and that we can get it again. - Session session = pool.getReadSession(); - assertThat(session).isNotNull(); - session.close(); - assertThat(pool.getNumWaiterTimeouts()).isAtLeast(1L); - } + // Verify that the session was returned to the pool and that we can get it again. + Session session = pool.getSession(); + assertThat(session).isNotNull(); + session.close(); + assertThat(pool.getNumWaiterTimeouts()).isAtLeast(1L); } @Test @@ -1247,7 +790,7 @@ public void run() { FakeClock clock = new FakeClock(); clock.currentTimeMillis = System.currentTimeMillis(); pool = createPool(clock); - ReadContext context = pool.getReadSession().singleUse(); + ReadContext context = pool.getSession().singleUse(); ResultSet resultSet = context.executeQuery(statement); assertThat(resultSet.next()).isTrue(); } @@ -1303,7 +846,7 @@ public void run() { FakeClock clock = new FakeClock(); clock.currentTimeMillis = System.currentTimeMillis(); pool = createPool(clock); - ReadOnlyTransaction transaction = pool.getReadSession().readOnlyTransaction(); + ReadOnlyTransaction transaction = pool.getSession().readOnlyTransaction(); ResultSet resultSet = transaction.executeQuery(statement); assertThat(resultSet.next()).isTrue(); } @@ -1327,254 +870,171 @@ public void testSessionNotFoundReadWriteTransaction() { for (ReadWriteTransactionTestStatementType statementType : ReadWriteTransactionTestStatementType.values()) { final ReadWriteTransactionTestStatementType executeStatementType = statementType; - for (boolean prepared : new boolean[] {true, false}) { - final boolean hasPreparedTransaction = prepared; - SpannerRpc.StreamingCall closedStreamingCall = mock(SpannerRpc.StreamingCall.class); - doThrow(sessionNotFound).when(closedStreamingCall).request(Mockito.anyInt()); - SpannerRpc rpc = mock(SpannerRpc.class); - when(rpc.asyncDeleteSession(Mockito.anyString(), Mockito.anyMap())) - .thenReturn(ApiFutures.immediateFuture(Empty.getDefaultInstance())); - when(rpc.executeQuery( - any(ExecuteSqlRequest.class), any(ResultStreamConsumer.class), any(Map.class))) - .thenReturn(closedStreamingCall); - when(rpc.executeQuery(any(ExecuteSqlRequest.class), any(Map.class))) - .thenThrow(sessionNotFound); - when(rpc.executeBatchDml(any(ExecuteBatchDmlRequest.class), any(Map.class))) - .thenThrow(sessionNotFound); - when(rpc.commitAsync(any(CommitRequest.class), any(Map.class))) - .thenReturn(ApiFutures.immediateFailedFuture(sessionNotFound)); - when(rpc.rollbackAsync(any(RollbackRequest.class), any(Map.class))) - .thenReturn(ApiFutures.immediateFailedFuture(sessionNotFound)); - final SessionImpl closedSession = mock(SessionImpl.class); - when(closedSession.getName()) - .thenReturn("projects/dummy/instances/dummy/database/dummy/sessions/session-closed"); - ByteString preparedTransactionId = - hasPreparedTransaction ? ByteString.copyFromUtf8("test-txn") : null; - final TransactionContextImpl closedTransactionContext = - TransactionContextImpl.newBuilder() - .setSession(closedSession) - .setTransactionId(preparedTransactionId) - .setRpc(rpc) - .build(); - when(closedSession.asyncClose()) - .thenReturn(ApiFutures.immediateFuture(Empty.getDefaultInstance())); - when(closedSession.newTransaction()).thenReturn(closedTransactionContext); - when(closedSession.beginTransactionAsync()).thenThrow(sessionNotFound); - TransactionRunnerImpl closedTransactionRunner = - new TransactionRunnerImpl(closedSession, rpc, 10, false); - closedTransactionRunner.setSpan(mock(Span.class)); - when(closedSession.readWriteTransaction()).thenReturn(closedTransactionRunner); - - final SessionImpl openSession = mock(SessionImpl.class); - when(openSession.asyncClose()) - .thenReturn(ApiFutures.immediateFuture(Empty.getDefaultInstance())); - when(openSession.getName()) - .thenReturn("projects/dummy/instances/dummy/database/dummy/sessions/session-open"); - final TransactionContextImpl openTransactionContext = mock(TransactionContextImpl.class); - when(openSession.newTransaction()).thenReturn(openTransactionContext); - when(openSession.beginTransactionAsync()) - .thenReturn(ApiFutures.immediateFuture(ByteString.copyFromUtf8("open-txn"))); - TransactionRunnerImpl openTransactionRunner = - new TransactionRunnerImpl(openSession, mock(SpannerRpc.class), 10, false); - openTransactionRunner.setSpan(mock(Span.class)); - when(openSession.readWriteTransaction()).thenReturn(openTransactionRunner); - - ResultSet openResultSet = mock(ResultSet.class); - when(openResultSet.next()).thenReturn(true, false); - ResultSet planResultSet = mock(ResultSet.class); - when(planResultSet.getStats()).thenReturn(ResultSetStats.getDefaultInstance()); - when(openTransactionContext.executeQuery(queryStatement)).thenReturn(openResultSet); - when(openTransactionContext.analyzeQuery(queryStatement, QueryAnalyzeMode.PLAN)) - .thenReturn(planResultSet); - when(openTransactionContext.executeUpdate(updateStatement)).thenReturn(1L); - when(openTransactionContext.batchUpdate(Arrays.asList(updateStatement, updateStatement))) - .thenReturn(new long[] {1L, 1L}); - SpannerImpl spanner = mock(SpannerImpl.class); - SessionClient sessionClient = mock(SessionClient.class); - when(spanner.getSessionClient(db)).thenReturn(sessionClient); - - doAnswer( - new Answer() { - @Override - public Void answer(final InvocationOnMock invocation) { - executor.submit( - new Runnable() { - @Override - public void run() { - SessionConsumerImpl consumer = - invocation.getArgumentAt(2, SessionConsumerImpl.class); - consumer.onSessionReady(closedSession); - } - }); - return null; - } - }) - .doAnswer( - new Answer() { - @Override - public Void answer(final InvocationOnMock invocation) { - executor.submit( - new Runnable() { - @Override - public void run() { - SessionConsumerImpl consumer = - invocation.getArgumentAt(2, SessionConsumerImpl.class); - consumer.onSessionReady(openSession); - } - }); - return null; + SpannerRpc.StreamingCall closedStreamingCall = mock(SpannerRpc.StreamingCall.class); + doThrow(sessionNotFound).when(closedStreamingCall).request(Mockito.anyInt()); + SpannerRpc rpc = mock(SpannerRpc.class); + when(rpc.asyncDeleteSession(Mockito.anyString(), Mockito.anyMap())) + .thenReturn(ApiFutures.immediateFuture(Empty.getDefaultInstance())); + when(rpc.executeQuery( + any(ExecuteSqlRequest.class), any(ResultStreamConsumer.class), any(Map.class))) + .thenReturn(closedStreamingCall); + when(rpc.executeQuery(any(ExecuteSqlRequest.class), any(Map.class))) + .thenThrow(sessionNotFound); + when(rpc.executeBatchDml(any(ExecuteBatchDmlRequest.class), any(Map.class))) + .thenThrow(sessionNotFound); + when(rpc.commitAsync(any(CommitRequest.class), any(Map.class))) + .thenReturn(ApiFutures.immediateFailedFuture(sessionNotFound)); + when(rpc.rollbackAsync(any(RollbackRequest.class), any(Map.class))) + .thenReturn(ApiFutures.immediateFailedFuture(sessionNotFound)); + final SessionImpl closedSession = mock(SessionImpl.class); + when(closedSession.getName()) + .thenReturn("projects/dummy/instances/dummy/database/dummy/sessions/session-closed"); + final TransactionContextImpl closedTransactionContext = + TransactionContextImpl.newBuilder().setSession(closedSession).setRpc(rpc).build(); + when(closedSession.asyncClose()) + .thenReturn(ApiFutures.immediateFuture(Empty.getDefaultInstance())); + when(closedSession.newTransaction()).thenReturn(closedTransactionContext); + when(closedSession.beginTransactionAsync()).thenThrow(sessionNotFound); + TransactionRunnerImpl closedTransactionRunner = + new TransactionRunnerImpl(closedSession, rpc, 10); + closedTransactionRunner.setSpan(mock(Span.class)); + when(closedSession.readWriteTransaction()).thenReturn(closedTransactionRunner); + + final SessionImpl openSession = mock(SessionImpl.class); + when(openSession.asyncClose()) + .thenReturn(ApiFutures.immediateFuture(Empty.getDefaultInstance())); + when(openSession.getName()) + .thenReturn("projects/dummy/instances/dummy/database/dummy/sessions/session-open"); + final TransactionContextImpl openTransactionContext = mock(TransactionContextImpl.class); + when(openSession.newTransaction()).thenReturn(openTransactionContext); + when(openSession.beginTransactionAsync()) + .thenReturn(ApiFutures.immediateFuture(ByteString.copyFromUtf8("open-txn"))); + TransactionRunnerImpl openTransactionRunner = + new TransactionRunnerImpl(openSession, mock(SpannerRpc.class), 10); + openTransactionRunner.setSpan(mock(Span.class)); + when(openSession.readWriteTransaction()).thenReturn(openTransactionRunner); + + ResultSet openResultSet = mock(ResultSet.class); + when(openResultSet.next()).thenReturn(true, false); + ResultSet planResultSet = mock(ResultSet.class); + when(planResultSet.getStats()).thenReturn(ResultSetStats.getDefaultInstance()); + when(openTransactionContext.executeQuery(queryStatement)).thenReturn(openResultSet); + when(openTransactionContext.analyzeQuery(queryStatement, QueryAnalyzeMode.PLAN)) + .thenReturn(planResultSet); + when(openTransactionContext.executeUpdate(updateStatement)).thenReturn(1L); + when(openTransactionContext.batchUpdate(Arrays.asList(updateStatement, updateStatement))) + .thenReturn(new long[] {1L, 1L}); + SpannerImpl spanner = mock(SpannerImpl.class); + SessionClient sessionClient = mock(SessionClient.class); + when(spanner.getSessionClient(db)).thenReturn(sessionClient); + + doAnswer( + new Answer() { + @Override + public Void answer(final InvocationOnMock invocation) { + executor.submit( + new Runnable() { + @Override + public void run() { + SessionConsumerImpl consumer = + invocation.getArgumentAt(2, SessionConsumerImpl.class); + consumer.onSessionReady(closedSession); + } + }); + return null; + } + }) + .doAnswer( + new Answer() { + @Override + public Void answer(final InvocationOnMock invocation) { + executor.submit( + new Runnable() { + @Override + public void run() { + SessionConsumerImpl consumer = + invocation.getArgumentAt(2, SessionConsumerImpl.class); + consumer.onSessionReady(openSession); + } + }); + return null; + } + }) + .when(sessionClient) + .asyncBatchCreateSessions( + Mockito.eq(1), Mockito.anyBoolean(), any(SessionConsumer.class)); + SessionPoolOptions options = + SessionPoolOptions.newBuilder() + .setMinSessions(0) // The pool should not auto-create any sessions + .setMaxSessions(2) + .setIncStep(1) + .setBlockIfPoolExhausted() + .build(); + SpannerOptions spannerOptions = mock(SpannerOptions.class); + when(spannerOptions.getSessionPoolOptions()).thenReturn(options); + when(spannerOptions.getNumChannels()).thenReturn(4); + when(spanner.getOptions()).thenReturn(spannerOptions); + SessionPool pool = + SessionPool.createPool(options, new TestExecutorFactory(), spanner.getSessionClient(db)); + try (PooledSessionFuture readWriteSession = pool.getSession()) { + TransactionRunner runner = readWriteSession.readWriteTransaction(); + try { + runner.run( + new TransactionCallable() { + private int callNumber = 0; + + @Override + public Integer run(TransactionContext transaction) { + callNumber++; + if (callNumber == 1) { + assertThat(transaction).isEqualTo(closedTransactionContext); + } else { + assertThat(transaction).isEqualTo(openTransactionContext); } - }) - .when(sessionClient) - .asyncBatchCreateSessions( - Mockito.eq(1), Mockito.anyBoolean(), any(SessionConsumer.class)); - SessionPoolOptions options = - SessionPoolOptions.newBuilder() - .setMinSessions(0) // The pool should not auto-create any sessions - .setMaxSessions(2) - .setIncStep(1) - .setBlockIfPoolExhausted() - .build(); - SpannerOptions spannerOptions = mock(SpannerOptions.class); - when(spannerOptions.getSessionPoolOptions()).thenReturn(options); - when(spannerOptions.getNumChannels()).thenReturn(4); - when(spanner.getOptions()).thenReturn(spannerOptions); - SessionPool pool = - SessionPool.createPool( - options, new TestExecutorFactory(), spanner.getSessionClient(db)); - try (PooledSessionFuture readWriteSession = pool.getReadWriteSession()) { - TransactionRunner runner = readWriteSession.readWriteTransaction(); - try { - runner.run( - new TransactionCallable() { - private int callNumber = 0; - - @Override - public Integer run(TransactionContext transaction) { - callNumber++; - if (hasPreparedTransaction) { - // If the session had a prepared read/write transaction, that transaction will - // be given to the runner in the first place and the SessionNotFoundException - // will occur on the first query / update statement. - if (callNumber == 1) { - assertThat(transaction).isEqualTo(closedTransactionContext); - } else { - assertThat(transaction).isEqualTo(openTransactionContext); - } - } else { - // If the session did not have a prepared read/write transaction, the library - // tried to create a new transaction before handing it to the transaction - // runner. The creation of the new transaction failed with a - // SessionNotFoundException, and the session was re-created before the run - // method was called. - assertThat(transaction).isEqualTo(openTransactionContext); - } - switch (executeStatementType) { - case QUERY: - ResultSet resultSet = transaction.executeQuery(queryStatement); - assertThat(resultSet.next()).isTrue(); - break; - case ANALYZE: - ResultSet planResultSet = - transaction.analyzeQuery(queryStatement, QueryAnalyzeMode.PLAN); - assertThat(planResultSet.next()).isFalse(); - assertThat(planResultSet.getStats()).isNotNull(); - break; - case UPDATE: - long updateCount = transaction.executeUpdate(updateStatement); - assertThat(updateCount).isEqualTo(1L); - break; - case BATCH_UPDATE: - long[] updateCounts = - transaction.batchUpdate( - Arrays.asList(updateStatement, updateStatement)); - assertThat(updateCounts).isEqualTo(new long[] {1L, 1L}); - break; - case WRITE: - transaction.buffer(Mutation.delete("FOO", Key.of(1L))); - break; - case EXCEPTION: - throw new RuntimeException("rollback at call " + callNumber); - default: - fail("Unknown statement type: " + executeStatementType); - } - return callNumber; + switch (executeStatementType) { + case QUERY: + ResultSet resultSet = transaction.executeQuery(queryStatement); + assertThat(resultSet.next()).isTrue(); + break; + case ANALYZE: + ResultSet planResultSet = + transaction.analyzeQuery(queryStatement, QueryAnalyzeMode.PLAN); + assertThat(planResultSet.next()).isFalse(); + assertThat(planResultSet.getStats()).isNotNull(); + break; + case UPDATE: + long updateCount = transaction.executeUpdate(updateStatement); + assertThat(updateCount).isEqualTo(1L); + break; + case BATCH_UPDATE: + long[] updateCounts = + transaction.batchUpdate(Arrays.asList(updateStatement, updateStatement)); + assertThat(updateCounts).isEqualTo(new long[] {1L, 1L}); + break; + case WRITE: + transaction.buffer(Mutation.delete("FOO", Key.of(1L))); + break; + case EXCEPTION: + throw new RuntimeException("rollback at call " + callNumber); + default: + fail("Unknown statement type: " + executeStatementType); } - }); - } catch (Exception e) { - // The rollback will also cause a SessionNotFoundException, but this is caught, logged - // and further ignored by the library, meaning that the session will not be re-created - // for retry. Hence rollback at call 1. - assertThat(executeStatementType) - .isEqualTo(ReadWriteTransactionTestStatementType.EXCEPTION); - assertThat(e.getMessage()).contains("rollback at call 1"); - - // assertThat( - // executeStatementType == - // ReadWriteTransactionTestStatementType.EXCEPTION - // && e.getMessage().contains("rollback at call 1")) - // .isTrue(); - } + return callNumber; + } + }); + } catch (Exception e) { + // The rollback will also cause a SessionNotFoundException, but this is caught, logged + // and further ignored by the library, meaning that the session will not be re-created + // for retry. Hence rollback at call 1. + assertThat(executeStatementType) + .isEqualTo(ReadWriteTransactionTestStatementType.EXCEPTION); + assertThat(e.getMessage()).contains("rollback at call 1"); } - pool.closeAsync(new SpannerImpl.ClosedException()); } + pool.closeAsync(new SpannerImpl.ClosedException()); } } - @Test - public void testSessionNotFoundOnPrepareTransaction() { - final SpannerException sessionNotFound = - SpannerExceptionFactoryTest.newSessionNotFoundException(sessionName); - final SessionImpl closedSession = mock(SessionImpl.class); - when(closedSession.getName()) - .thenReturn("projects/dummy/instances/dummy/database/dummy/sessions/session-closed"); - when(closedSession.beginTransaction()).thenThrow(sessionNotFound); - doThrow(sessionNotFound).when(closedSession).prepareReadWriteTransaction(); - - final SessionImpl openSession = mock(SessionImpl.class); - when(openSession.getName()) - .thenReturn("projects/dummy/instances/dummy/database/dummy/sessions/session-open"); - doAnswer( - new Answer() { - @Override - public Void answer(final InvocationOnMock invocation) { - executor.submit( - new Runnable() { - @Override - public void run() { - SessionConsumerImpl consumer = - invocation.getArgumentAt(2, SessionConsumerImpl.class); - consumer.onSessionReady(closedSession); - } - }); - return null; - } - }) - .doAnswer( - new Answer() { - @Override - public Void answer(final InvocationOnMock invocation) { - executor.submit( - new Runnable() { - @Override - public void run() { - SessionConsumerImpl consumer = - invocation.getArgumentAt(2, SessionConsumerImpl.class); - consumer.onSessionReady(openSession); - } - }); - return null; - } - }) - .when(sessionClient) - .asyncBatchCreateSessions(Mockito.eq(1), Mockito.anyBoolean(), any(SessionConsumer.class)); - FakeClock clock = new FakeClock(); - clock.currentTimeMillis = System.currentTimeMillis(); - pool = createPool(clock); - PooledSession session = pool.getReadWriteSession().get(); - assertThat(session.delegate).isEqualTo(openSession); - } - @Test public void testSessionNotFoundWrite() { SpannerException sessionNotFound = @@ -1623,7 +1083,7 @@ public void run() { FakeClock clock = new FakeClock(); clock.currentTimeMillis = System.currentTimeMillis(); pool = createPool(clock); - DatabaseClientImpl impl = new DatabaseClientImpl(pool, false); + DatabaseClientImpl impl = new DatabaseClientImpl(pool); assertThat(impl.write(mutations)).isNotNull(); } @@ -1674,7 +1134,7 @@ public void run() { FakeClock clock = new FakeClock(); clock.currentTimeMillis = System.currentTimeMillis(); pool = createPool(clock); - DatabaseClientImpl impl = new DatabaseClientImpl(pool, false); + DatabaseClientImpl impl = new DatabaseClientImpl(pool); assertThat(impl.writeAtLeastOnce(mutations)).isNotNull(); } @@ -1725,7 +1185,7 @@ public void run() { FakeClock clock = new FakeClock(); clock.currentTimeMillis = System.currentTimeMillis(); pool = createPool(clock); - DatabaseClientImpl impl = new DatabaseClientImpl(pool, false); + DatabaseClientImpl impl = new DatabaseClientImpl(pool); assertThat(impl.executePartitionedUpdate(statement)).isEqualTo(1L); } @@ -1751,8 +1211,8 @@ public void testSessionMetrics() throws Exception { setupMockSessionCreation(); pool = createPool(clock, metricRegistry, labelValues); - PooledSessionFuture session1 = pool.getReadSession(); - PooledSessionFuture session2 = pool.getReadSession(); + PooledSessionFuture session1 = pool.getSession(); + PooledSessionFuture session2 = pool.getSession(); session1.get(); session2.get(); @@ -1830,7 +1290,7 @@ public void testSessionMetrics() throws Exception { @Override public Void call() { latch.countDown(); - Session session = pool.getReadSession(); + Session session = pool.getSession(); session.close(); return null; } @@ -1888,7 +1348,7 @@ private void getSessionAsync(final CountDownLatch latch, final AtomicBoolean fai new Runnable() { @Override public void run() { - try (PooledSessionFuture future = pool.getReadSession()) { + try (PooledSessionFuture future = pool.getSession()) { PooledSession session = future.get(); failed.compareAndSet(false, session == null); Uninterruptibles.sleepUninterruptibly(10, TimeUnit.MILLISECONDS); @@ -1901,23 +1361,4 @@ public void run() { }) .start(); } - - private void getReadWriteSessionAsync(final CountDownLatch latch, final AtomicBoolean failed) { - new Thread( - new Runnable() { - @Override - public void run() { - try (PooledSessionFuture future = pool.getReadWriteSession()) { - PooledSession session = future.get(); - failed.compareAndSet(false, session == null); - Uninterruptibles.sleepUninterruptibly(2, TimeUnit.MILLISECONDS); - } catch (SpannerException e) { - failed.compareAndSet(false, true); - } finally { - latch.countDown(); - } - } - }) - .start(); - } } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpanTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpanTest.java index 7dcc9b65e1..75552c52e1 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpanTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpanTest.java @@ -96,22 +96,6 @@ public class SpanTest { private static final SimulatedExecutionTime ONE_SECOND = SimulatedExecutionTime.ofMinimumAndRandomTime(1000, 0); - private static final Statement SELECT1AND2 = - Statement.of("SELECT 1 AS COL1 UNION ALL SELECT 2 AS COL1"); - private static final ResultSetMetadata SELECT1AND2_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 StatusRuntimeException FAILED_PRECONDITION = io.grpc.Status.FAILED_PRECONDITION .withDescription("Non-retryable test exception.") @@ -162,11 +146,7 @@ public void setUp() throws Exception { .setProjectId(TEST_PROJECT) .setChannelProvider(channelProvider) .setCredentials(NoCredentials.getInstance()) - .setSessionPoolOption( - SessionPoolOptions.newBuilder() - .setMinSessions(0) - .setWriteSessionsFraction(0.0f) - .build()); + .setSessionPoolOption(SessionPoolOptions.newBuilder().setMinSessions(0).build()); spanner = builder.build().getService(); @@ -227,7 +207,7 @@ public void tearDown() { @Test public void singleUseNonRetryableErrorOnNext() { - try (ResultSet rs = client.singleUse().executeQuery(SELECT1AND2)) { + try (ResultSet rs = client.singleUse().executeQuery(SELECT1)) { mockSpanner.addException(FAILED_PRECONDITION); while (rs.next()) { // Just consume the result set. @@ -241,7 +221,7 @@ public void singleUseNonRetryableErrorOnNext() { @Test public void singleUseExecuteStreamingSqlTimeout() { - try (ResultSet rs = clientWithTimeout.singleUse().executeQuery(SELECT1AND2)) { + try (ResultSet rs = clientWithTimeout.singleUse().executeQuery(SELECT1)) { mockSpanner.setExecuteStreamingSqlExecutionTime(ONE_SECOND); while (rs.next()) { // Just consume the result set. @@ -302,7 +282,6 @@ public Void run(TransactionContext transaction) { assertThat(spans).containsEntry("CloudSpannerOperation.BatchCreateSessions", true); assertThat(spans).containsEntry("SessionPool.WaitForSession", true); assertThat(spans).containsEntry("CloudSpannerOperation.BatchCreateSessionsRequest", true); - assertThat(spans).containsEntry("CloudSpannerOperation.BeginTransaction", true); assertThat(spans).containsEntry("CloudSpannerOperation.Commit", true); } @@ -324,11 +303,10 @@ public Void run(TransactionContext transaction) { } Map spans = failOnOverkillTraceComponent.getSpans(); - assertThat(spans.size()).isEqualTo(5); + assertThat(spans.size()).isEqualTo(4); assertThat(spans).containsEntry("CloudSpanner.ReadWriteTransaction", true); assertThat(spans).containsEntry("CloudSpannerOperation.BatchCreateSessions", true); assertThat(spans).containsEntry("SessionPool.WaitForSession", true); assertThat(spans).containsEntry("CloudSpannerOperation.BatchCreateSessionsRequest", true); - assertThat(spans).containsEntry("CloudSpannerOperation.BeginTransaction", true); } } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerGaxRetryTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerGaxRetryTest.java index b98702f87c..cda4cf5f8f 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerGaxRetryTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerGaxRetryTest.java @@ -360,7 +360,7 @@ public void readWriteTransactionStatementAborted() { @Override public Long run(TransactionContext transaction) { if (attempts.getAndIncrement() == 0) { - mockSpanner.abortTransaction(transaction); + mockSpanner.abortNextStatement(); } return transaction.executeUpdate(UPDATE_STATEMENT); } @@ -418,7 +418,7 @@ public Long run(TransactionContext transaction) { @SuppressWarnings("resource") @Test public void transactionManagerTimeout() { - mockSpanner.setBeginTransactionExecutionTime(ONE_SECOND); + mockSpanner.setExecuteSqlExecutionTime(ONE_SECOND); try (TransactionManager txManager = clientWithTimeout.transactionManager()) { TransactionContext tx = txManager.begin(); while (true) { diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerAbortedTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerAbortedTest.java index dec674bd6c..0291e67868 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerAbortedTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerAbortedTest.java @@ -194,7 +194,7 @@ public void testTransactionManagerAbortOnCommit() throws InterruptedException { attempts++; try { if (attempts == 1) { - mockSpanner.abortAllTransactions(); + mockSpanner.abortNextTransaction(); } manager.commit(); break; @@ -219,7 +219,7 @@ public void testTransactionManagerAbortOnUpdate() throws InterruptedException { attempts++; try { if (attempts == 1) { - mockSpanner.abortAllTransactions(); + mockSpanner.abortNextTransaction(); } long updateCount = txn.executeUpdate(UPDATE_STATEMENT); assertThat(updateCount, is(equalTo(UPDATE_COUNT))); @@ -246,7 +246,7 @@ public void testTransactionManagerAbortOnBatchUpdate() throws InterruptedExcepti attempts++; try { if (attempts == 1) { - mockSpanner.abortAllTransactions(); + mockSpanner.abortNextTransaction(); } long[] updateCounts = txn.batchUpdate(Arrays.asList(UPDATE_STATEMENT, UPDATE_STATEMENT)); assertThat(updateCounts, is(equalTo(new long[] {UPDATE_COUNT, UPDATE_COUNT}))); @@ -301,7 +301,7 @@ public void testTransactionManagerAbortOnSelect() throws InterruptedException { attempts++; try { if (attempts == 1) { - mockSpanner.abortAllTransactions(); + mockSpanner.abortNextTransaction(); } try (ResultSet rs = txn.executeQuery(SELECT1AND2)) { int rows = 0; @@ -333,7 +333,7 @@ public void testTransactionManagerAbortOnRead() throws InterruptedException { attempts++; try { if (attempts == 1) { - mockSpanner.abortAllTransactions(); + mockSpanner.abortNextTransaction(); } try (ResultSet rs = txn.read("FOO", KeySet.all(), Arrays.asList("BAR"))) { int rows = 0; @@ -365,7 +365,7 @@ public void testTransactionManagerAbortOnReadUsingIndex() throws InterruptedExce attempts++; try { if (attempts == 1) { - mockSpanner.abortAllTransactions(); + mockSpanner.abortNextTransaction(); } try (ResultSet rs = txn.readUsingIndex("FOO", "INDEX", KeySet.all(), Arrays.asList("BAR"))) { @@ -398,7 +398,7 @@ public void testTransactionManagerAbortOnReadRow() throws InterruptedException { attempts++; try { if (attempts == 1) { - mockSpanner.abortAllTransactions(); + mockSpanner.abortNextTransaction(); } Struct row = txn.readRow("FOO", Key.of(), Arrays.asList("BAR")); assertThat(row.getLong(0), is(equalTo(1L))); @@ -425,7 +425,7 @@ public void testTransactionManagerAbortOnReadRowUsingIndex() throws InterruptedE attempts++; try { if (attempts == 1) { - mockSpanner.abortAllTransactions(); + mockSpanner.abortNextTransaction(); } Struct row = txn.readRowUsingIndex("FOO", "INDEX", Key.of(), Arrays.asList("BAR")); assertThat(row.getLong(0), is(equalTo(1L))); 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 340d24c55e..149002531a 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 @@ -82,7 +82,7 @@ public void release(ScheduledExecutorService exec) { @Before public void setUp() { initMocks(this); - manager = new TransactionManagerImpl(session, mock(Span.class), false); + manager = new TransactionManagerImpl(session, mock(Span.class)); } @Test @@ -301,7 +301,6 @@ public void inlineBegin() { SessionPoolOptions sessionPoolOptions = SessionPoolOptions.newBuilder().setMinSessions(0).setIncStep(1).build(); when(options.getSessionPoolOptions()).thenReturn(sessionPoolOptions); - when(options.isInlineBeginForReadWriteTransaction()).thenReturn(true); when(options.getSessionLabels()).thenReturn(Collections.emptyMap()); when(options.getDefaultQueryOptions(Mockito.any(DatabaseId.class))) .thenReturn(QueryOptions.getDefaultInstance()); 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 8b66ecb959..71a34950bb 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 @@ -20,6 +20,7 @@ import static org.junit.Assert.fail; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -122,7 +123,7 @@ public ResultSet answer(InvocationOnMock invocation) throws Throwable { return builder.build(); } }); - transactionRunner = new TransactionRunnerImpl(session, rpc, 1, false); + transactionRunner = new TransactionRunnerImpl(session, rpc, 1); when(rpc.commitAsync(Mockito.any(CommitRequest.class), Mockito.anyMap())) .thenReturn( ApiFutures.immediateFuture( @@ -216,7 +217,7 @@ public Void run(TransactionContext transaction) { } }); assertThat(numCalls.get()).isEqualTo(1); - verify(txn).ensureTxn(); + verify(txn, never()).ensureTxn(); verify(txn).commit(); } @@ -224,7 +225,7 @@ public Void run(TransactionContext transaction) { public void runAbort() { when(txn.isAborted()).thenReturn(true); runTransaction(abortedWithRetryInfo()); - verify(txn, times(2)).ensureTxn(); + verify(txn).ensureTxn(); } @Test @@ -242,7 +243,8 @@ public Void run(TransactionContext transaction) { } }); assertThat(numCalls.get()).isEqualTo(2); - verify(txn, times(2)).ensureTxn(); + // ensureTxn() is only called during retry. + verify(txn).ensureTxn(); } @Test @@ -266,7 +268,7 @@ public Void run(TransactionContext transaction) { assertThat(e.getErrorCode()).isEqualTo(ErrorCode.UNKNOWN); } assertThat(numCalls.get()).isEqualTo(1); - verify(txn, times(1)).ensureTxn(); + verify(txn, never()).ensureTxn(); verify(txn, times(1)).commit(); } @@ -320,9 +322,7 @@ public void prepareReadWriteTransaction() { } }; session.setCurrentSpan(mock(Span.class)); - // Create a transaction runner that will inline the BeginTransaction call with the first - // statement. - TransactionRunnerImpl runner = new TransactionRunnerImpl(session, rpc, 10, true); + TransactionRunnerImpl runner = new TransactionRunnerImpl(session, rpc, 10); runner.setSpan(mock(Span.class)); assertThat(usedInlinedBegin).isFalse(); runner.run( @@ -354,7 +354,7 @@ private long[] batchDmlException(int status) { .thenReturn( ApiFutures.immediateFuture(ByteString.copyFromUtf8(UUID.randomUUID().toString()))); when(session.getName()).thenReturn(SessionId.of("p", "i", "d", "test").getName()); - TransactionRunnerImpl runner = new TransactionRunnerImpl(session, rpc, 10, false); + TransactionRunnerImpl runner = new TransactionRunnerImpl(session, rpc, 10); runner.setSpan(mock(Span.class)); ExecuteBatchDmlResponse response1 = ExecuteBatchDmlResponse.newBuilder() diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITClosedSessionTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITClosedSessionTest.java index 1e00015cdf..22dc4c5c45 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITClosedSessionTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITClosedSessionTest.java @@ -245,21 +245,21 @@ public void testTransactionManager() throws InterruptedException { for (int run = 0; run < 2; run++) { try (TransactionManager manager = client.transactionManager()) { TransactionContext txn = manager.begin(); - while (true) { - for (int i = 0; i < 2; i++) { - try (ResultSet rs = txn.executeQuery(Statement.of("SELECT 1"))) { - assertThat(rs.next()).isTrue(); - assertThat(rs.getLong(0)).isEqualTo(1L); - assertThat(rs.next()).isFalse(); + try { + while (true) { + for (int i = 0; i < 2; i++) { + try (ResultSet rs = txn.executeQuery(Statement.of("SELECT 1"))) { + assertThat(rs.next()).isTrue(); + assertThat(rs.getLong(0)).isEqualTo(1L); + assertThat(rs.next()).isFalse(); + } } - } - try { manager.commit(); break; - } catch (AbortedException e) { - Thread.sleep(e.getRetryDelayInMillis() / 1000); - txn = manager.resetForRetry(); } + } catch (AbortedException e) { + Thread.sleep(e.getRetryDelayInMillis() / 1000); + txn = manager.resetForRetry(); } } } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDMLTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDMLTest.java index b8a4ae81ae..915efa604e 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDMLTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDMLTest.java @@ -30,7 +30,6 @@ import com.google.cloud.spanner.Mutation; import com.google.cloud.spanner.ParallelIntegrationTest; import com.google.cloud.spanner.ResultSet; -import com.google.cloud.spanner.Spanner; import com.google.cloud.spanner.SpannerException; import com.google.cloud.spanner.SpannerExceptionFactory; import com.google.cloud.spanner.Statement; @@ -39,24 +38,21 @@ import com.google.cloud.spanner.TransactionRunner; import com.google.cloud.spanner.TransactionRunner.TransactionCallable; import java.util.Arrays; -import java.util.Collection; -import org.junit.After; import org.junit.Before; import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Test; import org.junit.experimental.categories.Category; import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameter; -import org.junit.runners.Parameterized.Parameters; +import org.junit.runners.JUnit4; /** Integration tests for DML. */ @Category(ParallelIntegrationTest.class) -@RunWith(Parameterized.class) +@RunWith(JUnit4.class) public final class ITDMLTest { @ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv(); private static Database db; + private static DatabaseClient client; /** Sequence for assigning unique keys to test cases. */ private static int seq; @@ -71,15 +67,6 @@ public final class ITDMLTest { private static boolean throwAbortOnce = false; - @Parameters(name = "InlineBeginTx = {0}") - public static Collection data() { - return Arrays.asList(new Object[][] {{false}, {true}}); - } - - @Parameter public boolean inlineBeginTx; - private Spanner spanner; - private DatabaseClient client; - @BeforeClass public static void setUpDatabase() { db = @@ -89,27 +76,15 @@ public static void setUpDatabase() { + " K STRING(MAX) NOT NULL," + " V INT64," + ") PRIMARY KEY (K)"); + client = env.getTestHelper().getDatabaseClient(db); } @Before - public void setupClient() { - spanner = - env.getTestHelper() - .getOptions() - .toBuilder() - .setInlineBeginForReadWriteTransaction(inlineBeginTx) - .build() - .getService(); - client = spanner.getDatabaseClient(db.getId()); + public void increaseTestIdAndDeleteTestData() { client.writeAtLeastOnce(Arrays.asList(Mutation.delete("T", KeySet.all()))); id++; } - @After - public void teardownClient() { - spanner.close(); - } - private static String uniqueKey() { return "k" + seq++; } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionManagerAsyncTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionManagerAsyncTest.java index 684803ae54..7b4f340f0b 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionManagerAsyncTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionManagerAsyncTest.java @@ -64,19 +64,13 @@ public class ITTransactionManagerAsyncTest { @Parameter(0) public Executor executor; - @Parameter(1) - public boolean inlineBegin; - - @Parameters(name = "executor = {0}, inlineBegin = {1}") + @Parameters(name = "executor = {0}") public static Collection data() { return Arrays.asList( new Object[][] { - {MoreExecutors.directExecutor(), false}, - {MoreExecutors.directExecutor(), true}, - {Executors.newSingleThreadExecutor(), false}, - {Executors.newSingleThreadExecutor(), true}, - {Executors.newFixedThreadPool(4), false}, - {Executors.newFixedThreadPool(4), true} + {MoreExecutors.directExecutor()}, + {Executors.newSingleThreadExecutor()}, + {Executors.newFixedThreadPool(4)}, }); } @@ -99,13 +93,7 @@ public static void setUpDatabase() { @Before public void clearTable() { - spanner = - env.getTestHelper() - .getOptions() - .toBuilder() - .setInlineBeginForReadWriteTransaction(inlineBegin) - .build() - .getService(); + spanner = env.getTestHelper().getClient(); client = spanner.getDatabaseClient(db.getId()); client.write(ImmutableList.of(Mutation.delete("T", KeySet.all()))); } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionManagerTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionManagerTest.java index 0898547ad1..4d65af67ed 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionManagerTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionManagerTest.java @@ -30,7 +30,6 @@ import com.google.cloud.spanner.KeySet; import com.google.cloud.spanner.Mutation; import com.google.cloud.spanner.ParallelIntegrationTest; -import com.google.cloud.spanner.Spanner; import com.google.cloud.spanner.SpannerException; import com.google.cloud.spanner.Struct; import com.google.cloud.spanner.TransactionContext; @@ -38,34 +37,21 @@ import com.google.cloud.spanner.TransactionManager.TransactionState; import com.google.common.collect.ImmutableList; import java.util.Arrays; -import java.util.Collection; -import org.junit.After; import org.junit.Before; import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Test; import org.junit.experimental.categories.Category; import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameter; -import org.junit.runners.Parameterized.Parameters; +import org.junit.runners.JUnit4; @Category(ParallelIntegrationTest.class) -@RunWith(Parameterized.class) +@RunWith(JUnit4.class) public class ITTransactionManagerTest { - @Parameter(0) - public boolean inlineBegin; - - @Parameters(name = "inlineBegin = {0}") - public static Collection data() { - return Arrays.asList(new Object[][] {{false}, {true}}); - } - @ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv(); private static Database db; - private Spanner spanner; - private DatabaseClient client; + private static DatabaseClient client; @BeforeClass public static void setUpDatabase() { @@ -77,26 +63,14 @@ public static void setUpDatabase() { + " K STRING(MAX) NOT NULL," + " BoolValue BOOL," + ") PRIMARY KEY (K)"); + client = env.getTestHelper().getDatabaseClient(db); } @Before - public void setupClient() { - spanner = - env.getTestHelper() - .getOptions() - .toBuilder() - .setInlineBeginForReadWriteTransaction(inlineBegin) - .build() - .getService(); - client = spanner.getDatabaseClient(db.getId()); + public void deleteTestData() { client.write(ImmutableList.of(Mutation.delete("T", KeySet.all()))); } - @After - public void closeClient() { - spanner.close(); - } - @SuppressWarnings("resource") @Test public void simpleInsert() throws InterruptedException { diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionTest.java index 1d7a3e0c4f..1bfae5f7d6 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionTest.java @@ -38,7 +38,6 @@ import com.google.cloud.spanner.ReadContext; import com.google.cloud.spanner.ReadOnlyTransaction; import com.google.cloud.spanner.ResultSet; -import com.google.cloud.spanner.Spanner; import com.google.cloud.spanner.SpannerException; import com.google.cloud.spanner.Statement; import com.google.cloud.spanner.Struct; @@ -52,39 +51,24 @@ import com.google.common.util.concurrent.Uninterruptibles; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; import java.util.List; import java.util.Vector; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import org.junit.After; -import org.junit.Before; import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Test; import org.junit.experimental.categories.Category; import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameter; -import org.junit.runners.Parameterized.Parameters; +import org.junit.runners.JUnit4; /** Integration tests for read-write transactions. */ @Category(ParallelIntegrationTest.class) -@RunWith(Parameterized.class) +@RunWith(JUnit4.class) public class ITTransactionTest { - - @Parameter(0) - public boolean inlineBegin; - - @Parameters(name = "inlineBegin = {0}") - public static Collection data() { - return Arrays.asList(new Object[][] {{false}, {true}}); - } - @ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv(); private static Database db; - private Spanner spanner; - private DatabaseClient client; + private static DatabaseClient client; /** Sequence for assigning unique keys to test cases. */ private static int seq; @@ -97,24 +81,7 @@ public static void setUpDatabase() { + " K STRING(MAX) NOT NULL," + " V INT64," + ") PRIMARY KEY (K)"); - } - - @Before - public void setupClient() { - spanner = - env.getTestHelper() - .getOptions() - .toBuilder() - .setInlineBeginForReadWriteTransaction(inlineBegin) - .build() - .getService(); - client = spanner.getDatabaseClient(db.getId()); - } - - @After - public void closeClient() { - client.writeAtLeastOnce(ImmutableList.of(Mutation.delete("T", KeySet.all()))); - spanner.close(); + client = env.getTestHelper().getDatabaseClient(db); } private static String uniqueKey() { From 24ea415052e4af5d258bfdac4eed50a541818cc9 Mon Sep 17 00:00:00 2001 From: Olav Loite Date: Wed, 21 Oct 2020 09:43:41 +0200 Subject: [PATCH 18/19] chore: run formatter --- .../com/google/cloud/spanner/TransactionRunnerImpl.java | 2 +- .../google/cloud/spanner/InlineBeginTransactionTest.java | 6 +++--- .../com/google/cloud/spanner/MockSpannerServiceImpl.java | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) 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 5befec7a23..e38b704f70 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 @@ -159,7 +159,7 @@ public void removeListener(Runnable listener) { private volatile SettableApiFuture transactionIdFuture = null; volatile ByteString transactionId; - + private Timestamp commitTimestamp; private TransactionContextImpl(Builder builder) { 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 ec49939bfe..d1e3d93cb7 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 @@ -546,10 +546,10 @@ public void testInlinedBeginTxWithErrorOnSecondPartialResultSet() { final Statement statement = Statement.of("SELECT * FROM BROKEN_TABLE"); RandomResultSetGenerator generator = new RandomResultSetGenerator(2); mockSpanner.putStatementResult(StatementResult.query(statement, generator.generate())); - // The first PartialResultSet will be returned successfully, and then a DATA_LOSS exception will be returned. + // The first PartialResultSet will be returned successfully, and then a DATA_LOSS exception will + // be returned. mockSpanner.setExecuteStreamingSqlExecutionTime( - SimulatedExecutionTime.ofStreamException( - Status.DATA_LOSS.asRuntimeException(), 1)); + SimulatedExecutionTime.ofStreamException(Status.DATA_LOSS.asRuntimeException(), 1)); DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); Void res = diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java index 5c05e59d3f..85e935a75a 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java @@ -1920,7 +1920,7 @@ public void waitForLastRequestToBe(Class type, long t public List getTransactionsStarted() { return new ArrayList<>(transactionsStarted); } - + public void waitForRequestsToContain(Class type, long timeoutMillis) throws InterruptedException, TimeoutException { Stopwatch watch = Stopwatch.createStarted(); From 28277ff85aae454614eb14733db99fab8db1dcbc Mon Sep 17 00:00:00 2001 From: Olav Loite Date: Wed, 21 Oct 2020 10:40:31 +0200 Subject: [PATCH 19/19] test: fix integration test that relied on data from other test case --- .../google/cloud/spanner/it/ITTransactionTest.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionTest.java index 1bfae5f7d6..503f0ddf90 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionTest.java @@ -45,6 +45,7 @@ import com.google.cloud.spanner.TransactionContext; import com.google.cloud.spanner.TransactionRunner; import com.google.cloud.spanner.TransactionRunner.TransactionCallable; +import com.google.cloud.spanner.testing.EmulatorSpannerHelper; import com.google.common.collect.ImmutableList; import com.google.common.collect.Sets; import com.google.common.util.concurrent.SettableFuture; @@ -55,6 +56,7 @@ import java.util.Vector; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import org.junit.Before; import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Test; @@ -84,6 +86,11 @@ public static void setUpDatabase() { client = env.getTestHelper().getDatabaseClient(db); } + @Before + public void removeTestData() { + client.writeAtLeastOnce(Arrays.asList(Mutation.delete("T", KeySet.all()))); + } + private static String uniqueKey() { return "k" + seq++; } @@ -515,7 +522,7 @@ public Void run(TransactionContext transaction) throws SpannerException { public void testTxWithCaughtError() { assumeFalse( "Emulator does not recover from an error within a transaction", - env.getTestHelper().isEmulator()); + EmulatorSpannerHelper.isUsingEmulator()); long updateCount = client @@ -545,7 +552,7 @@ public Long run(TransactionContext transaction) throws Exception { public void testTxWithConstraintError() { assumeFalse( "Emulator does not recover from an error within a transaction", - env.getTestHelper().isEmulator()); + EmulatorSpannerHelper.isUsingEmulator()); // First insert a single row. client.writeAtLeastOnce(