diff --git a/google-cloud-clients/google-cloud-spanner/pom.xml b/google-cloud-clients/google-cloud-spanner/pom.xml
index cecfe3530085..41d13f064747 100644
--- a/google-cloud-clients/google-cloud-spanner/pom.xml
+++ b/google-cloud-clients/google-cloud-spanner/pom.xml
@@ -61,6 +61,7 @@
com.google.cloud.spanner.IntegrationTest
com.google.cloud.spanner.FlakyTest
+ 2400
diff --git a/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/BatchClientImpl.java b/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/BatchClientImpl.java
index 8840abc7415a..3e19ee30e026 100644
--- a/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/BatchClientImpl.java
+++ b/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/BatchClientImpl.java
@@ -46,7 +46,7 @@ public class BatchClientImpl implements BatchClient {
@Override
public BatchReadOnlyTransaction batchReadOnlyTransaction(TimestampBound bound) {
- SessionImpl session = (SessionImpl) spanner.createSession(db);
+ SessionImpl session = spanner.createSession(db);
return new BatchReadOnlyTransactionImpl(spanner, session, checkNotNull(bound));
}
diff --git a/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java b/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java
index 749d01278b65..da644d5a0fc3 100644
--- a/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java
+++ b/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java
@@ -17,6 +17,9 @@
package com.google.cloud.spanner;
import com.google.cloud.Timestamp;
+import com.google.cloud.spanner.SessionPool.PooledSession;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
import com.google.common.util.concurrent.ListenableFuture;
import io.opencensus.common.Scope;
import io.opencensus.trace.Span;
@@ -33,17 +36,33 @@ class DatabaseClientImpl implements DatabaseClient {
TraceUtil.exportSpans(READ_WRITE_TRANSACTION, READ_ONLY_TRANSACTION, PARTITION_DML_TRANSACTION);
}
- private final SessionPool pool;
+ @VisibleForTesting final SessionPool pool;
DatabaseClientImpl(SessionPool pool) {
this.pool = pool;
}
+ @VisibleForTesting
+ PooledSession getReadSession() {
+ return pool.getReadSession();
+ }
+
+ @VisibleForTesting
+ PooledSession getReadWriteSession() {
+ return pool.getReadWriteSession();
+ }
+
@Override
- public Timestamp write(Iterable mutations) throws SpannerException {
+ public Timestamp write(final Iterable mutations) throws SpannerException {
Span span = tracer.spanBuilder(READ_WRITE_TRANSACTION).startSpan();
try (Scope s = tracer.withSpan(span)) {
- return pool.getReadWriteSession().write(mutations);
+ return runWithSessionRetry(
+ new Function() {
+ @Override
+ public Timestamp apply(Session session) {
+ return session.write(mutations);
+ }
+ });
} catch (RuntimeException e) {
TraceUtil.endSpanWithFailure(span, e);
throw e;
@@ -53,10 +72,16 @@ public Timestamp write(Iterable mutations) throws SpannerException {
}
@Override
- public Timestamp writeAtLeastOnce(Iterable mutations) throws SpannerException {
+ public Timestamp writeAtLeastOnce(final Iterable mutations) throws SpannerException {
Span span = tracer.spanBuilder(READ_WRITE_TRANSACTION).startSpan();
try (Scope s = tracer.withSpan(span)) {
- return pool.getReadWriteSession().writeAtLeastOnce(mutations);
+ return runWithSessionRetry(
+ new Function() {
+ @Override
+ public Timestamp apply(Session session) {
+ return session.writeAtLeastOnce(mutations);
+ }
+ });
} catch (RuntimeException e) {
TraceUtil.endSpanWithFailure(span, e);
throw e;
@@ -69,7 +94,7 @@ public Timestamp writeAtLeastOnce(Iterable mutations) throws SpannerEx
public ReadContext singleUse() {
Span span = tracer.spanBuilder(READ_ONLY_TRANSACTION).startSpan();
try (Scope s = tracer.withSpan(span)) {
- return pool.getReadSession().singleUse();
+ return getReadSession().singleUse();
} catch (RuntimeException e) {
TraceUtil.endSpanWithFailure(span, e);
throw e;
@@ -80,7 +105,7 @@ public ReadContext singleUse() {
public ReadContext singleUse(TimestampBound bound) {
Span span = tracer.spanBuilder(READ_ONLY_TRANSACTION).startSpan();
try (Scope s = tracer.withSpan(span)) {
- return pool.getReadSession().singleUse(bound);
+ return getReadSession().singleUse(bound);
} catch (RuntimeException e) {
TraceUtil.endSpanWithFailure(span, e);
throw e;
@@ -91,7 +116,7 @@ public ReadContext singleUse(TimestampBound bound) {
public ReadOnlyTransaction singleUseReadOnlyTransaction() {
Span span = tracer.spanBuilder(READ_ONLY_TRANSACTION).startSpan();
try (Scope s = tracer.withSpan(span)) {
- return pool.getReadSession().singleUseReadOnlyTransaction();
+ return getReadSession().singleUseReadOnlyTransaction();
} catch (RuntimeException e) {
TraceUtil.endSpanWithFailure(span, e);
throw e;
@@ -102,7 +127,7 @@ public ReadOnlyTransaction singleUseReadOnlyTransaction() {
public ReadOnlyTransaction singleUseReadOnlyTransaction(TimestampBound bound) {
Span span = tracer.spanBuilder(READ_ONLY_TRANSACTION).startSpan();
try (Scope s = tracer.withSpan(span)) {
- return pool.getReadSession().singleUseReadOnlyTransaction(bound);
+ return getReadSession().singleUseReadOnlyTransaction(bound);
} catch (RuntimeException e) {
TraceUtil.endSpanWithFailure(span, e);
throw e;
@@ -113,7 +138,7 @@ public ReadOnlyTransaction singleUseReadOnlyTransaction(TimestampBound bound) {
public ReadOnlyTransaction readOnlyTransaction() {
Span span = tracer.spanBuilder(READ_ONLY_TRANSACTION).startSpan();
try (Scope s = tracer.withSpan(span)) {
- return pool.getReadSession().readOnlyTransaction();
+ return getReadSession().readOnlyTransaction();
} catch (RuntimeException e) {
TraceUtil.endSpanWithFailure(span, e);
throw e;
@@ -124,7 +149,7 @@ public ReadOnlyTransaction readOnlyTransaction() {
public ReadOnlyTransaction readOnlyTransaction(TimestampBound bound) {
Span span = tracer.spanBuilder(READ_ONLY_TRANSACTION).startSpan();
try (Scope s = tracer.withSpan(span)) {
- return pool.getReadSession().readOnlyTransaction(bound);
+ return getReadSession().readOnlyTransaction(bound);
} catch (RuntimeException e) {
TraceUtil.endSpanWithFailure(span, e);
throw e;
@@ -135,7 +160,7 @@ public ReadOnlyTransaction readOnlyTransaction(TimestampBound bound) {
public TransactionRunner readWriteTransaction() {
Span span = tracer.spanBuilder(READ_WRITE_TRANSACTION).startSpan();
try (Scope s = tracer.withSpan(span)) {
- return pool.getReadWriteSession().readWriteTransaction();
+ return getReadWriteSession().readWriteTransaction();
} catch (RuntimeException e) {
TraceUtil.endSpanWithFailure(span, e);
throw e;
@@ -146,7 +171,7 @@ public TransactionRunner readWriteTransaction() {
public TransactionManager transactionManager() {
Span span = tracer.spanBuilder(READ_WRITE_TRANSACTION).startSpan();
try (Scope s = tracer.withSpan(span)) {
- return pool.getReadWriteSession().transactionManager();
+ return getReadWriteSession().transactionManager();
} catch (RuntimeException e) {
TraceUtil.endSpanWithFailure(span, e);
throw e;
@@ -154,16 +179,33 @@ public TransactionManager transactionManager() {
}
@Override
- public long executePartitionedUpdate(Statement stmt) {
+ public long executePartitionedUpdate(final Statement stmt) {
Span span = tracer.spanBuilder(PARTITION_DML_TRANSACTION).startSpan();
try (Scope s = tracer.withSpan(span)) {
- return pool.getReadWriteSession().executePartitionedUpdate(stmt);
+ return runWithSessionRetry(
+ new Function() {
+ @Override
+ public Long apply(Session session) {
+ return session.executePartitionedUpdate(stmt);
+ }
+ });
} catch (RuntimeException e) {
TraceUtil.endSpanWithFailure(span, e);
throw e;
}
}
+ private T runWithSessionRetry(Function callable) {
+ PooledSession session = getReadWriteSession();
+ while (true) {
+ try {
+ return callable.apply(session);
+ } catch (SessionNotFoundException e) {
+ session = pool.replaceReadWriteSession(e, session);
+ }
+ }
+ }
+
ListenableFuture closeAsync() {
return pool.closeAsync();
}
diff --git a/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ForwardingResultSet.java b/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ForwardingResultSet.java
index 85b2f25f5d2a..753c3f6f3909 100644
--- a/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ForwardingResultSet.java
+++ b/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ForwardingResultSet.java
@@ -22,13 +22,26 @@
/** Forwarding implementation of ResultSet that forwards all calls to a delegate. */
public class ForwardingResultSet extends ForwardingStructReader implements ResultSet {
- private final ResultSet delegate;
+ private ResultSet delegate;
public ForwardingResultSet(ResultSet delegate) {
super(delegate);
this.delegate = Preconditions.checkNotNull(delegate);
}
+ /**
+ * Replaces the underlying {@link ResultSet}. It is the responsibility of the caller to ensure
+ * that the new delegate has the same properties and is in the same state as the original
+ * delegate. This method can be used if the underlying delegate needs to be replaced after a
+ * session or transaction needed to be restarted after the {@link ResultSet} had already been
+ * returned to the user.
+ */
+ void replaceDelegate(ResultSet newDelegate) {
+ Preconditions.checkNotNull(newDelegate);
+ super.replaceDelegate(newDelegate);
+ this.delegate = newDelegate;
+ }
+
@Override
public boolean next() throws SpannerException {
return delegate.next();
diff --git a/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ForwardingStructReader.java b/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ForwardingStructReader.java
index 6ad5b9a6c940..9b30b8998522 100644
--- a/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ForwardingStructReader.java
+++ b/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ForwardingStructReader.java
@@ -25,12 +25,23 @@
/** Forwarding implements of StructReader */
public class ForwardingStructReader implements StructReader {
- private final StructReader delegate;
+ private StructReader delegate;
public ForwardingStructReader(StructReader delegate) {
this.delegate = Preconditions.checkNotNull(delegate);
}
+ /**
+ * Replaces the underlying {@link StructReader}. It is the responsibility of the caller to ensure
+ * that the new delegate has the same properties and is in the same state as the original
+ * delegate. This method can be used if the underlying delegate needs to be replaced after a
+ * session or transaction needed to be restarted after the {@link StructReader} had already been
+ * returned to the user.
+ */
+ void replaceDelegate(StructReader newDelegate) {
+ this.delegate = Preconditions.checkNotNull(newDelegate);
+ }
+
@Override
public Type getType() {
return delegate.getType();
diff --git a/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionNotFoundException.java b/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionNotFoundException.java
new file mode 100644
index 000000000000..5fe18eff56ea
--- /dev/null
+++ b/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionNotFoundException.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2019 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 javax.annotation.Nullable;
+
+/**
+ * Exception thrown by Cloud Spanner when an operation detects that the session that is being used
+ * is no longer valid. This type of error has its own subclass as it is a condition that should
+ * normally be hidden from the user, and the client library should try to fix this internally.
+ */
+public class SessionNotFoundException extends SpannerException {
+ private static final long serialVersionUID = -6395746612598975751L;
+
+ /** Private constructor. Use {@link SpannerExceptionFactory} to create instances. */
+ SessionNotFoundException(
+ DoNotConstructDirectly token, @Nullable String message, @Nullable Throwable cause) {
+ super(token, ErrorCode.NOT_FOUND, false, message, cause);
+ }
+}
diff --git a/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPool.java b/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPool.java
index 695ce9685961..670608a6f551 100644
--- a/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPool.java
+++ b/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPool.java
@@ -24,7 +24,9 @@
import com.google.cloud.spanner.Options.QueryOption;
import com.google.cloud.spanner.Options.ReadOption;
import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
import com.google.common.base.Preconditions;
+import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.util.concurrent.ListenableFuture;
@@ -75,35 +77,79 @@ Instant instant() {
* Wrapper around {@code ReadContext} that releases the session to the pool once the call is
* finished, if it is a single use context.
*/
- private static class AutoClosingReadContext implements ReadContext {
- private final ReadContext delegate;
- private final PooledSession session;
+ private static class AutoClosingReadContext implements ReadContext {
+ private final Function readContextDelegateSupplier;
+ private T readContextDelegate;
+ private final SessionPool sessionPool;
+ private PooledSession session;
private final boolean isSingleUse;
private boolean closed;
+ private boolean sessionUsedForQuery = false;
private AutoClosingReadContext(
- ReadContext delegate, PooledSession session, boolean isSingleUse) {
- this.delegate = delegate;
+ Function delegateSupplier,
+ SessionPool sessionPool,
+ PooledSession session,
+ boolean isSingleUse) {
+ this.readContextDelegateSupplier = delegateSupplier;
+ this.sessionPool = sessionPool;
this.session = session;
this.isSingleUse = isSingleUse;
+ while (true) {
+ try {
+ this.readContextDelegate = readContextDelegateSupplier.apply(this.session);
+ break;
+ } catch (SessionNotFoundException e) {
+ replaceSessionIfPossible(e);
+ }
+ }
}
- private ResultSet wrap(final ResultSet resultSet) {
- session.markUsed();
- if (!isSingleUse) {
- return resultSet;
+ T getReadContextDelegate() {
+ return readContextDelegate;
+ }
+
+ private ResultSet wrap(final Supplier resultSetSupplier) {
+ ResultSet res;
+ while (true) {
+ try {
+ res = resultSetSupplier.get();
+ break;
+ } catch (SessionNotFoundException e) {
+ replaceSessionIfPossible(e);
+ }
}
- return new ForwardingResultSet(resultSet) {
+ return new ForwardingResultSet(res) {
+ private boolean beforeFirst = true;
+
@Override
public boolean next() throws SpannerException {
+ while (true) {
+ try {
+ return internalNext();
+ } catch (SessionNotFoundException e) {
+ replaceSessionIfPossible(e);
+ replaceDelegate(resultSetSupplier.get());
+ }
+ }
+ }
+
+ private boolean internalNext() {
try {
boolean ret = super.next();
- if (!ret) {
+ if (beforeFirst) {
+ session.markUsed();
+ beforeFirst = false;
+ sessionUsedForQuery = true;
+ }
+ if (!ret && isSingleUse) {
close();
}
return ret;
+ } catch (SessionNotFoundException e) {
+ throw e;
} catch (SpannerException e) {
- if (!closed) {
+ if (!closed && isSingleUse) {
session.lastException = e;
AutoClosingReadContext.this.close();
}
@@ -114,30 +160,69 @@ public boolean next() throws SpannerException {
@Override
public void close() {
super.close();
- AutoClosingReadContext.this.close();
+ if (isSingleUse) {
+ AutoClosingReadContext.this.close();
+ }
}
};
}
+ private void replaceSessionIfPossible(SessionNotFoundException e) {
+ 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(e, session);
+ readContextDelegate = readContextDelegateSupplier.apply(session);
+ } else {
+ throw e;
+ }
+ }
+
@Override
public ResultSet read(
- String table, KeySet keys, Iterable columns, ReadOption... options) {
- return wrap(delegate.read(table, keys, columns, options));
+ final String table,
+ final KeySet keys,
+ final Iterable columns,
+ final ReadOption... options) {
+ return wrap(
+ new Supplier() {
+ @Override
+ public ResultSet get() {
+ return readContextDelegate.read(table, keys, columns, options);
+ }
+ });
}
@Override
public ResultSet readUsingIndex(
- String table, String index, KeySet keys, Iterable columns, ReadOption... options) {
- return wrap(delegate.readUsingIndex(table, index, keys, columns, options));
+ final String table,
+ final String index,
+ final KeySet keys,
+ final Iterable columns,
+ final ReadOption... options) {
+ return wrap(
+ new Supplier() {
+ @Override
+ public ResultSet get() {
+ return readContextDelegate.readUsingIndex(table, index, keys, columns, options);
+ }
+ });
}
@Override
@Nullable
public Struct readRow(String table, Key key, Iterable columns) {
try {
- session.markUsed();
- return delegate.readRow(table, key, columns);
+ while (true) {
+ try {
+ session.markUsed();
+ return readContextDelegate.readRow(table, key, columns);
+ } catch (SessionNotFoundException e) {
+ replaceSessionIfPossible(e);
+ }
+ }
} finally {
+ sessionUsedForQuery = true;
if (isSingleUse) {
close();
}
@@ -148,9 +233,16 @@ public Struct readRow(String table, Key key, Iterable columns) {
@Nullable
public Struct readRowUsingIndex(String table, String index, Key key, Iterable columns) {
try {
- session.markUsed();
- return delegate.readRowUsingIndex(table, index, key, columns);
+ while (true) {
+ try {
+ session.markUsed();
+ return readContextDelegate.readRowUsingIndex(table, index, key, columns);
+ } catch (SessionNotFoundException e) {
+ replaceSessionIfPossible(e);
+ }
+ }
} finally {
+ sessionUsedForQuery = true;
if (isSingleUse) {
close();
}
@@ -158,13 +250,25 @@ public Struct readRowUsingIndex(String table, String index, Key key, Iterable() {
+ @Override
+ public ResultSet get() {
+ return readContextDelegate.executeQuery(statement, options);
+ }
+ });
}
@Override
- public ResultSet analyzeQuery(Statement statement, QueryAnalyzeMode queryMode) {
- return wrap(delegate.analyzeQuery(statement, queryMode));
+ public ResultSet analyzeQuery(final Statement statement, final QueryAnalyzeMode queryMode) {
+ return wrap(
+ new Supplier() {
+ @Override
+ public ResultSet get() {
+ return readContextDelegate.analyzeQuery(statement, queryMode);
+ }
+ });
}
@Override
@@ -173,46 +277,181 @@ public void close() {
return;
}
closed = true;
- delegate.close();
+ readContextDelegate.close();
session.close();
}
}
- private static class AutoClosingReadTransaction extends AutoClosingReadContext
- implements ReadOnlyTransaction {
- private final ReadOnlyTransaction txn;
+ private static class AutoClosingReadTransaction
+ extends AutoClosingReadContext implements ReadOnlyTransaction {
AutoClosingReadTransaction(
- ReadOnlyTransaction txn, PooledSession session, boolean isSingleUse) {
- super(txn, session, isSingleUse);
- this.txn = txn;
+ Function txnSupplier,
+ SessionPool sessionPool,
+ PooledSession session,
+ boolean isSingleUse) {
+ super(txnSupplier, sessionPool, session, isSingleUse);
}
@Override
public Timestamp getReadTimestamp() {
- return txn.getReadTimestamp();
+ return getReadContextDelegate().getReadTimestamp();
}
}
private static class AutoClosingTransactionManager implements TransactionManager {
- final TransactionManager delegate;
- final PooledSession session;
+ private class SessionPoolResultSet extends ForwardingResultSet {
+ private SessionPoolResultSet(ResultSet delegate) {
+ super(delegate);
+ }
+
+ @Override
+ public boolean next() {
+ try {
+ return super.next();
+ } catch (SessionNotFoundException e) {
+ throw handleSessionNotFound(e);
+ }
+ }
+ }
+
+ /**
+ * {@link TransactionContext} that is used in combination with an {@link
+ * AutoClosingTransactionManager}. This {@link TransactionContext} handles {@link
+ * SessionNotFoundException}s by replacing the underlying session with a fresh one, and then
+ * throws an {@link AbortedException} to trigger the retry-loop that has been created by the
+ * caller.
+ */
+ private class SessionPoolTransactionContext implements TransactionContext {
+ private final TransactionContext delegate;
+
+ private SessionPoolTransactionContext(TransactionContext delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public ResultSet read(
+ String table, KeySet keys, Iterable columns, ReadOption... options) {
+ return new SessionPoolResultSet(delegate.read(table, keys, columns, options));
+ }
+
+ @Override
+ public ResultSet readUsingIndex(
+ String table,
+ String index,
+ KeySet keys,
+ Iterable columns,
+ ReadOption... options) {
+ return new SessionPoolResultSet(
+ delegate.readUsingIndex(table, index, keys, columns, options));
+ }
+
+ @Override
+ public Struct readRow(String table, Key key, Iterable columns) {
+ try {
+ return delegate.readRow(table, key, columns);
+ } catch (SessionNotFoundException e) {
+ throw handleSessionNotFound(e);
+ }
+ }
+
+ @Override
+ public void buffer(Mutation mutation) {
+ delegate.buffer(mutation);
+ }
+
+ @Override
+ public Struct readRowUsingIndex(
+ String table, String index, Key key, Iterable columns) {
+ try {
+ return delegate.readRowUsingIndex(table, index, key, columns);
+ } catch (SessionNotFoundException e) {
+ throw handleSessionNotFound(e);
+ }
+ }
+
+ @Override
+ public void buffer(Iterable mutations) {
+ delegate.buffer(mutations);
+ }
+
+ @Override
+ public long executeUpdate(Statement statement) {
+ try {
+ return delegate.executeUpdate(statement);
+ } catch (SessionNotFoundException e) {
+ throw handleSessionNotFound(e);
+ }
+ }
+
+ @Override
+ public long[] batchUpdate(Iterable statements) {
+ try {
+ return delegate.batchUpdate(statements);
+ } catch (SessionNotFoundException e) {
+ throw handleSessionNotFound(e);
+ }
+ }
+
+ @Override
+ public ResultSet executeQuery(Statement statement, QueryOption... options) {
+ return new SessionPoolResultSet(delegate.executeQuery(statement, options));
+ }
+
+ @Override
+ public ResultSet analyzeQuery(Statement statement, QueryAnalyzeMode queryMode) {
+ return new SessionPoolResultSet(delegate.analyzeQuery(statement, queryMode));
+ }
+
+ @Override
+ public void close() {
+ delegate.close();
+ }
+ }
+
+ private TransactionManager delegate;
+ private final SessionPool sessionPool;
+ private PooledSession session;
private boolean closed;
+ private boolean restartedAfterSessionNotFound;
- AutoClosingTransactionManager(TransactionManager delegate, PooledSession session) {
- this.delegate = delegate;
+ AutoClosingTransactionManager(SessionPool sessionPool, PooledSession session) {
+ this.sessionPool = sessionPool;
this.session = session;
+ this.delegate = session.delegate.transactionManager();
}
@Override
public TransactionContext begin() {
- return delegate.begin();
+ while (true) {
+ try {
+ return internalBegin();
+ } catch (SessionNotFoundException e) {
+ session = sessionPool.replaceReadWriteSession(e, session);
+ delegate = session.delegate.transactionManager();
+ }
+ }
+ }
+
+ private TransactionContext internalBegin() {
+ TransactionContext res = new SessionPoolTransactionContext(delegate.begin());
+ session.markUsed();
+ return res;
+ }
+
+ private SpannerException handleSessionNotFound(SessionNotFoundException e) {
+ session = sessionPool.replaceReadWriteSession(e, session);
+ delegate = session.delegate.transactionManager();
+ restartedAfterSessionNotFound = true;
+ return SpannerExceptionFactory.newSpannerException(ErrorCode.ABORTED, e.getMessage(), e);
}
@Override
public void commit() {
try {
delegate.commit();
+ } catch (SessionNotFoundException e) {
+ throw handleSessionNotFound(e);
} finally {
if (getState() != TransactionState.ABORTED) {
close();
@@ -231,7 +470,21 @@ public void rollback() {
@Override
public TransactionContext resetForRetry() {
- return delegate.resetForRetry();
+ while (true) {
+ try {
+ if (restartedAfterSessionNotFound) {
+ TransactionContext res = new SessionPoolTransactionContext(delegate.begin());
+ restartedAfterSessionNotFound = false;
+ return res;
+ } else {
+ return new SessionPoolTransactionContext(delegate.resetForRetry());
+ }
+ } catch (SessionNotFoundException e) {
+ session = sessionPool.replaceReadWriteSession(e, session);
+ delegate = session.delegate.transactionManager();
+ restartedAfterSessionNotFound = true;
+ }
+ }
}
@Override
@@ -254,7 +507,61 @@ public void close() {
@Override
public TransactionState getState() {
- return delegate.getState();
+ if (restartedAfterSessionNotFound) {
+ return TransactionState.ABORTED;
+ } else {
+ return delegate.getState();
+ }
+ }
+ }
+
+ /**
+ * {@link TransactionRunner} that automatically handles {@link SessionNotFoundException}s by
+ * replacing the underlying read/write session and then restarts the transaction.
+ */
+ private static final class SessionPoolTransactionRunner implements TransactionRunner {
+ private final SessionPool sessionPool;
+ private PooledSession session;
+ private TransactionRunner runner;
+
+ private SessionPoolTransactionRunner(SessionPool sessionPool, PooledSession session) {
+ this.sessionPool = sessionPool;
+ this.session = session;
+ this.runner = session.delegate.readWriteTransaction();
+ }
+
+ @Override
+ @Nullable
+ public T run(TransactionCallable callable) {
+ try {
+ T result;
+ while (true) {
+ try {
+ result = runner.run(callable);
+ break;
+ } catch (SessionNotFoundException e) {
+ session = sessionPool.replaceReadWriteSession(e, session);
+ runner = session.delegate.readWriteTransaction();
+ }
+ }
+ session.markUsed();
+ return result;
+ } catch (SpannerException e) {
+ throw session.lastException = e;
+ } finally {
+ session.close();
+ }
+ }
+
+ @Override
+ public Timestamp getCommitTimestamp() {
+ return runner.getCommitTimestamp();
+ }
+
+ @Override
+ public TransactionRunner allowNestedTransaction() {
+ runner.allowNestedTransaction();
+ return runner;
}
}
@@ -275,18 +582,24 @@ private enum SessionState {
}
final class PooledSession implements Session {
- @VisibleForTesting final Session delegate;
+ @VisibleForTesting SessionImpl delegate;
private volatile Instant lastUseTime;
private volatile SpannerException lastException;
private volatile LeakedSessionException leakedException;
+ private volatile boolean allowReplacing = true;
@GuardedBy("lock")
private SessionState state;
- private PooledSession(Session delegate) {
+ private PooledSession(SessionImpl delegate) {
this.delegate = delegate;
this.state = SessionState.AVAILABLE;
- markUsed();
+ this.lastUseTime = clock.instant();
+ }
+
+ @VisibleForTesting
+ void setAllowReplacing(boolean allowReplacing) {
+ this.allowReplacing = allowReplacing;
}
private void markBusy() {
@@ -337,7 +650,16 @@ public Timestamp writeAtLeastOnce(Iterable mutations) throws SpannerEx
@Override
public ReadContext singleUse() {
try {
- return new AutoClosingReadContext(delegate.singleUse(), this, true);
+ return new AutoClosingReadContext<>(
+ new Function() {
+ @Override
+ public ReadContext apply(PooledSession session) {
+ return session.delegate.singleUse();
+ }
+ },
+ SessionPool.this,
+ this,
+ true);
} catch (Exception e) {
close();
throw e;
@@ -345,9 +667,18 @@ public ReadContext singleUse() {
}
@Override
- public ReadContext singleUse(TimestampBound bound) {
+ public ReadContext singleUse(final TimestampBound bound) {
try {
- return new AutoClosingReadContext(delegate.singleUse(bound), this, true);
+ return new AutoClosingReadContext<>(
+ new Function() {
+ @Override
+ public ReadContext apply(PooledSession session) {
+ return session.delegate.singleUse(bound);
+ }
+ },
+ SessionPool.this,
+ this,
+ true);
} catch (Exception e) {
close();
throw e;
@@ -356,39 +687,57 @@ public ReadContext singleUse(TimestampBound bound) {
@Override
public ReadOnlyTransaction singleUseReadOnlyTransaction() {
- try {
- return new AutoClosingReadTransaction(delegate.singleUseReadOnlyTransaction(), this, true);
- } catch (Exception e) {
- close();
- throw e;
- }
+ return internalReadOnlyTransaction(
+ new Function() {
+ @Override
+ public ReadOnlyTransaction apply(PooledSession session) {
+ return session.delegate.singleUseReadOnlyTransaction();
+ }
+ },
+ true);
}
@Override
- public ReadOnlyTransaction singleUseReadOnlyTransaction(TimestampBound bound) {
- try {
- return new AutoClosingReadTransaction(
- delegate.singleUseReadOnlyTransaction(bound), this, true);
- } catch (Exception e) {
- close();
- throw e;
- }
+ public ReadOnlyTransaction singleUseReadOnlyTransaction(final TimestampBound bound) {
+ return internalReadOnlyTransaction(
+ new Function() {
+ @Override
+ public ReadOnlyTransaction apply(PooledSession session) {
+ return session.delegate.singleUseReadOnlyTransaction(bound);
+ }
+ },
+ true);
}
@Override
public ReadOnlyTransaction readOnlyTransaction() {
- try {
- return new AutoClosingReadTransaction(delegate.readOnlyTransaction(), this, false);
- } catch (Exception e) {
- close();
- throw e;
- }
+ return internalReadOnlyTransaction(
+ new Function() {
+ @Override
+ public ReadOnlyTransaction apply(PooledSession session) {
+ return session.delegate.readOnlyTransaction();
+ }
+ },
+ false);
}
@Override
- public ReadOnlyTransaction readOnlyTransaction(TimestampBound bound) {
+ public ReadOnlyTransaction readOnlyTransaction(final TimestampBound bound) {
+ return internalReadOnlyTransaction(
+ new Function() {
+ @Override
+ public ReadOnlyTransaction apply(PooledSession session) {
+ return session.delegate.readOnlyTransaction(bound);
+ }
+ },
+ false);
+ }
+
+ private ReadOnlyTransaction internalReadOnlyTransaction(
+ Function transactionSupplier, boolean isSingleUse) {
try {
- return new AutoClosingReadTransaction(delegate.readOnlyTransaction(bound), this, false);
+ return new AutoClosingReadTransaction(
+ transactionSupplier, SessionPool.this, this, isSingleUse);
} catch (Exception e) {
close();
throw e;
@@ -397,34 +746,7 @@ public ReadOnlyTransaction readOnlyTransaction(TimestampBound bound) {
@Override
public TransactionRunner readWriteTransaction() {
- final TransactionRunner runner = delegate.readWriteTransaction();
- return new TransactionRunner() {
-
- @Override
- @Nullable
- public T run(TransactionCallable callable) {
- try {
- markUsed();
- T result = runner.run(callable);
- return result;
- } catch (SpannerException e) {
- throw lastException = e;
- } finally {
- close();
- }
- }
-
- @Override
- public Timestamp getCommitTimestamp() {
- return runner.getCommitTimestamp();
- }
-
- @Override
- public TransactionRunner allowNestedTransaction() {
- runner.allowNestedTransaction();
- return runner;
- }
- };
+ return new SessionPoolTransactionRunner(SessionPool.this, this);
}
@Override
@@ -469,8 +791,7 @@ private void markUsed() {
@Override
public TransactionManager transactionManager() {
- markUsed();
- return new AutoClosingTransactionManager(delegate.transactionManager(), this);
+ return new AutoClosingTransactionManager(SessionPool.this, this);
}
}
@@ -756,6 +1077,11 @@ private SessionPool(
this.poolMaintainer = new PoolMaintainer();
}
+ @VisibleForTesting
+ int getNumberOfAvailableWritePreparedSessions() {
+ return writePreparedSessions.size();
+ }
+
private void initPool() {
synchronized (lock) {
poolMaintainer.init();
@@ -823,7 +1149,7 @@ private PooledSession findSessionToKeepAlive(
* session being returned to the pool or a new session being created.
*
*/
- Session getReadSession() throws SpannerException {
+ PooledSession getReadSession() throws SpannerException {
Span span = Tracing.getTracer().getCurrentSpan();
span.addAnnotation("Acquiring session");
Waiter waiter = null;
@@ -879,7 +1205,7 @@ Session getReadSession() throws SpannerException {
* to the pool which is then write prepared.
*
*/
- Session getReadWriteSession() {
+ PooledSession getReadWriteSession() {
Span span = Tracing.getTracer().getCurrentSpan();
span.addAnnotation("Acquiring read write session");
Waiter waiter = null;
@@ -919,6 +1245,24 @@ Session getReadWriteSession() {
return sess;
}
+ PooledSession replaceReadSession(SessionNotFoundException e, PooledSession session) {
+ if (!options.isFailIfSessionNotFound() && session.allowReplacing) {
+ closeSessionAsync(session);
+ return getReadSession();
+ } else {
+ throw e;
+ }
+ }
+
+ PooledSession replaceReadWriteSession(SessionNotFoundException e, PooledSession session) {
+ if (!options.isFailIfSessionNotFound() && session.allowReplacing) {
+ closeSessionAsync(session);
+ return getReadWriteSession();
+ } else {
+ throw e;
+ }
+ }
+
private Annotation sessionAnnotation(Session session) {
AttributeValue sessionId = AttributeValue.stringAttributeValue(session.getName());
return Annotation.fromDescriptionAndAttributes(
@@ -1178,7 +1522,7 @@ private void createSession() {
new Runnable() {
@Override
public void run() {
- Session session = null;
+ SessionImpl session = null;
try {
session = spanner.createSession(db);
logger.log(Level.FINE, "Session created");
diff --git a/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPoolOptions.java b/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPoolOptions.java
index 0ae3b8aa7b03..cda7341b6e5a 100644
--- a/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPoolOptions.java
+++ b/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPoolOptions.java
@@ -16,6 +16,7 @@
package com.google.cloud.spanner;
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
/** Options for the session pool used by {@code DatabaseClient}. */
@@ -29,6 +30,7 @@ public class SessionPoolOptions {
private final float writeSessionsFraction;
private final ActionOnExhaustion actionOnExhaustion;
private final int keepAliveIntervalMinutes;
+ private final ActionOnSessionNotFound actionOnSessionNotFound;
private SessionPoolOptions(Builder builder) {
this.minSessions = builder.minSessions;
@@ -36,6 +38,7 @@ private SessionPoolOptions(Builder builder) {
this.maxIdleSessions = builder.maxIdleSessions;
this.writeSessionsFraction = builder.writeSessionsFraction;
this.actionOnExhaustion = builder.actionOnExhaustion;
+ this.actionOnSessionNotFound = builder.actionOnSessionNotFound;
this.keepAliveIntervalMinutes = builder.keepAliveIntervalMinutes;
}
@@ -67,6 +70,11 @@ public boolean isBlockIfPoolExhausted() {
return actionOnExhaustion == ActionOnExhaustion.BLOCK;
}
+ @VisibleForTesting
+ boolean isFailIfSessionNotFound() {
+ return actionOnSessionNotFound == ActionOnSessionNotFound.FAIL;
+ }
+
public static Builder newBuilder() {
return new Builder();
}
@@ -76,6 +84,11 @@ private static enum ActionOnExhaustion {
FAIL,
}
+ private static enum ActionOnSessionNotFound {
+ RETRY,
+ FAIL;
+ }
+
/** Builder for creating SessionPoolOptions. */
public static class Builder {
private int minSessions;
@@ -83,6 +96,7 @@ public static class Builder {
private int maxIdleSessions;
private float writeSessionsFraction = 0.2f;
private ActionOnExhaustion actionOnExhaustion = DEFAULT_ACTION;
+ private ActionOnSessionNotFound actionOnSessionNotFound = ActionOnSessionNotFound.RETRY;
private int keepAliveIntervalMinutes = 30;
/**
@@ -146,6 +160,16 @@ public Builder setBlockIfPoolExhausted() {
return this;
}
+ /**
+ * If a session has been invalidated by the server, the {@link SessionPool} will by default
+ * retry the session. Set this option to throw an exception instead of retrying.
+ */
+ @VisibleForTesting
+ Builder setFailIfSessionNotFound() {
+ this.actionOnSessionNotFound = ActionOnSessionNotFound.FAIL;
+ return this;
+ }
+
/**
* 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
diff --git a/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerExceptionFactory.java b/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerExceptionFactory.java
index 7577a06bfbce..6a34b0a86082 100644
--- a/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerExceptionFactory.java
+++ b/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerExceptionFactory.java
@@ -147,6 +147,11 @@ private static SpannerException newSpannerExceptionPreformatted(
switch (code) {
case ABORTED:
return new AbortedException(token, message, cause);
+ case NOT_FOUND:
+ if (message != null && message.contains("Session not found")) {
+ return new SessionNotFoundException(token, message, cause);
+ }
+ // Fall through to the default.
default:
return new SpannerException(token, code, isRetryable(code, cause), message, cause);
}
diff --git a/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java b/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java
index 712917fc1a18..5e60686c7e5b 100644
--- a/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java
+++ b/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java
@@ -29,6 +29,7 @@
import com.google.cloud.PageImpl.NextPageFetcher;
import com.google.cloud.spanner.spi.v1.SpannerRpc;
import com.google.cloud.spanner.spi.v1.SpannerRpc.Paginated;
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableMap;
@@ -203,8 +204,7 @@ int getDefaultPrefetchChunks() {
return defaultPrefetchChunks;
}
- // TODO(user): change this to return SessionImpl and modify all corresponding references.
- Session createSession(final DatabaseId db) throws SpannerException {
+ SessionImpl createSession(final DatabaseId db) throws SpannerException {
final Map options =
optionMap(SessionOption.channelHint(random.nextLong()));
Span span = tracer.spanBuilder(CREATE_SESSION).startSpan();
@@ -250,13 +250,18 @@ public DatabaseClient getDatabaseClient(DatabaseId db) {
return dbClients.get(db);
} else {
SessionPool pool = SessionPool.createPool(getOptions(), db, SpannerImpl.this);
- DatabaseClientImpl dbClient = new DatabaseClientImpl(pool);
+ DatabaseClientImpl dbClient = createDatabaseClient(pool);
dbClients.put(db, dbClient);
return dbClient;
}
}
}
+ @VisibleForTesting
+ DatabaseClientImpl createDatabaseClient(SessionPool pool) {
+ return new DatabaseClientImpl(pool);
+ }
+
@Override
public BatchClient getBatchClient(DatabaseId db) {
return new BatchClientImpl(db, SpannerImpl.this);
diff --git a/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/testing/RemoteSpannerHelper.java b/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/testing/RemoteSpannerHelper.java
index 09a314464295..9ab9e19435eb 100644
--- a/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/testing/RemoteSpannerHelper.java
+++ b/google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/testing/RemoteSpannerHelper.java
@@ -47,7 +47,7 @@ public class RemoteSpannerHelper {
private static int dbPrefix = new Random().nextInt(Integer.MAX_VALUE);
private final List dbs = new ArrayList<>();
- private RemoteSpannerHelper(SpannerOptions options, InstanceId instanceId, Spanner client) {
+ protected RemoteSpannerHelper(SpannerOptions options, InstanceId instanceId, Spanner client) {
this.options = options;
this.instanceId = instanceId;
this.client = client;
diff --git a/google-cloud-clients/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BaseSessionPoolTest.java b/google-cloud-clients/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BaseSessionPoolTest.java
index 8209fe2fd3a9..340327336373 100644
--- a/google-cloud-clients/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BaseSessionPoolTest.java
+++ b/google-cloud-clients/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BaseSessionPoolTest.java
@@ -56,8 +56,8 @@ public void release(ScheduledExecutorService executor) {
}
}
- Session mockSession() {
- Session session = mock(Session.class);
+ SessionImpl mockSession() {
+ SessionImpl session = mock(SessionImpl.class);
when(session.getName())
.thenReturn(
"projects/dummy/instances/dummy/database/dummy/sessions/session" + sessionIndex);
diff --git a/google-cloud-clients/google-cloud-spanner/src/test/java/com/google/cloud/spanner/IntegrationTestEnv.java b/google-cloud-clients/google-cloud-spanner/src/test/java/com/google/cloud/spanner/IntegrationTestEnv.java
index fa5ee4257683..63741cd2f7a9 100644
--- a/google-cloud-clients/google-cloud-spanner/src/test/java/com/google/cloud/spanner/IntegrationTestEnv.java
+++ b/google-cloud-clients/google-cloud-spanner/src/test/java/com/google/cloud/spanner/IntegrationTestEnv.java
@@ -85,7 +85,7 @@ protected void before() throws Throwable {
instanceId = InstanceId.of(config.spannerOptions().getProjectId(), "test-instance");
isOwnedInstance = true;
}
- testHelper = RemoteSpannerHelper.create(options, instanceId);
+ testHelper = createTestHelper(options, instanceId);
instanceAdminClient = testHelper.getClient().getInstanceAdminClient();
logger.log(Level.FINE, "Test env endpoint is {0}", options.getHost());
if (isOwnedInstance) {
@@ -93,6 +93,11 @@ protected void before() throws Throwable {
}
}
+ RemoteSpannerHelper createTestHelper(SpannerOptions options, InstanceId instanceId)
+ throws Throwable {
+ return RemoteSpannerHelper.create(options, instanceId);
+ }
+
@Override
protected void after() {
cleanUpInstance();
@@ -138,7 +143,7 @@ private void cleanUpInstance() {
}
}
- private void checkInitialized() {
+ void checkInitialized() {
checkState(testHelper != null, "Setup has not completed successfully");
}
}
diff --git a/google-cloud-clients/google-cloud-spanner/src/test/java/com/google/cloud/spanner/IntegrationTestWithClosedSessionsEnv.java b/google-cloud-clients/google-cloud-spanner/src/test/java/com/google/cloud/spanner/IntegrationTestWithClosedSessionsEnv.java
new file mode 100644
index 000000000000..66e0893be7b0
--- /dev/null
+++ b/google-cloud-clients/google-cloud-spanner/src/test/java/com/google/cloud/spanner/IntegrationTestWithClosedSessionsEnv.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2019 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 com.google.cloud.spanner.SessionPool.PooledSession;
+import com.google.cloud.spanner.testing.RemoteSpannerHelper;
+
+/**
+ * Subclass of {@link IntegrationTestEnv} that allows the user to specify when the underlying
+ * session of a {@link PooledSession} should be closed. This can be used to ensure that the
+ * recreation of sessions that have been invalidated by the server works.
+ */
+public class IntegrationTestWithClosedSessionsEnv extends IntegrationTestEnv {
+ private static class RemoteSpannerHelperWithClosedSessions extends RemoteSpannerHelper {
+ private RemoteSpannerHelperWithClosedSessions(
+ SpannerOptions options, InstanceId instanceId, Spanner client) {
+ super(options, instanceId, client);
+ }
+ }
+
+ @Override
+ RemoteSpannerHelper createTestHelper(SpannerOptions options, InstanceId instanceId)
+ throws Throwable {
+ SpannerWithClosedSessionsImpl spanner = new SpannerWithClosedSessionsImpl(options);
+ return new RemoteSpannerHelperWithClosedSessions(options, instanceId, spanner);
+ }
+
+ private static class SpannerWithClosedSessionsImpl extends SpannerImpl {
+ SpannerWithClosedSessionsImpl(SpannerOptions options) {
+ super(options);
+ }
+
+ @Override
+ DatabaseClientImpl createDatabaseClient(SessionPool pool) {
+ return new DatabaseClientWithClosedSessionImpl(pool);
+ }
+ }
+
+ /**
+ * {@link DatabaseClient} that allows the user to specify when an underlying session of a {@link
+ * PooledSession} should be closed.
+ */
+ public static class DatabaseClientWithClosedSessionImpl extends DatabaseClientImpl {
+ private boolean invalidateNextSession = false;
+ private boolean allowReplacing = true;
+
+ DatabaseClientWithClosedSessionImpl(SessionPool pool) {
+ super(pool);
+ }
+
+ /** Invalidate the next session that is checked out from the pool. */
+ public void invalidateNextSession() {
+ invalidateNextSession = true;
+ }
+
+ /** Sets whether invalidated sessions should be replaced or not. */
+ public void setAllowSessionReplacing(boolean allow) {
+ this.allowReplacing = allow;
+ }
+
+ @Override
+ PooledSession getReadSession() {
+ PooledSession session = super.getReadSession();
+ if (invalidateNextSession) {
+ session.delegate.close();
+ session.setAllowReplacing(false);
+ awaitDeleted(session.delegate);
+ session.setAllowReplacing(allowReplacing);
+ invalidateNextSession = false;
+ }
+ session.setAllowReplacing(allowReplacing);
+ return session;
+ }
+
+ @Override
+ PooledSession getReadWriteSession() {
+ PooledSession session = super.getReadWriteSession();
+ if (invalidateNextSession) {
+ session.delegate.close();
+ session.setAllowReplacing(false);
+ awaitDeleted(session.delegate);
+ session.setAllowReplacing(allowReplacing);
+ invalidateNextSession = false;
+ }
+ session.setAllowReplacing(allowReplacing);
+ return session;
+ }
+
+ /**
+ * Deleting a session server side takes some time. This method checks and waits until the
+ * session really has been deleted.
+ */
+ private void awaitDeleted(Session session) {
+ // Wait until the session has actually been deleted.
+ while (true) {
+ try (ResultSet rs = session.singleUse().executeQuery(Statement.of("SELECT 1"))) {
+ while (rs.next()) {
+ // Do nothing.
+ }
+ Thread.sleep(500L);
+ } catch (SpannerException e) {
+ if (e.getErrorCode() == ErrorCode.NOT_FOUND
+ && e.getMessage().contains("Session not found")) {
+ break;
+ } else {
+ throw e;
+ }
+ } catch (InterruptedException e) {
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/google-cloud-clients/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java b/google-cloud-clients/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java
index 8564b51b66cd..8cd369c6c687 100644
--- a/google-cloud-clients/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java
+++ b/google-cloud-clients/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java
@@ -1248,7 +1248,6 @@ public void commit(CommitRequest request, StreamObserver respons
.asRuntimeException());
return;
}
-
if (transaction == null) {
setTransactionNotFound(request.getTransactionId(), responseObserver);
return;
diff --git a/google-cloud-clients/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RetryOnInvalidatedSessionTest.java b/google-cloud-clients/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RetryOnInvalidatedSessionTest.java
new file mode 100644
index 000000000000..ac14701e84d0
--- /dev/null
+++ b/google-cloud-clients/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RetryOnInvalidatedSessionTest.java
@@ -0,0 +1,1411 @@
+/*
+ * Copyright 2019 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 org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.api.gax.core.NoCredentialsProvider;
+import com.google.api.gax.grpc.testing.LocalChannelProvider;
+import com.google.cloud.NoCredentials;
+import com.google.cloud.Timestamp;
+import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult;
+import com.google.cloud.spanner.TransactionRunner.TransactionCallable;
+import com.google.cloud.spanner.v1.SpannerClient;
+import com.google.cloud.spanner.v1.SpannerClient.ListSessionsPagedResponse;
+import com.google.cloud.spanner.v1.SpannerSettings;
+import com.google.common.base.Stopwatch;
+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.inprocess.InProcessServerBuilder;
+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.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+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.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class RetryOnInvalidatedSessionTest {
+ @Rule public ExpectedException expected = ExpectedException.none();
+
+ @Parameter(0)
+ public boolean failOnInvalidatedSession;
+
+ @Parameters(name = "fail on invalidated session = {0}")
+ public static Collection