diff --git a/.github/blunderbuss.yml b/.github/blunderbuss.yml
index 20fd1993c80..cd895beee2e 100644
--- a/.github/blunderbuss.yml
+++ b/.github/blunderbuss.yml
@@ -1,7 +1,7 @@
# Configuration for the Blunderbuss GitHub app. For more info see
# https://github.com/googleapis/repo-automation-bots/tree/main/packages/blunderbuss
assign_issues:
- - arpan14
+ - rahul2393
assign_prs_by:
- labels:
- samples
diff --git a/.github/workflows/unmanaged_dependency_check.yaml b/.github/workflows/unmanaged_dependency_check.yaml
index 55757cde3bc..1d7e28014c4 100644
--- a/.github/workflows/unmanaged_dependency_check.yaml
+++ b/.github/workflows/unmanaged_dependency_check.yaml
@@ -17,6 +17,6 @@ jobs:
# repository
.kokoro/build.sh
- name: Unmanaged dependency check
- uses: googleapis/sdk-platform-java/java-shared-dependencies/unmanaged-dependency-check@google-cloud-shared-dependencies/v3.28.1
+ uses: googleapis/sdk-platform-java/java-shared-dependencies/unmanaged-dependency-check@google-cloud-shared-dependencies/v3.29.0
with:
bom-path: google-cloud-spanner-bom/pom.xml
diff --git a/.kokoro/presubmit/graalvm-native-17.cfg b/.kokoro/presubmit/graalvm-native-17.cfg
index c2a88196e84..326361c6b5e 100644
--- a/.kokoro/presubmit/graalvm-native-17.cfg
+++ b/.kokoro/presubmit/graalvm-native-17.cfg
@@ -3,7 +3,7 @@
# Configure the docker image for kokoro-trampoline.
env_vars: {
key: "TRAMPOLINE_IMAGE"
- value: "gcr.io/cloud-devrel-public-resources/graalvm_sdk_platform_b:3.28.1"
+ value: "gcr.io/cloud-devrel-public-resources/graalvm_sdk_platform_b:3.29.0"
}
env_vars: {
diff --git a/.kokoro/presubmit/graalvm-native.cfg b/.kokoro/presubmit/graalvm-native.cfg
index 94e00cbaa0a..1b1d4c4bfe3 100644
--- a/.kokoro/presubmit/graalvm-native.cfg
+++ b/.kokoro/presubmit/graalvm-native.cfg
@@ -3,7 +3,7 @@
# Configure the docker image for kokoro-trampoline.
env_vars: {
key: "TRAMPOLINE_IMAGE"
- value: "gcr.io/cloud-devrel-public-resources/graalvm_sdk_platform_a:3.28.1"
+ value: "gcr.io/cloud-devrel-public-resources/graalvm_sdk_platform_a:3.29.0"
}
env_vars: {
diff --git a/.repo-metadata.json b/.repo-metadata.json
index d047e61ba9c..80355fa2aff 100644
--- a/.repo-metadata.json
+++ b/.repo-metadata.json
@@ -16,6 +16,7 @@
"requires_billing": true,
"codeowner_team": "@googleapis/api-spanner-java",
"library_type": "GAPIC_COMBO",
- "excluded_poms": "google-cloud-spanner-bom"
+ "excluded_poms": "google-cloud-spanner-bom",
+ "recommended_package": "com.google.cloud.spanner"
}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6081c97954a..24f68a8a7f4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,25 @@
# Changelog
+## [6.65.0](https://github.com/googleapis/java-spanner/compare/v6.64.0...v6.65.0) (2024-04-20)
+
+
+### Features
+
+* Remove grpclb ([#2760](https://github.com/googleapis/java-spanner/issues/2760)) ([1df09d9](https://github.com/googleapis/java-spanner/commit/1df09d9b9189c5527de91189a063ecc15779ac77))
+* Support client-side hints for tags and priority ([#3005](https://github.com/googleapis/java-spanner/issues/3005)) ([48828df](https://github.com/googleapis/java-spanner/commit/48828df3489465bb53a18be50808fbd435f3e896)), closes [#2978](https://github.com/googleapis/java-spanner/issues/2978)
+
+
+### Bug Fixes
+
+* **deps:** Update the Java code generator (gapic-generator-java) to 2.39.0 ([#3001](https://github.com/googleapis/java-spanner/issues/3001)) ([6cec1bf](https://github.com/googleapis/java-spanner/commit/6cec1bf1bb44a52c62c2310447c6a068a88209ea))
+* NullPointerException on AbstractReadContext.span ([#3036](https://github.com/googleapis/java-spanner/issues/3036)) ([55732fd](https://github.com/googleapis/java-spanner/commit/55732fd107ac1d3b8c16eee198c904d54d98b2b4))
+
+
+### Dependencies
+
+* Update dependency com.google.cloud:sdk-platform-java-config to v3.29.0 ([#3045](https://github.com/googleapis/java-spanner/issues/3045)) ([67a6534](https://github.com/googleapis/java-spanner/commit/67a65346d5a01d118d5220230e3bed6db7e79a33))
+* Update dependency commons-cli:commons-cli to v1.7.0 ([#3043](https://github.com/googleapis/java-spanner/issues/3043)) ([9fea7a3](https://github.com/googleapis/java-spanner/commit/9fea7a30e90227e735ad3595f4ca58dfb1ca1b93))
+
## [6.64.0](https://github.com/googleapis/java-spanner/compare/v6.63.0...v6.64.0) (2024-04-12)
diff --git a/README.md b/README.md
index 0526b78e237..0417e7f9a4f 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@ If you are using Maven with [BOM][libraries-bom], add this to your pom.xml file:
com.google.cloudlibraries-bom
- 26.36.0
+ 26.37.0pomimport
@@ -42,7 +42,7 @@ If you are using Maven without the BOM, add this to your dependencies:
com.google.cloudgoogle-cloud-spanner
- 6.63.0
+ 6.64.0
```
@@ -57,13 +57,13 @@ implementation 'com.google.cloud:google-cloud-spanner'
If you are using Gradle without BOM, add this to your dependencies:
```Groovy
-implementation 'com.google.cloud:google-cloud-spanner:6.63.0'
+implementation 'com.google.cloud:google-cloud-spanner:6.64.0'
```
If you are using SBT, add this to your dependencies:
```Scala
-libraryDependencies += "com.google.cloud" % "google-cloud-spanner" % "6.63.0"
+libraryDependencies += "com.google.cloud" % "google-cloud-spanner" % "6.64.0"
```
@@ -650,7 +650,7 @@ Java is a registered trademark of Oracle and/or its affiliates.
[kokoro-badge-link-5]: http://storage.googleapis.com/cloud-devrel-public/java/badges/java-spanner/java11.html
[stability-image]: https://img.shields.io/badge/stability-stable-green
[maven-version-image]: https://img.shields.io/maven-central/v/com.google.cloud/google-cloud-spanner.svg
-[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-spanner/6.63.0
+[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-spanner/6.64.0
[authentication]: https://github.com/googleapis/google-cloud-java#authentication
[auth-scopes]: https://developers.google.com/identity/protocols/oauth2/scopes
[predefined-iam-roles]: https://cloud.google.com/iam/docs/understanding-roles#predefined_roles
diff --git a/google-cloud-spanner-bom/pom.xml b/google-cloud-spanner-bom/pom.xml
index 2e58b597a98..19331d33f0c 100644
--- a/google-cloud-spanner-bom/pom.xml
+++ b/google-cloud-spanner-bom/pom.xml
@@ -3,12 +3,12 @@
4.0.0com.google.cloudgoogle-cloud-spanner-bom
- 6.64.0
+ 6.65.0pomcom.google.cloudsdk-platform-java-config
- 3.28.1
+ 3.29.0Google Cloud Spanner BOM
@@ -53,43 +53,43 @@
com.google.cloudgoogle-cloud-spanner
- 6.64.0
+ 6.65.0com.google.cloudgoogle-cloud-spannertest-jar
- 6.64.0
+ 6.65.0com.google.api.grpcgrpc-google-cloud-spanner-v1
- 6.64.0
+ 6.65.0com.google.api.grpcgrpc-google-cloud-spanner-admin-instance-v1
- 6.64.0
+ 6.65.0com.google.api.grpcgrpc-google-cloud-spanner-admin-database-v1
- 6.64.0
+ 6.65.0com.google.api.grpcproto-google-cloud-spanner-admin-instance-v1
- 6.64.0
+ 6.65.0com.google.api.grpcproto-google-cloud-spanner-v1
- 6.64.0
+ 6.65.0com.google.api.grpcproto-google-cloud-spanner-admin-database-v1
- 6.64.0
+ 6.65.0
diff --git a/google-cloud-spanner-executor/pom.xml b/google-cloud-spanner-executor/pom.xml
index b61e3b65513..4cae5076d45 100644
--- a/google-cloud-spanner-executor/pom.xml
+++ b/google-cloud-spanner-executor/pom.xml
@@ -5,14 +5,14 @@
4.0.0com.google.cloudgoogle-cloud-spanner-executor
- 6.64.0
+ 6.65.0jarGoogle Cloud Spanner Executorcom.google.cloudgoogle-cloud-spanner-parent
- 6.64.0
+ 6.65.0
@@ -129,7 +129,7 @@
commons-clicommons-cli
- 1.6.0
+ 1.7.0commons-io
diff --git a/google-cloud-spanner-executor/src/main/java/com/google/cloud/spanner/executor/v1/stub/SpannerExecutorProxyStubSettings.java b/google-cloud-spanner-executor/src/main/java/com/google/cloud/spanner/executor/v1/stub/SpannerExecutorProxyStubSettings.java
index cb96087cc6c..2b8c17ada97 100644
--- a/google-cloud-spanner-executor/src/main/java/com/google/cloud/spanner/executor/v1/stub/SpannerExecutorProxyStubSettings.java
+++ b/google-cloud-spanner-executor/src/main/java/com/google/cloud/spanner/executor/v1/stub/SpannerExecutorProxyStubSettings.java
@@ -107,15 +107,6 @@ public SpannerExecutorProxyStub createStub() throws IOException {
"Transport not supported: %s", getTransportChannelProvider().getTransportName()));
}
- /** Returns the endpoint set by the user or the the service's default endpoint. */
- @Override
- public String getEndpoint() {
- if (super.getEndpoint() != null) {
- return super.getEndpoint();
- }
- return getDefaultEndpoint();
- }
-
/** Returns the default service name. */
@Override
public String getServiceName() {
@@ -273,15 +264,6 @@ public Builder applyToAllUnaryMethods(
return executeActionAsyncSettings;
}
- /** Returns the endpoint set by the user or the the service's default endpoint. */
- @Override
- public String getEndpoint() {
- if (super.getEndpoint() != null) {
- return super.getEndpoint();
- }
- return getDefaultEndpoint();
- }
-
@Override
public SpannerExecutorProxyStubSettings build() throws IOException {
return new SpannerExecutorProxyStubSettings(this);
diff --git a/google-cloud-spanner-executor/src/main/resources/META-INF/native-image/com.google.cloud.spanner.executor.v1/reflect-config.json b/google-cloud-spanner-executor/src/main/resources/META-INF/native-image/com.google.cloud.spanner.executor.v1/reflect-config.json
index 71898e0bba2..b2933abb24e 100644
--- a/google-cloud-spanner-executor/src/main/resources/META-INF/native-image/com.google.cloud.spanner.executor.v1/reflect-config.json
+++ b/google-cloud-spanner-executor/src/main/resources/META-INF/native-image/com.google.cloud.spanner.executor.v1/reflect-config.json
@@ -2708,6 +2708,15 @@
"allDeclaredClasses": true,
"allPublicClasses": true
},
+ {
+ "name": "com.google.spanner.admin.instance.v1.FulfillmentPeriod",
+ "queryAllDeclaredConstructors": true,
+ "queryAllPublicConstructors": true,
+ "queryAllDeclaredMethods": true,
+ "allPublicMethods": true,
+ "allDeclaredClasses": true,
+ "allPublicClasses": true
+ },
{
"name": "com.google.spanner.admin.instance.v1.GetInstanceConfigRequest",
"queryAllDeclaredConstructors": true,
diff --git a/google-cloud-spanner/clirr-ignored-differences.xml b/google-cloud-spanner/clirr-ignored-differences.xml
index fd0477c6716..5fea47fe9a8 100644
--- a/google-cloud-spanner/clirr-ignored-differences.xml
+++ b/google-cloud-spanner/clirr-ignored-differences.xml
@@ -649,4 +649,12 @@
com/google/cloud/spanner/connection/Connectionvoid setMaxCommitDelay(java.time.Duration)
+
+
+
+ 7012
+ com/google/cloud/spanner/connection/Connection
+ com.google.cloud.spanner.Spanner getSpanner()
+
+
diff --git a/google-cloud-spanner/pom.xml b/google-cloud-spanner/pom.xml
index b9bd156201b..0a8c69b7b1d 100644
--- a/google-cloud-spanner/pom.xml
+++ b/google-cloud-spanner/pom.xml
@@ -3,7 +3,7 @@
4.0.0com.google.cloudgoogle-cloud-spanner
- 6.64.0
+ 6.65.0jarGoogle Cloud Spannerhttps://github.com/googleapis/java-spanner
@@ -11,7 +11,7 @@
com.google.cloudgoogle-cloud-spanner-parent
- 6.64.0
+ 6.65.0google-cloud-spanner
@@ -151,7 +151,7 @@
org.apache.maven.pluginsmaven-dependency-plugin
- io.grpc:grpc-protobuf-lite,org.hamcrest:hamcrest,org.hamcrest:hamcrest-core,com.google.errorprone:error_prone_annotations,org.openjdk.jmh:jmh-generator-annprocess,com.google.api.grpc:grpc-google-cloud-spanner-v1,com.google.api.grpc:grpc-google-cloud-spanner-admin-instance-v1,com.google.api.grpc:grpc-google-cloud-spanner-admin-database-v1,javax.annotation:javax.annotation-api,io.opencensus:opencensus-impl,org.graalvm.sdk:graal-sdk,io.grpc:grpc-grpclb,io.grpc:grpc-googleapis,io.grpc:grpc-rls,com.google.api.grpc:proto-google-cloud-spanner-executor-v1,com.google.api.grpc:grpc-google-cloud-spanner-executor-v1
+ io.grpc:grpc-protobuf-lite,org.hamcrest:hamcrest,org.hamcrest:hamcrest-core,com.google.errorprone:error_prone_annotations,org.openjdk.jmh:jmh-generator-annprocess,com.google.api.grpc:grpc-google-cloud-spanner-v1,com.google.api.grpc:grpc-google-cloud-spanner-admin-instance-v1,com.google.api.grpc:grpc-google-cloud-spanner-admin-database-v1,javax.annotation:javax.annotation-api,io.opencensus:opencensus-impl,org.graalvm.sdk:graal-sdk,io.grpc:grpc-googleapis,io.grpc:grpc-rls,com.google.api.grpc:proto-google-cloud-spanner-executor-v1,com.google.api.grpc:grpc-google-cloud-spanner-executor-v1
@@ -325,11 +325,6 @@
io.grpcgrpc-alts
-
- io.grpc
- grpc-grpclb
- runtime
- io.grpcgrpc-googleapis
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbortedException.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbortedException.java
index 21b0bb2224a..03dff9f7609 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbortedException.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbortedException.java
@@ -46,4 +46,12 @@ public class AbortedException extends SpannerException {
@Nullable ApiException apiException) {
super(token, ErrorCode.ABORTED, IS_RETRYABLE, message, cause, apiException);
}
+
+ /**
+ * Returns true if this aborted exception was returned by the emulator, and was caused by another
+ * transaction already being active on the emulator.
+ */
+ public boolean isEmulatorOnlySupportsOneTransactionException() {
+ return getMessage().endsWith("The emulator only supports one transaction at a time.");
+ }
}
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 ae18a1e4f5f..d452f84e2c3 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
@@ -73,7 +73,7 @@ abstract static class Builder, T extends AbstractReadCon
private DecodeMode defaultDecodeMode = SpannerOptions.Builder.DEFAULT_DECODE_MODE;
private DirectedReadOptions defaultDirectedReadOption;
private ExecutorProvider executorProvider;
- private Clock clock = new Clock();
+ private Clock clock = Clock.INSTANCE;
Builder() {}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Clock.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Clock.java
index bb3507eeb48..4fbe841cd84 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Clock.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Clock.java
@@ -23,6 +23,10 @@
* Clock.
*/
class Clock {
+ static final Clock INSTANCE = new Clock();
+
+ Clock() {}
+
Instant instant() {
return Instant.now();
}
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 e9c9818f451..c89cce9a79e 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
@@ -21,6 +21,7 @@
import com.google.cloud.spanner.Options.TransactionOption;
import com.google.cloud.spanner.Options.UpdateOption;
import com.google.cloud.spanner.SessionPool.PooledSessionFuture;
+import com.google.cloud.spanner.SessionPool.SessionFutureWrapper;
import com.google.cloud.spanner.SpannerImpl.ClosedException;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
@@ -52,6 +53,11 @@ PooledSessionFuture getSession() {
return pool.getSession();
}
+ @VisibleForTesting
+ SessionFutureWrapper getMultiplexedSession() {
+ return pool.getMultiplexedSessionWithFallback();
+ }
+
@Override
public Dialect getDialect() {
return pool.getDialect();
@@ -123,7 +129,7 @@ public ServerStream batchWriteAtLeastOnce(
public ReadContext singleUse() {
ISpan span = tracer.spanBuilder(READ_ONLY_TRANSACTION);
try (IScope s = tracer.withSpan(span)) {
- return getSession().singleUse();
+ return getMultiplexedSession().singleUse();
} catch (RuntimeException e) {
span.setStatus(e);
span.end();
@@ -135,7 +141,7 @@ public ReadContext singleUse() {
public ReadContext singleUse(TimestampBound bound) {
ISpan span = tracer.spanBuilder(READ_ONLY_TRANSACTION);
try (IScope s = tracer.withSpan(span)) {
- return getSession().singleUse(bound);
+ return getMultiplexedSession().singleUse(bound);
} catch (RuntimeException e) {
span.setStatus(e);
span.end();
@@ -147,7 +153,7 @@ public ReadContext singleUse(TimestampBound bound) {
public ReadOnlyTransaction singleUseReadOnlyTransaction() {
ISpan span = tracer.spanBuilder(READ_ONLY_TRANSACTION);
try (IScope s = tracer.withSpan(span)) {
- return getSession().singleUseReadOnlyTransaction();
+ return getMultiplexedSession().singleUseReadOnlyTransaction();
} catch (RuntimeException e) {
span.setStatus(e);
span.end();
@@ -159,7 +165,7 @@ public ReadOnlyTransaction singleUseReadOnlyTransaction() {
public ReadOnlyTransaction singleUseReadOnlyTransaction(TimestampBound bound) {
ISpan span = tracer.spanBuilder(READ_ONLY_TRANSACTION);
try (IScope s = tracer.withSpan(span)) {
- return getSession().singleUseReadOnlyTransaction(bound);
+ return getMultiplexedSession().singleUseReadOnlyTransaction(bound);
} catch (RuntimeException e) {
span.setStatus(e);
span.end();
@@ -171,7 +177,7 @@ public ReadOnlyTransaction singleUseReadOnlyTransaction(TimestampBound bound) {
public ReadOnlyTransaction readOnlyTransaction() {
ISpan span = tracer.spanBuilder(READ_ONLY_TRANSACTION);
try (IScope s = tracer.withSpan(span)) {
- return getSession().readOnlyTransaction();
+ return getMultiplexedSession().readOnlyTransaction();
} catch (RuntimeException e) {
span.setStatus(e);
span.end();
@@ -183,7 +189,7 @@ public ReadOnlyTransaction readOnlyTransaction() {
public ReadOnlyTransaction readOnlyTransaction(TimestampBound bound) {
ISpan span = tracer.spanBuilder(READ_ONLY_TRANSACTION);
try (IScope s = tracer.withSpan(span)) {
- return getSession().readOnlyTransaction(bound);
+ return getMultiplexedSession().readOnlyTransaction(bound);
} catch (RuntimeException e) {
span.setStatus(e);
span.end();
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 161397bba00..84cba59d54f 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
@@ -30,6 +30,9 @@ class MetricRegistryConstants {
LabelKey.create("instance_id", "Name of the instance");
private static final LabelKey LIBRARY_VERSION =
LabelKey.create("library_version", "Library version");
+ static final LabelKey IS_MULTIPLEXED_KEY =
+ LabelKey.create("is_multiplexed", "Multiplexed Session");
+
private static final LabelKey SESSION_TYPE = LabelKey.create("Type", "Type of the Sessions");
/** The label value is used to represent missing value. */
@@ -62,6 +65,9 @@ class MetricRegistryConstants {
static final ImmutableList SPANNER_DEFAULT_LABEL_VALUES =
ImmutableList.of(UNSET_LABEL, UNSET_LABEL, UNSET_LABEL, UNSET_LABEL);
+ static final ImmutableList SPANNER_LABEL_KEYS_WITH_MULTIPLEXED_SESSIONS =
+ ImmutableList.of(CLIENT_ID, DATABASE, INSTANCE_ID, LIBRARY_VERSION, IS_MULTIPLEXED_KEY);
+
/** Unit to represent counts. */
static final String COUNT = "1";
@@ -80,7 +86,6 @@ class MetricRegistryConstants {
static final String NUM_SESSIONS_AVAILABLE = "spanner/num_available_sessions";
static final String SESSIONS_TYPE = "session_type";
static final String IS_MULTIPLEXED = "is_multiplexed";
-
static final String MAX_IN_USE_SESSIONS_DESCRIPTION =
"The maximum number of sessions in use during the last 10 minute interval.";
static final String MAX_ALLOWED_SESSIONS_DESCRIPTION =
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionClient.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionClient.java
index 26d6e962116..0eed13b018c 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionClient.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionClient.java
@@ -215,8 +215,10 @@ SessionImpl createSession() {
spanner.getOptions().getDatabaseRole(),
spanner.getOptions().getSessionLabels(),
options);
- return new SessionImpl(
- spanner, session.getName(), session.getCreateTime(), session.getMultiplexed(), options);
+ SessionReference sessionReference =
+ new SessionReference(
+ session.getName(), session.getCreateTime(), session.getMultiplexed(), options);
+ return new SessionImpl(spanner, sessionReference);
} catch (RuntimeException e) {
span.setStatus(e);
throw e;
@@ -248,7 +250,9 @@ void createMultiplexedSession(SessionConsumer consumer) {
true);
SessionImpl sessionImpl =
new SessionImpl(
- spanner, session.getName(), session.getCreateTime(), session.getMultiplexed(), null);
+ spanner,
+ new SessionReference(
+ session.getName(), session.getCreateTime(), session.getMultiplexed(), null));
consumer.onSessionReady(sessionImpl);
} catch (Throwable t) {
span.setStatus(t);
@@ -258,6 +262,61 @@ void createMultiplexedSession(SessionConsumer consumer) {
}
}
+ /**
+ * Create a multiplexed session asynchronously and returns it to the given {@link
+ * SessionConsumer}. A multiplexed session is not affiliated with any GRPC channel. The given
+ * {@link SessionConsumer} is guaranteed to eventually get exactly 1 multiplexed session unless an
+ * error occurs. In case of an error on the gRPC calls, the consumer will receive one {@link
+ * SessionConsumer#onSessionCreateFailure(Throwable, int)} call with the error.
+ *
+ * @param consumer The {@link SessionConsumer} to use for callbacks when sessions are available.
+ */
+ void asyncCreateMultiplexedSession(SessionConsumer consumer) {
+ try {
+ executor.submit(new CreateMultiplexedSessionsRunnable(consumer));
+ } catch (Throwable t) {
+ consumer.onSessionCreateFailure(t, 1);
+ }
+ }
+
+ private final class CreateMultiplexedSessionsRunnable implements Runnable {
+ private final SessionConsumer consumer;
+
+ private CreateMultiplexedSessionsRunnable(SessionConsumer consumer) {
+ Preconditions.checkNotNull(consumer);
+ this.consumer = consumer;
+ }
+
+ @Override
+ public void run() {
+ ISpan span = spanner.getTracer().spanBuilder(SpannerImpl.CREATE_MULTIPLEXED_SESSION);
+ try (IScope s = spanner.getTracer().withSpan(span)) {
+ com.google.spanner.v1.Session session =
+ spanner
+ .getRpc()
+ .createSession(
+ db.getName(),
+ spanner.getOptions().getDatabaseRole(),
+ spanner.getOptions().getSessionLabels(),
+ null,
+ true);
+ SessionImpl sessionImpl =
+ new SessionImpl(
+ spanner,
+ new SessionReference(
+ session.getName(), session.getCreateTime(), session.getMultiplexed(), null));
+ span.addAnnotation(
+ String.format("Request for %d multiplexed session returned %d session", 1, 1));
+ consumer.onSessionReady(sessionImpl);
+ } catch (Throwable t) {
+ span.setStatus(t);
+ consumer.onSessionCreateFailure(t, 1);
+ } finally {
+ span.end();
+ }
+ }
+ }
+
/**
* Asynchronously creates a batch of sessions and returns these to the given {@link
* SessionConsumer}. This method may split the actual session creation over several gRPC calls in
@@ -348,10 +407,11 @@ private List internalBatchCreateSessions(
res.add(
new SessionImpl(
spanner,
- session.getName(),
- session.getCreateTime(),
- session.getMultiplexed(),
- options));
+ new SessionReference(
+ session.getName(),
+ session.getCreateTime(),
+ session.getMultiplexed(),
+ options)));
}
return res;
} catch (RuntimeException e) {
@@ -367,6 +427,6 @@ SessionImpl sessionWithId(String name) {
synchronized (this) {
options = optionMap(SessionOption.channelHint(sessionChannelCounter++));
}
- return new SessionImpl(spanner, name, options);
+ return new SessionImpl(spanner, new SessionReference(name, options));
}
}
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 436c670d58c..8d7dcb34756 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
@@ -17,7 +17,6 @@
package com.google.cloud.spanner;
import static com.google.cloud.spanner.SpannerExceptionFactory.newSpannerException;
-import static com.google.common.base.Preconditions.checkNotNull;
import com.google.api.core.ApiFuture;
import com.google.api.core.SettableApiFuture;
@@ -28,7 +27,6 @@
import com.google.cloud.spanner.AbstractReadContext.SingleUseReadOnlyTransaction;
import com.google.cloud.spanner.Options.TransactionOption;
import com.google.cloud.spanner.Options.UpdateOption;
-import com.google.cloud.spanner.SessionClient.SessionId;
import com.google.cloud.spanner.TransactionRunnerImpl.TransactionContextImpl;
import com.google.cloud.spanner.spi.v1.SpannerRpc;
import com.google.common.base.Ticker;
@@ -97,49 +95,25 @@ interface SessionTransaction {
}
private final SpannerImpl spanner;
- private final String name;
- private final DatabaseId databaseId;
+ private final SessionReference sessionReference;
private SessionTransaction activeTransaction;
- private final Map options;
- private volatile Instant lastUseTime;
- @Nullable private final Instant createTime;
- private final boolean isMultiplexed;
private ISpan currentSpan;
+ private final Clock clock;
- SessionImpl(SpannerImpl spanner, String name, Map options) {
+ SessionImpl(SpannerImpl spanner, SessionReference sessionReference) {
this.spanner = spanner;
this.tracer = spanner.getTracer();
- this.options = options;
- this.name = checkNotNull(name);
- this.databaseId = SessionId.of(name).getDatabaseId();
- this.lastUseTime = Instant.now();
- this.createTime = null;
- this.isMultiplexed = false;
- }
-
- SessionImpl(
- SpannerImpl spanner,
- String name,
- com.google.protobuf.Timestamp createTime,
- boolean isMultiplexed,
- Map options) {
- this.spanner = spanner;
- this.tracer = spanner.getTracer();
- this.options = options;
- this.name = checkNotNull(name);
- this.databaseId = SessionId.of(name).getDatabaseId();
- this.lastUseTime = Instant.now();
- this.createTime = convert(createTime);
- this.isMultiplexed = isMultiplexed;
+ this.sessionReference = sessionReference;
+ this.clock = spanner.getOptions().getSessionPoolOptions().getPoolMaintainerClock();
}
@Override
public String getName() {
- return name;
+ return sessionReference.getName();
}
Map getOptions() {
- return options;
+ return sessionReference.getOptions();
}
void setCurrentSpan(ISpan span) {
@@ -151,19 +125,27 @@ ISpan getCurrentSpan() {
}
Instant getLastUseTime() {
- return lastUseTime;
+ return sessionReference.getLastUseTime();
}
Instant getCreateTime() {
- return createTime;
+ return sessionReference.getCreateTime();
}
boolean getIsMultiplexed() {
- return isMultiplexed;
+ return sessionReference.getIsMultiplexed();
+ }
+
+ SessionReference getSessionReference() {
+ return sessionReference;
}
void markUsed(Instant instant) {
- lastUseTime = instant;
+ sessionReference.markUsed(instant);
+ }
+
+ public DatabaseId getDatabaseId() {
+ return sessionReference.getDatabaseId();
}
@Override
@@ -211,7 +193,7 @@ public CommitResponse writeAtLeastOnceWithOptions(
Options options = Options.fromTransactionOptions(transactionOptions);
final CommitRequest.Builder requestBuilder =
CommitRequest.newBuilder()
- .setSession(name)
+ .setSession(getName())
.setReturnCommitStats(options.withCommitStats())
.addAllMutations(mutationsProto);
@@ -239,7 +221,7 @@ public CommitResponse writeAtLeastOnceWithOptions(
ISpan span = tracer.spanBuilder(SpannerImpl.COMMIT);
try (IScope s = tracer.withSpan(span)) {
return SpannerRetryHelper.runTxWithRetriesOnAborted(
- () -> new CommitResponse(spanner.getRpc().commit(request, this.options)));
+ () -> new CommitResponse(spanner.getRpc().commit(request, getOptions())));
} catch (RuntimeException e) {
span.setStatus(e);
throw e;
@@ -271,7 +253,9 @@ public ServerStream batchWriteAtLeastOnce(
List mutationGroupsProto =
MutationGroup.toListProto(mutationGroups);
final BatchWriteRequest.Builder requestBuilder =
- BatchWriteRequest.newBuilder().setSession(name).addAllMutationGroups(mutationGroupsProto);
+ BatchWriteRequest.newBuilder()
+ .setSession(getName())
+ .addAllMutationGroups(mutationGroupsProto);
RequestOptions batchWriteRequestOptions = getRequestOptions(transactionOptions);
if (batchWriteRequestOptions != null) {
requestBuilder.setRequestOptions(batchWriteRequestOptions);
@@ -282,7 +266,7 @@ public ServerStream batchWriteAtLeastOnce(
}
ISpan span = tracer.spanBuilder(SpannerImpl.BATCH_WRITE);
try (IScope s = tracer.withSpan(span)) {
- return spanner.getRpc().batchWriteAtLeastOnce(requestBuilder.build(), this.options);
+ return spanner.getRpc().batchWriteAtLeastOnce(requestBuilder.build(), getOptions());
} catch (Throwable e) {
span.setStatus(e);
throw SpannerExceptionFactory.newSpannerException(e);
@@ -303,13 +287,14 @@ public ReadContext singleUse(TimestampBound bound) {
.setSession(this)
.setTimestampBound(bound)
.setRpc(spanner.getRpc())
- .setDefaultQueryOptions(spanner.getDefaultQueryOptions(databaseId))
+ .setDefaultQueryOptions(spanner.getDefaultQueryOptions(getDatabaseId()))
.setDefaultPrefetchChunks(spanner.getDefaultPrefetchChunks())
.setDefaultDecodeMode(spanner.getDefaultDecodeMode())
.setDefaultDirectedReadOptions(spanner.getOptions().getDirectedReadOptions())
.setSpan(currentSpan)
.setTracer(tracer)
.setExecutorProvider(spanner.getAsyncExecutorProvider())
+ .setClock(clock)
.build());
}
@@ -325,13 +310,14 @@ public ReadOnlyTransaction singleUseReadOnlyTransaction(TimestampBound bound) {
.setSession(this)
.setTimestampBound(bound)
.setRpc(spanner.getRpc())
- .setDefaultQueryOptions(spanner.getDefaultQueryOptions(databaseId))
+ .setDefaultQueryOptions(spanner.getDefaultQueryOptions(getDatabaseId()))
.setDefaultPrefetchChunks(spanner.getDefaultPrefetchChunks())
.setDefaultDecodeMode(spanner.getDefaultDecodeMode())
.setDefaultDirectedReadOptions(spanner.getOptions().getDirectedReadOptions())
.setSpan(currentSpan)
.setTracer(tracer)
.setExecutorProvider(spanner.getAsyncExecutorProvider())
+ .setClock(clock)
.buildSingleUseReadOnlyTransaction());
}
@@ -347,13 +333,14 @@ public ReadOnlyTransaction readOnlyTransaction(TimestampBound bound) {
.setSession(this)
.setTimestampBound(bound)
.setRpc(spanner.getRpc())
- .setDefaultQueryOptions(spanner.getDefaultQueryOptions(databaseId))
+ .setDefaultQueryOptions(spanner.getDefaultQueryOptions(getDatabaseId()))
.setDefaultPrefetchChunks(spanner.getDefaultPrefetchChunks())
.setDefaultDecodeMode(spanner.getDefaultDecodeMode())
.setDefaultDirectedReadOptions(spanner.getOptions().getDirectedReadOptions())
.setSpan(currentSpan)
.setTracer(tracer)
.setExecutorProvider(spanner.getAsyncExecutorProvider())
+ .setClock(clock)
.build());
}
@@ -379,14 +366,14 @@ public AsyncTransactionManagerImpl transactionManagerAsync(TransactionOption...
@Override
public ApiFuture asyncClose() {
- return spanner.getRpc().asyncDeleteSession(name, options);
+ return spanner.getRpc().asyncDeleteSession(getName(), getOptions());
}
@Override
public void close() {
ISpan span = tracer.spanBuilder(SpannerImpl.DELETE_SESSION);
try (IScope s = tracer.withSpan(span)) {
- spanner.getRpc().deleteSession(name, options);
+ spanner.getRpc().deleteSession(getName(), getOptions());
} catch (RuntimeException e) {
span.setStatus(e);
throw e;
@@ -400,11 +387,11 @@ ApiFuture beginTransactionAsync(Options transactionOptions, boolean
final ISpan span = tracer.spanBuilder(SpannerImpl.BEGIN_TRANSACTION);
final BeginTransactionRequest request =
BeginTransactionRequest.newBuilder()
- .setSession(name)
+ .setSession(getName())
.setOptions(createReadWriteTransactionOptions(transactionOptions))
.build();
final ApiFuture requestFuture =
- spanner.getRpc().beginTransactionAsync(request, options, routeToLeader);
+ spanner.getRpc().beginTransactionAsync(request, getOptions(), routeToLeader);
requestFuture.addListener(
() -> {
try (IScope s = tracer.withSpan(span)) {
@@ -436,9 +423,6 @@ ApiFuture beginTransactionAsync(Options transactionOptions, boolean
}
TransactionContextImpl newTransaction(Options options) {
- // A clock instance is passed in {@code SessionPoolOptions} in order to allow mocking via tests.
- final Clock poolMaintainerClock =
- spanner.getOptions().getSessionPoolOptions().getPoolMaintainerClock();
return TransactionContextImpl.newBuilder()
.setSession(this)
.setOptions(options)
@@ -446,21 +430,23 @@ TransactionContextImpl newTransaction(Options options) {
.setOptions(options)
.setTrackTransactionStarter(spanner.getOptions().isTrackTransactionStarter())
.setRpc(spanner.getRpc())
- .setDefaultQueryOptions(spanner.getDefaultQueryOptions(databaseId))
+ .setDefaultQueryOptions(spanner.getDefaultQueryOptions(getDatabaseId()))
.setDefaultPrefetchChunks(spanner.getDefaultPrefetchChunks())
.setDefaultDecodeMode(spanner.getDefaultDecodeMode())
.setSpan(currentSpan)
.setTracer(tracer)
.setExecutorProvider(spanner.getAsyncExecutorProvider())
- .setClock(poolMaintainerClock == null ? new Clock() : poolMaintainerClock)
+ .setClock(clock)
.build();
}
T setActive(@Nullable T ctx) {
throwIfTransactionsPending();
-
- if (activeTransaction != null) {
- activeTransaction.invalidate();
+ // multiplexed sessions support running concurrent transactions
+ if (!getIsMultiplexed()) {
+ if (activeTransaction != null) {
+ activeTransaction.invalidate();
+ }
}
activeTransaction = ctx;
if (activeTransaction != null) {
@@ -472,11 +458,4 @@ T setActive(@Nullable T ctx) {
TraceWrapper getTracer() {
return tracer;
}
-
- private Instant convert(com.google.protobuf.Timestamp timestamp) {
- if (timestamp == null) {
- return null;
- }
- return Instant.ofEpochSecond(timestamp.getSeconds(), timestamp.getNanos());
- }
}
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 f4c04e4c78b..ba0031e587a 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
@@ -40,7 +40,9 @@
import static com.google.cloud.spanner.MetricRegistryConstants.SESSIONS_TYPE;
import static com.google.cloud.spanner.MetricRegistryConstants.SPANNER_DEFAULT_LABEL_VALUES;
import static com.google.cloud.spanner.MetricRegistryConstants.SPANNER_LABEL_KEYS;
+import static com.google.cloud.spanner.MetricRegistryConstants.SPANNER_LABEL_KEYS_WITH_MULTIPLEXED_SESSIONS;
import static com.google.cloud.spanner.MetricRegistryConstants.SPANNER_LABEL_KEYS_WITH_TYPE;
+import static com.google.cloud.spanner.SpannerExceptionFactory.asSpannerException;
import static com.google.cloud.spanner.SpannerExceptionFactory.newSpannerException;
import static com.google.common.base.Preconditions.checkState;
@@ -141,6 +143,14 @@ void maybeWaitOnMinSessions() {
ErrorCode.DEADLINE_EXCEEDED,
"Timed out after waiting " + timeoutMillis + "ms for session pool creation");
}
+
+ if (options.getUseMultiplexedSession()
+ && !waitOnMultiplexedSessionsLatch.await(timeoutNanos, TimeUnit.NANOSECONDS)) {
+ final long timeoutMillis = options.getWaitForMinSessions().toMillis();
+ throw SpannerExceptionFactory.newSpannerException(
+ ErrorCode.DEADLINE_EXCEEDED,
+ "Timed out after waiting " + timeoutMillis + "ms for multiplexed session creation");
+ }
} catch (InterruptedException e) {
throw SpannerExceptionFactory.propagateInterrupt(e);
}
@@ -1074,7 +1084,7 @@ public ApiFuture runAsync(final AsyncWork work, Executor executor) {
r = runner.runAsync(work, MoreExecutors.directExecutor()).get();
break;
} catch (ExecutionException e) {
- se = SpannerExceptionFactory.asSpannerException(e.getCause());
+ se = asSpannerException(e.getCause());
} catch (InterruptedException e) {
se = SpannerExceptionFactory.propagateInterrupt(e);
} catch (Throwable t) {
@@ -1158,10 +1168,86 @@ private PooledSessionFuture createPooledSessionFuture(
}
/** Wrapper class for the {@link SessionFuture} implementations. */
- interface SessionFutureWrapper {
+ interface SessionFutureWrapper extends DatabaseClient {
/** Method to resolve {@link SessionFuture} implementation for different use-cases. */
T get();
+
+ default Dialect getDialect() {
+ return get().getDialect();
+ }
+
+ default String getDatabaseRole() {
+ return get().getDatabaseRole();
+ }
+
+ default Timestamp write(Iterable mutations) throws SpannerException {
+ return get().write(mutations);
+ }
+
+ default CommitResponse writeWithOptions(
+ Iterable mutations, TransactionOption... options) throws SpannerException {
+ return get().writeWithOptions(mutations, options);
+ }
+
+ default Timestamp writeAtLeastOnce(Iterable mutations) throws SpannerException {
+ return get().writeAtLeastOnce(mutations);
+ }
+
+ default CommitResponse writeAtLeastOnceWithOptions(
+ Iterable mutations, TransactionOption... options) throws SpannerException {
+ return get().writeAtLeastOnceWithOptions(mutations, options);
+ }
+
+ default ServerStream batchWriteAtLeastOnce(
+ Iterable mutationGroups, TransactionOption... options)
+ throws SpannerException {
+ return get().batchWriteAtLeastOnce(mutationGroups, options);
+ }
+
+ default ReadContext singleUse() {
+ return get().singleUse();
+ }
+
+ default ReadContext singleUse(TimestampBound bound) {
+ return get().singleUse(bound);
+ }
+
+ default ReadOnlyTransaction singleUseReadOnlyTransaction() {
+ return get().singleUseReadOnlyTransaction();
+ }
+
+ default ReadOnlyTransaction singleUseReadOnlyTransaction(TimestampBound bound) {
+ return get().singleUseReadOnlyTransaction(bound);
+ }
+
+ default ReadOnlyTransaction readOnlyTransaction() {
+ return get().readOnlyTransaction();
+ }
+
+ default ReadOnlyTransaction readOnlyTransaction(TimestampBound bound) {
+ return get().readOnlyTransaction(bound);
+ }
+
+ default TransactionRunner readWriteTransaction(TransactionOption... options) {
+ return get().readWriteTransaction(options);
+ }
+
+ default TransactionManager transactionManager(TransactionOption... options) {
+ return get().transactionManager(options);
+ }
+
+ default AsyncRunner runAsync(TransactionOption... options) {
+ return get().runAsync(options);
+ }
+
+ default AsyncTransactionManager transactionManagerAsync(TransactionOption... options) {
+ return get().transactionManagerAsync(options);
+ }
+
+ default long executePartitionedUpdate(Statement stmt, UpdateOption... options) {
+ return get().executePartitionedUpdate(stmt, options);
+ }
}
class PooledSessionFutureWrapper implements SessionFutureWrapper {
@@ -1178,22 +1264,35 @@ public PooledSessionFuture get() {
}
class MultiplexedSessionFutureWrapper implements SessionFutureWrapper {
- SettableApiFuture multiplexedSessionSettableApiFuture;
+ private ISpan span;
+ private volatile MultiplexedSessionFuture multiplexedSessionFuture;
- public MultiplexedSessionFutureWrapper(
- SettableApiFuture multiplexedSessionSettableApiFuture) {
- this.multiplexedSessionSettableApiFuture = multiplexedSessionSettableApiFuture;
+ public MultiplexedSessionFutureWrapper(ISpan span) {
+ this.span = span;
}
@Override
public MultiplexedSessionFuture get() {
- try {
- return this.multiplexedSessionSettableApiFuture.get();
- } catch (InterruptedException interruptedException) {
- throw SpannerExceptionFactory.propagateInterrupt(interruptedException);
- } catch (ExecutionException executionException) {
- throw SpannerExceptionFactory.asSpannerException(executionException.getCause());
+ 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);
}
+ if (multiplexedSessionFuture == null) {
+ synchronized (lock) {
+ if (multiplexedSessionFuture == null) {
+ // Creating a new reference where the request's span state can be stored.
+ MultiplexedSessionFuture multiplexedSessionFuture = new MultiplexedSessionFuture(span);
+ this.multiplexedSessionFuture = multiplexedSessionFuture;
+ return multiplexedSessionFuture;
+ }
+ }
+ }
+ return multiplexedSessionFuture;
}
}
@@ -1412,7 +1511,7 @@ public void close() {
} catch (InterruptedException e) {
throw SpannerExceptionFactory.propagateInterrupt(e);
} catch (ExecutionException e) {
- throw SpannerExceptionFactory.asSpannerException(e.getCause());
+ throw asSpannerException(e.getCause());
}
}
@@ -1475,11 +1574,13 @@ PooledSession get(final boolean eligibleForLongRunning) {
}
}
- class MultiplexedSessionFuture extends SimpleForwardingListenableFuture
- implements SessionFuture {
- @VisibleForTesting
- MultiplexedSessionFuture(ListenableFuture delegate) {
- super(delegate);
+ class MultiplexedSessionFuture implements SessionFuture {
+
+ private final ISpan span;
+ private volatile MultiplexedSession multiplexedSession;
+
+ MultiplexedSessionFuture(ISpan span) {
+ this.span = span;
}
@Override
@@ -1659,7 +1760,7 @@ public void close() {
} catch (InterruptedException e) {
throw SpannerExceptionFactory.propagateInterrupt(e);
} catch (ExecutionException e) {
- throw SpannerExceptionFactory.asSpannerException(e.getCause());
+ throw asSpannerException(e.getCause());
}
}
@@ -1685,7 +1786,27 @@ private MultiplexedSession getOrNull() {
@Override
public MultiplexedSession get() {
try {
- return super.get();
+ if (multiplexedSession == null) {
+ boolean created = false;
+ synchronized (this) {
+ if (multiplexedSession == null) {
+ SessionImpl sessionImpl =
+ new SessionImpl(
+ sessionClient.getSpanner(), currentMultiplexedSessionReference.get().get());
+ MultiplexedSession multiplexedSession = new MultiplexedSession(sessionImpl);
+ multiplexedSession.markBusy(span);
+ span.addAnnotation("Using Session", "sessionId", multiplexedSession.getName());
+ this.multiplexedSession = multiplexedSession;
+ created = true;
+ }
+ }
+ if (created) {
+ synchronized (lock) {
+ incrementNumSessionsInUse(true);
+ }
+ }
+ }
+ return multiplexedSession;
} catch (ExecutionException e) {
throw SpannerExceptionFactory.newSpannerException(e.getCause());
} catch (InterruptedException e) {
@@ -1698,7 +1819,6 @@ interface CachedSession extends Session {
SessionImpl getDelegate();
- // TODO This method can be removed once we fully migrate to multiplexed sessions.
void markBusy(ISpan span);
void markUsed();
@@ -2023,8 +2143,7 @@ public void setAllowReplacing(boolean allowReplacing) {
@Override
public void markBusy(ISpan span) {
- // no-op for a multiplexed session since a new span is already created and set in context
- // for every handler invocation.
+ this.delegate.setCurrentSpan(span);
}
@Override
@@ -2223,12 +2342,20 @@ private PooledSession pollUninterruptiblyWithTimeout(
interrupted = true;
} catch (TimeoutException e) {
if (acquireSessionTimeout != null) {
- throw SpannerExceptionFactory.newSpannerException(
- ErrorCode.RESOURCE_EXHAUSTED,
- "Timed out after waiting "
- + acquireSessionTimeout.toMillis()
- + "ms for acquiring session. To mitigate error SessionPoolOptions#setAcquireSessionTimeout(Duration) to set a higher timeout"
- + " or increase the number of sessions in the session pool.");
+ SpannerException exception =
+ SpannerExceptionFactory.newSpannerException(
+ ErrorCode.RESOURCE_EXHAUSTED,
+ "Timed out after waiting "
+ + acquireSessionTimeout.toMillis()
+ + "ms for acquiring session. To mitigate error SessionPoolOptions#setAcquireSessionTimeout(Duration) to set a higher timeout"
+ + " or increase the number of sessions in the session pool.");
+ if (waiter.setException(exception)) {
+ // Only throw the exception if setting it on the waiter was successful. The
+ // waiter.setException(..) method returns false if some other thread in the meantime
+ // called waiter.set(..), which means that a session became available between the
+ // time that the TimeoutException was thrown and now.
+ throw exception;
+ }
}
return null;
} catch (ExecutionException e) {
@@ -2523,26 +2650,27 @@ void maintainMultiplexedSession(Instant currentTime) {
try {
if (options.getUseMultiplexedSession()) {
synchronized (lock) {
- if (getMultiplexedSession().isDone()
- && getMultiplexedSession().get() != null
- && isMultiplexedSessionStale(currentTime)) {
- final Instant minExecutionTime =
- multiplexedSessionReplacementAttemptTime.plus(
- multiplexedSessionCreationRetryDelay);
- if (currentTime.isBefore(minExecutionTime)) {
- return;
+ if (currentMultiplexedSessionReference.get().isDone()) {
+ SessionReference sessionReference = getMultiplexedSessionInstance();
+ if (sessionReference != null
+ && isMultiplexedSessionStale(sessionReference, currentTime)) {
+ final Instant minExecutionTime =
+ multiplexedSessionReplacementAttemptTime.plus(
+ multiplexedSessionCreationRetryDelay);
+ if (currentTime.isBefore(minExecutionTime)) {
+ return;
+ }
+ /*
+ This will attempt to create a new multiplexed session. if successfully created then
+ the existing session will be replaced. Note that there maybe active transactions
+ running on the stale session. Hence, it is important that we only replace the reference
+ and not invoke a DeleteSession RPC.
+ */
+ maybeCreateMultiplexedSession(multiplexedMaintainerConsumer);
+
+ // update this only after we have attempted to replace the multiplexed session
+ multiplexedSessionReplacementAttemptTime = currentTime;
}
-
- /*
- This will attempt to create a new multiplexed session. if successfully created then
- the existing session will be replaced. Note that there maybe active transactions
- running on the stale session. Hence, it is important that we only replace the reference
- and not invoke a DeleteSession RPC.
- */
- maybeCreateMultiplexedSession(multiplexedMaintainerConsumer);
-
- // update this only after we have attempted to replace the multiplexed session
- multiplexedSessionReplacementAttemptTime = currentTime;
}
}
}
@@ -2551,10 +2679,9 @@ && isMultiplexedSessionStale(currentTime)) {
}
}
- boolean isMultiplexedSessionStale(Instant currentTime) {
- final CachedSession session = getMultiplexedSession().get();
+ boolean isMultiplexedSessionStale(SessionReference sessionReference, Instant currentTime) {
final Duration durationFromCreationTime =
- Duration.between(session.getDelegate().getCreateTime(), currentTime);
+ Duration.between(sessionReference.getCreateTime(), currentTime);
return durationFromCreationTime.compareTo(options.getMultiplexedSessionMaintenanceDuration())
> 0;
}
@@ -2666,9 +2793,8 @@ enum Position {
private AtomicLong numWaiterTimeouts = new AtomicLong();
- private final AtomicReference>
+ private final AtomicReference>
currentMultiplexedSessionReference = new AtomicReference<>(SettableApiFuture.create());
- MultiplexedSessionFutureWrapper wrappedMultiplexedSessionFuture = null;
@GuardedBy("lock")
private final Set allSessions = new HashSet<>();
@@ -2687,8 +2813,9 @@ enum Position {
@VisibleForTesting Function idleSessionRemovedListener;
@VisibleForTesting Function longRunningSessionRemovedListener;
- @VisibleForTesting Function multiplexedSessionRemovedListener;
+ @VisibleForTesting Function multiplexedSessionRemovedListener;
private final CountDownLatch waitOnMinSessionsLatch;
+ private final CountDownLatch waitOnMultiplexedSessionsLatch;
private final SessionReplacementHandler pooledSessionReplacementHandler =
new PooledSessionReplacementHandler();
private static final SessionReplacementHandler multiplexedSessionReplacementHandler =
@@ -2819,6 +2946,7 @@ private SessionPool(
this.initOpenTelemetryMetricsCollection(openTelemetry, attributes);
this.waitOnMinSessionsLatch =
options.getMinSessions() > 0 ? new CountDownLatch(1) : new CountDownLatch(0);
+ this.waitOnMultiplexedSessionsLatch = new CountDownLatch(1);
}
/**
@@ -2846,7 +2974,7 @@ Dialect getDialect() {
try {
return dialect.get(60L, TimeUnit.SECONDS);
} catch (ExecutionException executionException) {
- throw SpannerExceptionFactory.asSpannerException(executionException);
+ throw asSpannerException(executionException);
} catch (InterruptedException interruptedException) {
throw SpannerExceptionFactory.propagateInterrupt(interruptedException);
} catch (TimeoutException timeoutException) {
@@ -2920,6 +3048,20 @@ int getNumberOfSessionsBeingCreated() {
}
}
+ @VisibleForTesting
+ int getTotalSessionsPlusNumSessionsBeingCreated() {
+ synchronized (lock) {
+ return numSessionsBeingCreated + allSessions.size();
+ }
+ }
+
+ @VisibleForTesting
+ boolean isMultiplexedSessionBeingCreated() {
+ synchronized (lock) {
+ return multiplexedSessionBeingCreated;
+ }
+ }
+
@VisibleForTesting
long getNumWaiterTimeouts() {
return numWaiterTimeouts.get();
@@ -3001,30 +3143,34 @@ boolean isValid() {
*/
SessionFutureWrapper getMultiplexedSessionWithFallback() throws SpannerException {
if (options.getUseMultiplexedSession()) {
+ ISpan span = tracer.getCurrentSpan();
try {
- SessionFutureWrapper sessionFuture = getWrappedMultiplexedSessionFuture();
- incrementNumSessionsInUse(true);
- return sessionFuture;
+ return getWrappedMultiplexedSessionFuture(span);
} catch (Throwable t) {
- ISpan span = tracer.getCurrentSpan();
span.addAnnotation("No multiplexed session available.");
- throw SpannerExceptionFactory.asSpannerException(t.getCause());
+ throw asSpannerException(t.getCause());
}
} else {
return new PooledSessionFutureWrapper(getSession());
}
}
- SessionFutureWrapper getWrappedMultiplexedSessionFuture() {
- return wrappedMultiplexedSessionFuture;
+ SessionFutureWrapper getWrappedMultiplexedSessionFuture(ISpan span) {
+ return new MultiplexedSessionFutureWrapper(span);
}
/**
* This method is a blocking method. It will block until the underlying {@code
- * SettableApiFuture} is resolved.
+ * SettableApiFuture} is resolved.
*/
- MultiplexedSessionFuture getMultiplexedSession() {
- return (MultiplexedSessionFuture) getWrappedMultiplexedSessionFuture().get();
+ SessionReference getMultiplexedSessionInstance() {
+ try {
+ return currentMultiplexedSessionReference.get().get();
+ } catch (InterruptedException e) {
+ throw SpannerExceptionFactory.propagateInterrupt(e);
+ } catch (ExecutionException e) {
+ throw asSpannerException(e.getCause());
+ }
}
/**
@@ -3436,7 +3582,7 @@ private void maybeCreateMultiplexedSession(SessionConsumer sessionConsumer) {
logger.log(Level.FINE, String.format("Creating multiplexed sessions"));
try {
multiplexedSessionBeingCreated = true;
- sessionClient.createMultiplexedSession(sessionConsumer);
+ sessionClient.asyncCreateMultiplexedSession(sessionConsumer);
} catch (Throwable ignore) {
// such an exception will never be thrown. the exception will be passed onto the consumer.
}
@@ -3480,25 +3626,24 @@ private void createSessions(final int sessionCount, boolean distributeOverChanne
class MultiplexedSessionMaintainerConsumer implements SessionConsumer {
@Override
public void onSessionReady(SessionImpl sessionImpl) {
- final SettableFuture settableFuture = SettableFuture.create();
- final MultiplexedSession newSession = new MultiplexedSession(sessionImpl);
- settableFuture.set(newSession);
+ final SessionReference sessionReference = sessionImpl.getSessionReference();
+ final SettableFuture settableFuture = SettableFuture.create();
+ settableFuture.set(sessionReference);
synchronized (lock) {
- MultiplexedSession oldSession = null;
+ SessionReference oldSession = null;
if (currentMultiplexedSessionReference.get().isDone()) {
- oldSession = getMultiplexedSession().get();
+ oldSession = getMultiplexedSessionInstance();
}
- SettableApiFuture settableApiFuture = SettableApiFuture.create();
- settableApiFuture.set(new MultiplexedSessionFuture(settableFuture));
+ SettableApiFuture settableApiFuture = SettableApiFuture.create();
+ settableApiFuture.set(sessionReference);
currentMultiplexedSessionReference.set(settableApiFuture);
- wrappedMultiplexedSessionFuture = new MultiplexedSessionFutureWrapper(settableApiFuture);
if (oldSession != null) {
logger.log(
Level.INFO,
String.format(
- "Removed Multiplexed Session => %s created at => %s and",
- oldSession.getName(), oldSession.getDelegate().getCreateTime()));
+ "Removed Multiplexed Session => %s created at => %s",
+ oldSession.getName(), oldSession.getCreateTime()));
if (multiplexedSessionRemovedListener != null) {
multiplexedSessionRemovedListener.apply(oldSession);
}
@@ -3515,8 +3660,6 @@ public void onSessionReady(SessionImpl sessionImpl) {
public void onSessionCreateFailure(Throwable t, int createFailureForSessionCount) {
synchronized (lock) {
multiplexedSessionBeingCreated = false;
- wrappedMultiplexedSessionFuture =
- new MultiplexedSessionFutureWrapper(currentMultiplexedSessionReference.get());
}
logger.log(
Level.WARNING,
@@ -3534,16 +3677,13 @@ public void onSessionCreateFailure(Throwable t, int createFailureForSessionCount
class MultiplexedSessionInitializationConsumer implements SessionConsumer {
@Override
public void onSessionReady(SessionImpl sessionImpl) {
- final SettableFuture settableFuture = SettableFuture.create();
- final MultiplexedSession newSession = new MultiplexedSession(sessionImpl);
- settableFuture.set(newSession);
-
+ final SessionReference sessionReference = sessionImpl.getSessionReference();
synchronized (lock) {
- SettableApiFuture settableApiFuture =
+ SettableApiFuture settableApiFuture =
currentMultiplexedSessionReference.get();
- settableApiFuture.set(new MultiplexedSessionFuture(settableFuture));
- wrappedMultiplexedSessionFuture = new MultiplexedSessionFutureWrapper(settableApiFuture);
+ settableApiFuture.set(sessionReference);
multiplexedSessionBeingCreated = false;
+ waitOnMultiplexedSessionsLatch.countDown();
}
}
@@ -3556,9 +3696,11 @@ public void onSessionReady(SessionImpl sessionImpl) {
public void onSessionCreateFailure(Throwable t, int createFailureForSessionCount) {
synchronized (lock) {
multiplexedSessionBeingCreated = false;
- wrappedMultiplexedSessionFuture =
- new MultiplexedSessionFutureWrapper(currentMultiplexedSessionReference.get());
- currentMultiplexedSessionReference.get().setException(newSpannerException(t));
+ if (isDatabaseOrInstanceNotFound(asSpannerException(t))) {
+ setResourceNotFoundException((ResourceNotFoundException) t);
+ poolMaintainer.close();
+ }
+ currentMultiplexedSessionReference.get().setException(asSpannerException(t));
}
}
}
@@ -3662,7 +3804,7 @@ private void initOpenCensusMetricsCollection(
MetricOptions.builder()
.setDescription(NUM_ACQUIRED_SESSIONS_DESCRIPTION)
.setUnit(COUNT)
- .setLabelKeys(SPANNER_LABEL_KEYS)
+ .setLabelKeys(SPANNER_LABEL_KEYS_WITH_MULTIPLEXED_SESSIONS)
.build());
DerivedLongCumulative numReleasedSessionsMetric =
@@ -3671,7 +3813,7 @@ private void initOpenCensusMetricsCollection(
MetricOptions.builder()
.setDescription(NUM_RELEASED_SESSIONS_DESCRIPTION)
.setUnit(COUNT)
- .setLabelKeys(SPANNER_LABEL_KEYS)
+ .setLabelKeys(SPANNER_LABEL_KEYS_WITH_MULTIPLEXED_SESSIONS)
.build());
DerivedLongGauge numSessionsInPoolMetric =
@@ -3700,13 +3842,28 @@ private void initOpenCensusMetricsCollection(
sessionsTimeouts.removeTimeSeries(labelValues);
sessionsTimeouts.createTimeSeries(labelValues, this, SessionPool::getNumWaiterTimeouts);
- numAcquiredSessionsMetric.removeTimeSeries(labelValues);
+ List labelValuesWithRegularSessions = new ArrayList<>(labelValues);
+ List labelValuesWithMultiplexedSessions = new ArrayList<>(labelValues);
+ labelValuesWithMultiplexedSessions.add(LabelValue.create("true"));
+ labelValuesWithRegularSessions.add(LabelValue.create("false"));
+
+ numAcquiredSessionsMetric.removeTimeSeries(labelValuesWithRegularSessions);
+ numAcquiredSessionsMetric.createTimeSeries(
+ labelValuesWithRegularSessions, this, sessionPool -> sessionPool.numSessionsAcquired);
+ numAcquiredSessionsMetric.removeTimeSeries(labelValuesWithMultiplexedSessions);
numAcquiredSessionsMetric.createTimeSeries(
- labelValues, this, sessionPool -> sessionPool.numSessionsAcquired);
+ labelValuesWithMultiplexedSessions,
+ this,
+ sessionPool -> sessionPool.numMultiplexedSessionsAcquired);
- numReleasedSessionsMetric.removeTimeSeries(labelValues);
+ numReleasedSessionsMetric.removeTimeSeries(labelValuesWithRegularSessions);
+ numReleasedSessionsMetric.createTimeSeries(
+ labelValuesWithRegularSessions, this, sessionPool -> sessionPool.numSessionsReleased);
+ numReleasedSessionsMetric.removeTimeSeries(labelValuesWithMultiplexedSessions);
numReleasedSessionsMetric.createTimeSeries(
- labelValues, this, sessionPool -> sessionPool.numSessionsReleased);
+ labelValuesWithMultiplexedSessions,
+ this,
+ sessionPool -> sessionPool.numMultiplexedSessionsReleased);
List labelValuesWithBeingPreparedType = new ArrayList<>(labelValues);
labelValuesWithBeingPreparedType.add(NUM_SESSIONS_BEING_PREPARED);
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 139dce8f0f0..4a048af52c0 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
@@ -284,7 +284,9 @@ long getRandomizePositionQPSThreshold() {
return randomizePositionQPSThreshold;
}
- boolean getUseMultiplexedSession() {
+ @VisibleForTesting
+ @InternalApi
+ public boolean getUseMultiplexedSession() {
return useMultiplexedSession;
}
@@ -491,7 +493,7 @@ public static class Builder {
private boolean useMultiplexedSession = getUseMultiplexedSessionFromEnvVariable();
private Duration multiplexedSessionMaintenanceDuration = Duration.ofDays(7);
- private Clock poolMaintainerClock;
+ private Clock poolMaintainerClock = Clock.INSTANCE;
private static Position getReleaseToPositionFromSystemProperty() {
// NOTE: This System property is a beta feature. Support for it can be removed in the future.
@@ -701,7 +703,7 @@ Builder setCloseIfInactiveTransactions() {
@VisibleForTesting
Builder setPoolMaintainerClock(Clock poolMaintainerClock) {
- this.poolMaintainerClock = poolMaintainerClock;
+ this.poolMaintainerClock = Preconditions.checkNotNull(poolMaintainerClock);
return this;
}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionReference.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionReference.java
new file mode 100644
index 00000000000..bc12cf8ee77
--- /dev/null
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionReference.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2024 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.base.Preconditions.checkNotNull;
+
+import com.google.cloud.spanner.SessionClient.SessionId;
+import com.google.cloud.spanner.spi.v1.SpannerRpc;
+import java.util.Map;
+import javax.annotation.Nullable;
+import org.threeten.bp.Instant;
+
+/**
+ * A {@code Session} can be used to perform transactions that read and/or modify data in a Cloud
+ * Spanner database. Sessions are managed internally by the client library, and users need not be
+ * aware of the actual session management, pooling and handling.
+ */
+class SessionReference {
+
+ private final String name;
+ private final DatabaseId databaseId;
+ private final Map options;
+ private volatile Instant lastUseTime;
+ @Nullable private final Instant createTime;
+ private final boolean isMultiplexed;
+
+ SessionReference(String name, Map options) {
+ this.options = options;
+ this.name = checkNotNull(name);
+ this.databaseId = SessionId.of(name).getDatabaseId();
+ this.lastUseTime = Instant.now();
+ this.createTime = null;
+ this.isMultiplexed = false;
+ }
+
+ SessionReference(
+ String name,
+ com.google.protobuf.Timestamp createTime,
+ boolean isMultiplexed,
+ Map options) {
+ this.options = options;
+ this.name = checkNotNull(name);
+ this.databaseId = SessionId.of(name).getDatabaseId();
+ this.lastUseTime = Instant.now();
+ this.createTime = convert(createTime);
+ this.isMultiplexed = isMultiplexed;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public DatabaseId getDatabaseId() {
+ return databaseId;
+ }
+
+ Map getOptions() {
+ return options;
+ }
+
+ Instant getLastUseTime() {
+ return lastUseTime;
+ }
+
+ Instant getCreateTime() {
+ return createTime;
+ }
+
+ boolean getIsMultiplexed() {
+ return isMultiplexed;
+ }
+
+ void markUsed(Instant instant) {
+ lastUseTime = instant;
+ }
+
+ private Instant convert(com.google.protobuf.Timestamp timestamp) {
+ if (timestamp == null) {
+ return null;
+ }
+ return Instant.ofEpochSecond(timestamp.getSeconds(), timestamp.getNanos());
+ }
+}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Statement.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Statement.java
index ea6cdf3f65c..a89c7c048fc 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Statement.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Statement.java
@@ -178,6 +178,11 @@ public String getSql() {
return sql;
}
+ /** Returns a copy of this statement with the SQL string replaced by the given SQL string. */
+ public Statement withReplacedSql(String sql) {
+ return new Statement(sql, this.parameters, this.queryOptions);
+ }
+
/** Returns the {@link QueryOptions} that will be used with this {@link Statement}. */
public QueryOptions getQueryOptions() {
return queryOptions;
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/admin/database/v1/stub/DatabaseAdminStubSettings.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/admin/database/v1/stub/DatabaseAdminStubSettings.java
index 68432959c50..4808f1553e9 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/admin/database/v1/stub/DatabaseAdminStubSettings.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/admin/database/v1/stub/DatabaseAdminStubSettings.java
@@ -654,15 +654,6 @@ public DatabaseAdminStub createStub() throws IOException {
"Transport not supported: %s", getTransportChannelProvider().getTransportName()));
}
- /** Returns the endpoint set by the user or the the service's default endpoint. */
- @Override
- public String getEndpoint() {
- if (super.getEndpoint() != null) {
- return super.getEndpoint();
- }
- return getDefaultEndpoint();
- }
-
/** Returns the default service name. */
@Override
public String getServiceName() {
@@ -1469,15 +1460,6 @@ public UnaryCallSettings.Builder restoreDatab
return listDatabaseRolesSettings;
}
- /** Returns the endpoint set by the user or the the service's default endpoint. */
- @Override
- public String getEndpoint() {
- if (super.getEndpoint() != null) {
- return super.getEndpoint();
- }
- return getDefaultEndpoint();
- }
-
@Override
public DatabaseAdminStubSettings build() throws IOException {
return new DatabaseAdminStubSettings(this);
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/admin/instance/v1/stub/InstanceAdminStubSettings.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/admin/instance/v1/stub/InstanceAdminStubSettings.java
index 36454ee1a55..9a00b312c61 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/admin/instance/v1/stub/InstanceAdminStubSettings.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/admin/instance/v1/stub/InstanceAdminStubSettings.java
@@ -726,15 +726,6 @@ public InstanceAdminStub createStub() throws IOException {
"Transport not supported: %s", getTransportChannelProvider().getTransportName()));
}
- /** Returns the endpoint set by the user or the the service's default endpoint. */
- @Override
- public String getEndpoint() {
- if (super.getEndpoint() != null) {
- return super.getEndpoint();
- }
- return getDefaultEndpoint();
- }
-
/** Returns the default service name. */
@Override
public String getServiceName() {
@@ -1586,15 +1577,6 @@ public UnaryCallSettings.Builder getIamPolicySettin
return listInstancePartitionOperationsSettings;
}
- /** Returns the endpoint set by the user or the the service's default endpoint. */
- @Override
- public String getEndpoint() {
- if (super.getEndpoint() != null) {
- return super.getEndpoint();
- }
- return getDefaultEndpoint();
- }
-
@Override
public InstanceAdminStubSettings build() throws IOException {
return new InstanceAdminStubSettings(this);
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AbstractMultiUseTransaction.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AbstractMultiUseTransaction.java
index 7d16f895976..da1ad2051c7 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AbstractMultiUseTransaction.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AbstractMultiUseTransaction.java
@@ -42,13 +42,15 @@ abstract class AbstractMultiUseTransaction extends AbstractBaseUnitOfWork {
/** In-memory savepoint implementation that is used by the Connection API. */
static class Savepoint {
private final String name;
+ private final boolean autoSavepoint;
static Savepoint of(String name) {
- return new Savepoint(name);
+ return new Savepoint(name, false);
}
- Savepoint(String name) {
+ Savepoint(String name, boolean autoSavepoint) {
this.name = name;
+ this.autoSavepoint = autoSavepoint;
}
/** Returns the index of the first statement that was executed after this savepoint. */
@@ -61,17 +63,23 @@ int getMutationPosition() {
return -1;
}
+ boolean isAutoSavepoint() {
+ return this.autoSavepoint;
+ }
+
@Override
public boolean equals(Object o) {
if (!(o instanceof Savepoint)) {
return false;
}
- return Objects.equals(((Savepoint) o).name, name);
+ Savepoint other = (Savepoint) o;
+ return Objects.equals(other.name, this.name)
+ && Objects.equals(other.autoSavepoint, this.autoSavepoint);
}
@Override
public int hashCode() {
- return name.hashCode();
+ return Objects.hash(this.name, this.autoSavepoint);
}
@Override
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AbstractStatementParser.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AbstractStatementParser.java
index d0c06fa1d9d..b45d444b744 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AbstractStatementParser.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AbstractStatementParser.java
@@ -16,9 +16,13 @@
package com.google.cloud.spanner.connection;
+import static com.google.cloud.spanner.connection.SimpleParser.isValidIdentifierChar;
+import static com.google.cloud.spanner.connection.StatementHintParser.convertHintsToOptions;
+
import com.google.api.core.InternalApi;
import com.google.cloud.spanner.Dialect;
import com.google.cloud.spanner.ErrorCode;
+import com.google.cloud.spanner.Options.ReadQueryUpdateTransactionOption;
import com.google.cloud.spanner.SpannerException;
import com.google.cloud.spanner.SpannerExceptionFactory;
import com.google.cloud.spanner.Statement;
@@ -169,6 +173,7 @@ public static class ParsedStatement {
private final Statement statement;
private final String sqlWithoutComments;
private final boolean returningClause;
+ private final ReadQueryUpdateTransactionOption[] optionsFromHints;
private static ParsedStatement clientSideStatement(
ClientSideStatementImpl clientSideStatement,
@@ -182,15 +187,27 @@ private static ParsedStatement ddl(Statement statement, String sqlWithoutComment
}
private static ParsedStatement query(
- Statement statement, String sqlWithoutComments, QueryOptions defaultQueryOptions) {
+ Statement statement,
+ String sqlWithoutComments,
+ QueryOptions defaultQueryOptions,
+ ReadQueryUpdateTransactionOption[] optionsFromHints) {
return new ParsedStatement(
- StatementType.QUERY, null, statement, sqlWithoutComments, defaultQueryOptions, false);
+ StatementType.QUERY,
+ null,
+ statement,
+ sqlWithoutComments,
+ defaultQueryOptions,
+ false,
+ optionsFromHints);
}
private static ParsedStatement update(
- Statement statement, String sqlWithoutComments, boolean returningClause) {
+ Statement statement,
+ String sqlWithoutComments,
+ boolean returningClause,
+ ReadQueryUpdateTransactionOption[] optionsFromHints) {
return new ParsedStatement(
- StatementType.UPDATE, statement, sqlWithoutComments, returningClause);
+ StatementType.UPDATE, statement, sqlWithoutComments, returningClause, optionsFromHints);
}
private static ParsedStatement unknown(Statement statement, String sqlWithoutComments) {
@@ -208,18 +225,20 @@ private ParsedStatement(
this.statement = statement;
this.sqlWithoutComments = Preconditions.checkNotNull(sqlWithoutComments);
this.returningClause = false;
+ this.optionsFromHints = EMPTY_OPTIONS;
}
private ParsedStatement(
StatementType type,
Statement statement,
String sqlWithoutComments,
- boolean returningClause) {
- this(type, null, statement, sqlWithoutComments, null, returningClause);
+ boolean returningClause,
+ ReadQueryUpdateTransactionOption[] optionsFromHints) {
+ this(type, null, statement, sqlWithoutComments, null, returningClause, optionsFromHints);
}
private ParsedStatement(StatementType type, Statement statement, String sqlWithoutComments) {
- this(type, null, statement, sqlWithoutComments, null, false);
+ this(type, null, statement, sqlWithoutComments, null, false, EMPTY_OPTIONS);
}
private ParsedStatement(
@@ -228,33 +247,37 @@ private ParsedStatement(
Statement statement,
String sqlWithoutComments,
QueryOptions defaultQueryOptions,
- boolean returningClause) {
+ boolean returningClause,
+ ReadQueryUpdateTransactionOption[] optionsFromHints) {
Preconditions.checkNotNull(type);
this.type = type;
this.clientSideStatement = clientSideStatement;
this.statement = statement == null ? null : mergeQueryOptions(statement, defaultQueryOptions);
this.sqlWithoutComments = Preconditions.checkNotNull(sqlWithoutComments);
this.returningClause = returningClause;
+ this.optionsFromHints = optionsFromHints;
}
private ParsedStatement copy(Statement statement, QueryOptions defaultQueryOptions) {
return new ParsedStatement(
this.type,
this.clientSideStatement,
- statement,
+ statement.withReplacedSql(this.statement.getSql()),
this.sqlWithoutComments,
defaultQueryOptions,
- this.returningClause);
+ this.returningClause,
+ this.optionsFromHints);
}
private ParsedStatement forCache() {
return new ParsedStatement(
this.type,
this.clientSideStatement,
- null,
+ Statement.of(this.statement.getSql()),
this.sqlWithoutComments,
null,
- this.returningClause);
+ this.returningClause,
+ this.optionsFromHints);
}
@Override
@@ -287,6 +310,11 @@ public boolean hasReturningClause() {
return this.returningClause;
}
+ @InternalApi
+ public ReadQueryUpdateTransactionOption[] getOptionsFromHints() {
+ return this.optionsFromHints;
+ }
+
/**
* @return true if the statement is a query that will return a {@link
* com.google.cloud.spanner.ResultSet}.
@@ -480,14 +508,23 @@ ParsedStatement parse(Statement statement, QueryOptions defaultQueryOptions) {
}
private ParsedStatement internalParse(Statement statement, QueryOptions defaultQueryOptions) {
+ StatementHintParser statementHintParser =
+ new StatementHintParser(getDialect(), statement.getSql());
+ ReadQueryUpdateTransactionOption[] optionsFromHints = EMPTY_OPTIONS;
+ if (statementHintParser.hasStatementHints()
+ && !statementHintParser.getClientSideStatementHints().isEmpty()) {
+ statement =
+ statement.toBuilder().replace(statementHintParser.getSqlWithoutClientSideHints()).build();
+ optionsFromHints = convertHintsToOptions(statementHintParser.getClientSideStatementHints());
+ }
String sql = removeCommentsAndTrim(statement.getSql());
ClientSideStatementImpl client = parseClientSideStatement(sql);
if (client != null) {
return ParsedStatement.clientSideStatement(client, statement, sql);
} else if (isQuery(sql)) {
- return ParsedStatement.query(statement, sql, defaultQueryOptions);
+ return ParsedStatement.query(statement, sql, defaultQueryOptions, optionsFromHints);
} else if (isUpdateStatement(sql)) {
- return ParsedStatement.update(statement, sql, checkReturningClause(sql));
+ return ParsedStatement.update(statement, sql, checkReturningClause(sql), optionsFromHints);
} else if (isDdlStatement(sql)) {
return ParsedStatement.ddl(statement, sql);
}
@@ -621,6 +658,10 @@ public String removeCommentsAndTrim(String sql) {
/** Removes any statement hints at the beginning of the statement. */
abstract String removeStatementHint(String sql);
+ @VisibleForTesting
+ static final ReadQueryUpdateTransactionOption[] EMPTY_OPTIONS =
+ new ReadQueryUpdateTransactionOption[0];
+
/** Parameter information with positional parameters translated to named parameters. */
@InternalApi
public static class ParametersInfo {
@@ -697,9 +738,10 @@ public boolean checkReturningClause(String sql) {
return checkReturningClauseInternal(sql);
}
+ abstract Dialect getDialect();
+
/**
- * <<<<<<< HEAD Returns true if this dialect supports nested comments. ======= <<<<<<< HEAD
- * Returns true if this dialect supports nested comments. >>>>>>> main
+ * Returns true if this dialect supports nested comments.
*
*
*
This method should return false for dialects that consider this to be a valid comment:
@@ -757,18 +799,6 @@ public boolean checkReturningClause(String sql) {
/** Returns the query parameter prefix that should be used for this dialect. */
abstract String getQueryParameterPrefix();
- /**
- * Returns true for characters that can be used as the first character in unquoted identifiers.
- */
- boolean isValidIdentifierFirstChar(char c) {
- return Character.isLetter(c) || c == UNDERSCORE;
- }
-
- /** Returns true for characters that can be used in unquoted identifiers. */
- boolean isValidIdentifierChar(char c) {
- return isValidIdentifierFirstChar(c) || Character.isDigit(c) || c == DOLLAR;
- }
-
/** Reads a dollar-quoted string literal from position index in the given sql string. */
String parseDollarQuotedString(String sql, int index) {
// Look ahead to the next dollar sign (if any). Everything in between is the quote tag.
@@ -812,9 +842,9 @@ int skip(String sql, int currentIndex, @Nullable StringBuilder result) {
} else if (currentChar == HYPHEN
&& sql.length() > (currentIndex + 1)
&& sql.charAt(currentIndex + 1) == HYPHEN) {
- return skipSingleLineComment(sql, currentIndex, result);
+ return skipSingleLineComment(sql, /* prefixLength = */ 2, currentIndex, result);
} else if (currentChar == DASH && supportsHashSingleLineComments()) {
- return skipSingleLineComment(sql, currentIndex, result);
+ return skipSingleLineComment(sql, /* prefixLength = */ 1, currentIndex, result);
} else if (currentChar == SLASH
&& sql.length() > (currentIndex + 1)
&& sql.charAt(currentIndex + 1) == ASTERISK) {
@@ -826,44 +856,31 @@ int skip(String sql, int currentIndex, @Nullable StringBuilder result) {
}
/** Skips a single-line comment from startIndex and adds it to result if result is not null. */
- static int skipSingleLineComment(String sql, int startIndex, @Nullable StringBuilder result) {
- int endIndex = sql.indexOf('\n', startIndex + 2);
- if (endIndex == -1) {
- endIndex = sql.length();
- } else {
- // Include the newline character.
- endIndex++;
+ int skipSingleLineComment(
+ String sql, int prefixLength, int startIndex, @Nullable StringBuilder result) {
+ return skipSingleLineComment(getDialect(), sql, prefixLength, startIndex, result);
+ }
+
+ static int skipSingleLineComment(
+ Dialect dialect,
+ String sql,
+ int prefixLength,
+ int startIndex,
+ @Nullable StringBuilder result) {
+ SimpleParser simpleParser = new SimpleParser(dialect, sql, startIndex, false);
+ if (simpleParser.skipSingleLineComment(prefixLength)) {
+ appendIfNotNull(result, sql.substring(startIndex, simpleParser.getPos()));
}
- appendIfNotNull(result, sql.substring(startIndex, endIndex));
- return endIndex;
+ return simpleParser.getPos();
}
/** Skips a multi-line comment from startIndex and adds it to result if result is not null. */
int skipMultiLineComment(String sql, int startIndex, @Nullable StringBuilder result) {
- // Current position is start + '/*'.length().
- int pos = startIndex + 2;
- // PostgreSQL allows comments to be nested. That is, the following is allowed:
- // '/* test /* inner comment */ still a comment */'
- int level = 1;
- while (pos < sql.length()) {
- if (supportsNestedComments()
- && sql.charAt(pos) == SLASH
- && sql.length() > (pos + 1)
- && sql.charAt(pos + 1) == ASTERISK) {
- level++;
- }
- if (sql.charAt(pos) == ASTERISK && sql.length() > (pos + 1) && sql.charAt(pos + 1) == SLASH) {
- level--;
- if (level == 0) {
- pos += 2;
- appendIfNotNull(result, sql.substring(startIndex, pos));
- return pos;
- }
- }
- pos++;
+ SimpleParser simpleParser = new SimpleParser(getDialect(), sql, startIndex, false);
+ if (simpleParser.skipMultiLineComment()) {
+ appendIfNotNull(result, sql.substring(startIndex, simpleParser.getPos()));
}
- appendIfNotNull(result, sql.substring(startIndex));
- return sql.length();
+ return simpleParser.getPos();
}
/** Skips a quoted string from startIndex. */
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/Connection.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/Connection.java
index d43e14a177b..c0859b57903 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/Connection.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/Connection.java
@@ -33,6 +33,7 @@
import com.google.cloud.spanner.PartitionOptions;
import com.google.cloud.spanner.ReadContext.QueryAnalyzeMode;
import com.google.cloud.spanner.ResultSet;
+import com.google.cloud.spanner.Spanner;
import com.google.cloud.spanner.SpannerBatchUpdateException;
import com.google.cloud.spanner.SpannerException;
import com.google.cloud.spanner.Statement;
@@ -1370,6 +1371,12 @@ default DatabaseClient getDatabaseClient() {
throw new UnsupportedOperationException("Not implemented");
}
+ /** The {@link Spanner} instance that is used by this {@link Connection}. */
+ @InternalApi
+ default Spanner getSpanner() {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
/**
* This query option is used internally to indicate that a query is executed by the library itself
* to fetch metadata. These queries are specifically allowed to be executed even when a DDL batch
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionImpl.java
index 96c509a16aa..d0cb7169793 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionImpl.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionImpl.java
@@ -32,6 +32,7 @@
import com.google.cloud.spanner.Mutation;
import com.google.cloud.spanner.Options;
import com.google.cloud.spanner.Options.QueryOption;
+import com.google.cloud.spanner.Options.ReadQueryUpdateTransactionOption;
import com.google.cloud.spanner.Options.RpcPriority;
import com.google.cloud.spanner.Options.UpdateOption;
import com.google.cloud.spanner.PartitionOptions;
@@ -305,8 +306,8 @@ static UnitOfWorkType of(TransactionMode transactionMode) {
setDefaultTransactionOptions();
}
- @VisibleForTesting
- Spanner getSpanner() {
+ @Override
+ public Spanner getSpanner() {
return this.spanner;
}
@@ -1154,6 +1155,7 @@ public ResultSet partitionQuery(
"Only queries can be partitioned. Invalid statement: " + query.getSql());
}
+ QueryOption[] combinedOptions = concat(parsedStatement.getOptionsFromHints(), options);
UnitOfWork transaction = getCurrentUnitOfWorkOrStartNewUnitOfWork();
return get(
transaction.partitionQueryAsync(
@@ -1161,7 +1163,8 @@ public ResultSet partitionQuery(
parsedStatement,
getEffectivePartitionOptions(partitionOptions),
mergeDataBoost(
- mergeQueryRequestOptions(parsedStatement, mergeQueryStatementTag(options)))));
+ mergeQueryRequestOptions(
+ parsedStatement, mergeQueryStatementTag(combinedOptions)))));
}
private PartitionOptions getEffectivePartitionOptions(
@@ -1455,6 +1458,34 @@ private List parseUpdateStatements(Iterable updates)
return parsedStatements;
}
+ private UpdateOption[] concat(
+ ReadQueryUpdateTransactionOption[] statementOptions, UpdateOption[] argumentOptions) {
+ if (statementOptions == null || statementOptions.length == 0) {
+ return argumentOptions;
+ }
+ if (argumentOptions == null || argumentOptions.length == 0) {
+ return statementOptions;
+ }
+ UpdateOption[] result =
+ Arrays.copyOf(statementOptions, statementOptions.length + argumentOptions.length);
+ System.arraycopy(argumentOptions, 0, result, statementOptions.length, argumentOptions.length);
+ return result;
+ }
+
+ private QueryOption[] concat(
+ ReadQueryUpdateTransactionOption[] statementOptions, QueryOption[] argumentOptions) {
+ if (statementOptions == null || statementOptions.length == 0) {
+ return argumentOptions;
+ }
+ if (argumentOptions == null || argumentOptions.length == 0) {
+ return statementOptions;
+ }
+ QueryOption[] result =
+ Arrays.copyOf(statementOptions, statementOptions.length + argumentOptions.length);
+ System.arraycopy(argumentOptions, 0, result, statementOptions.length, argumentOptions.length);
+ return result;
+ }
+
private QueryOption[] mergeDataBoost(QueryOption... options) {
if (this.dataBoostEnabled) {
options = appendQueryOption(options, Options.dataBoostEnabled(true));
@@ -1531,19 +1562,20 @@ private ResultSet internalExecuteQuery(
&& (analyzeMode != AnalyzeMode.NONE || statement.hasReturningClause())),
"Statement must either be a query or a DML mode with analyzeMode!=NONE or returning clause");
boolean isInternalMetadataQuery = isInternalMetadataQuery(options);
+ QueryOption[] combinedOptions = concat(statement.getOptionsFromHints(), options);
UnitOfWork transaction = getCurrentUnitOfWorkOrStartNewUnitOfWork(isInternalMetadataQuery);
if (autoPartitionMode
&& statement.getType() == StatementType.QUERY
&& !isInternalMetadataQuery) {
return runPartitionedQuery(
- statement.getStatement(), PartitionOptions.getDefaultInstance(), options);
+ statement.getStatement(), PartitionOptions.getDefaultInstance(), combinedOptions);
}
return get(
transaction.executeQueryAsync(
callType,
statement,
analyzeMode,
- mergeQueryRequestOptions(statement, mergeQueryStatementTag(options))));
+ mergeQueryRequestOptions(statement, mergeQueryStatementTag(combinedOptions))));
}
private AsyncResultSet internalExecuteQueryAsync(
@@ -1558,25 +1590,27 @@ private AsyncResultSet internalExecuteQueryAsync(
ConnectionPreconditions.checkState(
!(autoPartitionMode && statement.getType() == StatementType.QUERY),
"Partitioned queries cannot be executed asynchronously");
- UnitOfWork transaction =
- getCurrentUnitOfWorkOrStartNewUnitOfWork(isInternalMetadataQuery(options));
+ boolean isInternalMetadataQuery = isInternalMetadataQuery(options);
+ QueryOption[] combinedOptions = concat(statement.getOptionsFromHints(), options);
+ UnitOfWork transaction = getCurrentUnitOfWorkOrStartNewUnitOfWork(isInternalMetadataQuery);
return ResultSets.toAsyncResultSet(
transaction.executeQueryAsync(
callType,
statement,
analyzeMode,
- mergeQueryRequestOptions(statement, mergeQueryStatementTag(options))),
+ mergeQueryRequestOptions(statement, mergeQueryStatementTag(combinedOptions))),
spanner.getAsyncExecutorProvider(),
- options);
+ combinedOptions);
}
private ApiFuture internalExecuteUpdateAsync(
final CallType callType, final ParsedStatement update, UpdateOption... options) {
Preconditions.checkArgument(
update.getType() == StatementType.UPDATE, "Statement must be an update");
+ UpdateOption[] combinedOptions = concat(update.getOptionsFromHints(), options);
UnitOfWork transaction = getCurrentUnitOfWorkOrStartNewUnitOfWork();
return transaction.executeUpdateAsync(
- callType, update, mergeUpdateRequestOptions(mergeUpdateStatementTag(options)));
+ callType, update, mergeUpdateRequestOptions(mergeUpdateStatementTag(combinedOptions)));
}
private ApiFuture internalAnalyzeUpdateAsync(
@@ -1586,16 +1620,22 @@ private ApiFuture internalAnalyzeUpdateAsync(
UpdateOption... options) {
Preconditions.checkArgument(
update.getType() == StatementType.UPDATE, "Statement must be an update");
+ UpdateOption[] combinedOptions = concat(update.getOptionsFromHints(), options);
UnitOfWork transaction = getCurrentUnitOfWorkOrStartNewUnitOfWork();
return transaction.analyzeUpdateAsync(
- callType, update, analyzeMode, mergeUpdateRequestOptions(mergeUpdateStatementTag(options)));
+ callType,
+ update,
+ analyzeMode,
+ mergeUpdateRequestOptions(mergeUpdateStatementTag(combinedOptions)));
}
private ApiFuture internalExecuteBatchUpdateAsync(
CallType callType, List updates, UpdateOption... options) {
+ UpdateOption[] combinedOptions =
+ updates.isEmpty() ? options : concat(updates.get(0).getOptionsFromHints(), options);
UnitOfWork transaction = getCurrentUnitOfWorkOrStartNewUnitOfWork();
return transaction.executeBatchUpdateAsync(
- callType, updates, mergeUpdateRequestOptions(mergeUpdateStatementTag(options)));
+ callType, updates, mergeUpdateRequestOptions(mergeUpdateStatementTag(combinedOptions)));
}
private UnitOfWork getCurrentUnitOfWorkOrStartNewUnitOfWork() {
@@ -1648,6 +1688,7 @@ UnitOfWork createNewUnitOfWork(boolean isInternalMetadataQuery) {
.build();
case READ_WRITE_TRANSACTION:
return ReadWriteTransaction.newBuilder()
+ .setUseAutoSavepointsForEmulator(options.useAutoSavepointsForEmulator())
.setDatabaseClient(dbClient)
.setDelayTransactionStartUntilFirstWrite(delayTransactionStartUntilFirstWrite)
.setRetryAbortsInternally(retryAbortsInternally)
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java
index de58117d4bc..f79a764a94c 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java
@@ -1447,6 +1447,16 @@ public boolean isAutoConfigEmulator() {
return autoConfigEmulator;
}
+ /**
+ * Returns true if a connection should generate auto-savepoints for retrying transactions on the
+ * emulator. This allows some more concurrent transactions on the emulator.
+ */
+ boolean useAutoSavepointsForEmulator() {
+ // For now, this option is directly linked to the option autoConfigEmulator=true, which is the
+ // recommended way to configure the emulator for the Connection API.
+ return autoConfigEmulator;
+ }
+
public Dialect getDialect() {
return dialect;
}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/PostgreSQLStatementParser.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/PostgreSQLStatementParser.java
index be4aa9d7f46..4f39c549de9 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/PostgreSQLStatementParser.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/PostgreSQLStatementParser.java
@@ -16,6 +16,8 @@
package com.google.cloud.spanner.connection;
+import static com.google.cloud.spanner.connection.SimpleParser.isValidIdentifierFirstChar;
+
import com.google.api.core.InternalApi;
import com.google.cloud.spanner.Dialect;
import com.google.cloud.spanner.ErrorCode;
@@ -39,6 +41,11 @@ public class PostgreSQLStatementParser extends AbstractStatementParser {
ClientSideStatements.getInstance(Dialect.POSTGRESQL).getCompiledStatements()));
}
+ @Override
+ Dialect getDialect() {
+ return Dialect.POSTGRESQL;
+ }
+
/**
* Indicates whether the parser supports the {@code EXPLAIN} clause. The PostgreSQL parser does
* not support it.
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReadWriteTransaction.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReadWriteTransaction.java
index 05a8e899988..86d6feff90e 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReadWriteTransaction.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReadWriteTransaction.java
@@ -63,6 +63,7 @@
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Callable;
+import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;
@@ -82,7 +83,38 @@ class ReadWriteTransaction extends AbstractMultiUseTransaction {
private static final AtomicLong ID_GENERATOR = new AtomicLong();
private static final String MAX_INTERNAL_RETRIES_EXCEEDED =
"Internal transaction retry maximum exceeded";
- private static final int MAX_INTERNAL_RETRIES = 50;
+ private static final int DEFAULT_MAX_INTERNAL_RETRIES = 50;
+
+ /**
+ * A reference to the currently active transaction on the emulator that was started by the same
+ * thread. This reference is only used when running on the emulator, and enables the Connection
+ * API to manually abort the current transaction on the emulator, so other transactions can try to
+ * make progress.
+ */
+ private static final ThreadLocal CURRENT_ACTIVE_TRANSACTION =
+ new ThreadLocal<>();
+ /**
+ * The name of the automatic savepoint that is generated by the Connection API if automatically
+ * aborting the current active transaction on the emulator is enabled.
+ */
+ private static final String AUTO_SAVEPOINT_NAME = "_auto_savepoint";
+
+ /**
+ * Indicates whether an automatic savepoint should be generated after each statement, so the
+ * transaction can be manually aborted and retried by the Connection API when connected to the
+ * emulator. This feature is only intended for use with the Spanner emulator. When connected to
+ * real Spanner, the decision whether to abort a transaction or not should be delegated to
+ * Spanner.
+ */
+ private final boolean useAutoSavepointsForEmulator;
+ /**
+ * The savepoint that was automatically generated after executing the last statement. This is used
+ * to abort transactions on the emulator, if one thread tries to execute concurrent transactions
+ * on the emulator, and would otherwise be deadlocked.
+ */
+ private Savepoint autoSavepoint;
+
+ private final int maxInternalRetries;
private final ReentrantLock abortedLock = new ReentrantLock();
private final long transactionId;
private final DatabaseClient dbClient;
@@ -105,9 +137,20 @@ class ReadWriteTransaction extends AbstractMultiUseTransaction {
private final List mutations = new ArrayList<>();
private Timestamp transactionStarted;
- private static final class RollbackToSavepointException extends Exception {}
+ private static final class RollbackToSavepointException extends Exception {
+ private final Savepoint savepoint;
+
+ RollbackToSavepointException(Savepoint savepoint) {
+ this.savepoint = Preconditions.checkNotNull(savepoint);
+ }
+
+ Savepoint getSavepoint() {
+ return this.savepoint;
+ }
+ }
static class Builder extends AbstractMultiUseTransaction.Builder {
+ private boolean useAutoSavepointsForEmulator;
private DatabaseClient dbClient;
private Boolean retryAbortsInternally;
private boolean delayTransactionStartUntilFirstWrite;
@@ -118,6 +161,11 @@ static class Builder extends AbstractMultiUseTransaction.Builder T runWithRetry(Callable callable) throws SpannerException {
checkAborted();
try {
checkRolledBackToSavepoint();
- return callable.call();
+ T result = callable.call();
+ if (this.useAutoSavepointsForEmulator) {
+ this.autoSavepoint = createAutoSavepoint();
+ }
+ return result;
} catch (final AbortedException aborted) {
handleAborted(aborted);
} catch (SpannerException e) {
@@ -806,6 +871,20 @@ T runWithRetry(Callable callable) throws SpannerException {
}
}
+ private void maybeUpdateActiveTransaction() {
+ if (this.useAutoSavepointsForEmulator) {
+ if (CURRENT_ACTIVE_TRANSACTION.get() != null && CURRENT_ACTIVE_TRANSACTION.get() != this) {
+ ReadWriteTransaction activeTransaction = CURRENT_ACTIVE_TRANSACTION.get();
+ if (activeTransaction.isActive() && activeTransaction.autoSavepoint != null) {
+ activeTransaction.rollbackToSavepoint(activeTransaction.autoSavepoint);
+ activeTransaction.autoSavepoint = null;
+ }
+ CURRENT_ACTIVE_TRANSACTION.remove();
+ }
+ CURRENT_ACTIVE_TRANSACTION.set(this);
+ }
+ }
+
/**
* Registers a {@link ResultSet} on this transaction that must be checked during a retry, and
* returns a retryable {@link ResultSet}.
@@ -878,7 +957,7 @@ private void addRetryStatement(RetriableStatement statement) {
* {@link AbortedException}.
*/
private void handleAborted(AbortedException aborted) {
- if (transactionRetryAttempts >= MAX_INTERNAL_RETRIES) {
+ if (transactionRetryAttempts >= maxInternalRetries) {
// If the same statement in transaction keeps aborting, then we need to abort here.
throwAbortWithRetryAttemptsExceeded();
} else if (retryAbortsInternally) {
@@ -890,6 +969,9 @@ private void handleAborted(AbortedException aborted) {
if (delay > 0L) {
//noinspection BusyWait
Thread.sleep(delay);
+ } else if (aborted.isEmulatorOnlySupportsOneTransactionException()) {
+ //noinspection BusyWait
+ Thread.sleep(ThreadLocalRandom.current().nextInt(50));
}
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
@@ -932,13 +1014,16 @@ private void handleAborted(AbortedException aborted) {
this.state = UnitOfWorkState.ABORTED;
this.abortedException = e;
throw e;
- } catch (AbortedException e) {
+ } catch (AbortedException abortedExceptionDuringRetry) {
// Retry aborted, do another retry of the transaction.
- if (transactionRetryAttempts >= MAX_INTERNAL_RETRIES) {
+ if (transactionRetryAttempts >= maxInternalRetries) {
throwAbortWithRetryAttemptsExceeded();
}
invokeTransactionRetryListenersOnFinish(RetryResult.RETRY_ABORTED_AND_RESTARTING);
logger.fine(toString() + ": Internal transaction retry aborted, trying again");
+ // Use the new aborted exception to determine both the backoff delay and how to handle
+ // the retry.
+ aborted = abortedExceptionDuringRetry;
} catch (SpannerException e) {
// unexpected exception
logger.log(
@@ -1051,7 +1136,12 @@ static class ReadWriteSavepoint extends Savepoint {
private final int mutationPosition;
ReadWriteSavepoint(String name, int statementPosition, int mutationPosition) {
- super(name);
+ this(name, statementPosition, mutationPosition, false);
+ }
+
+ ReadWriteSavepoint(
+ String name, int statementPosition, int mutationPosition, boolean autoSavepoint) {
+ super(name, autoSavepoint);
this.statementPosition = statementPosition;
this.mutationPosition = mutationPosition;
}
@@ -1072,6 +1162,10 @@ Savepoint savepoint(String name) {
return new ReadWriteSavepoint(name, statements.size(), mutations.size());
}
+ private Savepoint createAutoSavepoint() {
+ return new ReadWriteSavepoint(AUTO_SAVEPOINT_NAME, statements.size(), mutations.size(), true);
+ }
+
@Override
void rollbackToSavepoint(Savepoint savepoint) {
get(rollbackAsync(CallType.SYNC, false));
@@ -1082,7 +1176,7 @@ void rollbackToSavepoint(Savepoint savepoint) {
SpannerExceptionFactory.newSpannerException(
ErrorCode.ABORTED,
"Transaction has been rolled back to a savepoint",
- new RollbackToSavepointException());
+ new RollbackToSavepointException(savepoint));
// Clear all statements and mutations after the savepoint.
this.statements.subList(savepoint.getStatementPosition(), this.statements.size()).clear();
this.mutations.subList(savepoint.getMutationPosition(), this.mutations.size()).clear();
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SimpleParser.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SimpleParser.java
new file mode 100644
index 00000000000..0af86892dde
--- /dev/null
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SimpleParser.java
@@ -0,0 +1,303 @@
+/*
+ * Copyright 2024 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.connection;
+
+import static com.google.cloud.spanner.connection.AbstractStatementParser.ASTERISK;
+import static com.google.cloud.spanner.connection.AbstractStatementParser.DASH;
+import static com.google.cloud.spanner.connection.AbstractStatementParser.HYPHEN;
+import static com.google.cloud.spanner.connection.AbstractStatementParser.SLASH;
+
+import com.google.cloud.spanner.Dialect;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import java.util.Objects;
+
+/** A very simple token-based parser for extracting relevant information from SQL strings. */
+class SimpleParser {
+ /**
+ * An immutable result from a parse action indicating whether the parse action was successful, and
+ * if so, what the value was.
+ */
+ static class Result {
+ static final Result NOT_FOUND = new Result(null);
+
+ static Result found(String value) {
+ return new Result(Preconditions.checkNotNull(value));
+ }
+
+ private final String value;
+
+ private Result(String value) {
+ this.value = value;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(this.value);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof Result)) {
+ return false;
+ }
+ return Objects.equals(this.value, ((Result) o).value);
+ }
+
+ @Override
+ public String toString() {
+ if (isValid()) {
+ return this.value;
+ }
+ return "NOT FOUND";
+ }
+
+ boolean isValid() {
+ return this.value != null;
+ }
+
+ String getValue() {
+ return this.value;
+ }
+ }
+
+ // TODO: Replace this with a direct reference to the dialect, and move the isXYZSupported methods
+ // from the AbstractStatementParser class to the Dialect class.
+ private final AbstractStatementParser statementParser;
+
+ private final String sql;
+
+ private final boolean treatHintCommentsAsTokens;
+
+ private int pos;
+
+ /** Constructs a simple parser for the given SQL string and dialect. */
+ SimpleParser(Dialect dialect, String sql) {
+ this(dialect, sql, 0, /* treatHintCommentsAsTokens = */ false);
+ }
+
+ /**
+ * Constructs a simple parser for the given SQL string and dialect.
+ * treatHintCommentsAsTokens indicates whether comments that start with '/*@' should be
+ * treated as tokens or not. This option may only be enabled if the dialect is PostgreSQL.
+ */
+ SimpleParser(Dialect dialect, String sql, int pos, boolean treatHintCommentsAsTokens) {
+ Preconditions.checkArgument(
+ !(treatHintCommentsAsTokens && dialect != Dialect.POSTGRESQL),
+ "treatHintCommentsAsTokens can only be enabled for PostgreSQL");
+ this.sql = sql;
+ this.pos = pos;
+ this.statementParser = AbstractStatementParser.getInstance(dialect);
+ this.treatHintCommentsAsTokens = treatHintCommentsAsTokens;
+ }
+
+ Dialect getDialect() {
+ return this.statementParser.getDialect();
+ }
+
+ String getSql() {
+ return this.sql;
+ }
+
+ int getPos() {
+ return this.pos;
+ }
+
+ /** Returns true if this parser has more tokens. Advances the position to the first next token. */
+ boolean hasMoreTokens() {
+ skipWhitespaces();
+ return pos < sql.length();
+ }
+
+ /**
+ * Eats and returns the identifier at the current position. This implementation does not support
+ * quoted identifiers.
+ */
+ Result eatIdentifier() {
+ // TODO: Implement support for quoted identifiers.
+ // TODO: Implement support for identifiers with multiple parts (e.g. my_schema.my_table).
+ if (!hasMoreTokens()) {
+ return Result.NOT_FOUND;
+ }
+ if (!isValidIdentifierFirstChar(sql.charAt(pos))) {
+ return Result.NOT_FOUND;
+ }
+ int startPos = pos;
+ while (pos < sql.length() && isValidIdentifierChar(sql.charAt(pos))) {
+ pos++;
+ }
+ return Result.found(sql.substring(startPos, pos));
+ }
+
+ /**
+ * Eats a single-quoted string. This implementation currently does not support escape sequences.
+ */
+ Result eatSingleQuotedString() {
+ if (!eatToken('\'')) {
+ return Result.NOT_FOUND;
+ }
+ int startPos = pos;
+ while (pos < sql.length() && sql.charAt(pos) != '\'') {
+ if (sql.charAt(pos) == '\n') {
+ return Result.NOT_FOUND;
+ }
+ pos++;
+ }
+ if (pos == sql.length()) {
+ return Result.NOT_FOUND;
+ }
+ return Result.found(sql.substring(startPos, pos++));
+ }
+
+ boolean peekTokens(char... tokens) {
+ return internalEatTokens(/* updatePos = */ false, tokens);
+ }
+
+ /**
+ * Returns true if the next tokens in the SQL string are equal to the given tokens, and advances
+ * the position of the parser to after the tokens. The position is not changed if the next tokens
+ * are not equal to the list of tokens.
+ */
+ boolean eatTokens(char... tokens) {
+ return internalEatTokens(/* updatePos = */ true, tokens);
+ }
+
+ /**
+ * Returns true if the next tokens in the SQL string are equal to the given tokens, and advances
+ * the position of the parser to after the tokens if updatePos is true. The position is not
+ * changed if the next tokens are not equal to the list of tokens, or if updatePos is false.
+ */
+ private boolean internalEatTokens(boolean updatePos, char... tokens) {
+ int currentPos = pos;
+ for (char token : tokens) {
+ if (!eatToken(token)) {
+ pos = currentPos;
+ return false;
+ }
+ }
+ if (!updatePos) {
+ pos = currentPos;
+ }
+ return true;
+ }
+
+ /**
+ * Returns true if the next token is equal to the given character, but does not advance the
+ * position of the parser.
+ */
+ boolean peekToken(char token) {
+ int currentPos = pos;
+ boolean res = eatToken(token);
+ pos = currentPos;
+ return res;
+ }
+
+ /**
+ * Returns true and advances the position of the parser if the next token is equal to the given
+ * character.
+ */
+ boolean eatToken(char token) {
+ skipWhitespaces();
+ if (pos < sql.length() && sql.charAt(pos) == token) {
+ pos++;
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if the given character is valid as the first character of an identifier. That
+ * means that it can be used as the first character of an unquoted identifier.
+ */
+ static boolean isValidIdentifierFirstChar(char c) {
+ return Character.isLetter(c) || c == '_';
+ }
+
+ /**
+ * Returns true if the given character is a valid identifier character. That means that it can be
+ * used in an unquoted identifiers.
+ */
+ static boolean isValidIdentifierChar(char c) {
+ return isValidIdentifierFirstChar(c) || Character.isDigit(c) || c == '$';
+ }
+
+ /**
+ * Skips all whitespaces, including comments, from the current position and advances the parser to
+ * the next actual token.
+ */
+ @VisibleForTesting
+ void skipWhitespaces() {
+ while (pos < sql.length()) {
+ if (sql.charAt(pos) == HYPHEN && sql.length() > (pos + 1) && sql.charAt(pos + 1) == HYPHEN) {
+ skipSingleLineComment(/* prefixLength = */ 2);
+ } else if (statementParser.supportsHashSingleLineComments() && sql.charAt(pos) == DASH) {
+ skipSingleLineComment(/* prefixLength = */ 1);
+ } else if (sql.charAt(pos) == SLASH
+ && sql.length() > (pos + 1)
+ && sql.charAt(pos + 1) == ASTERISK) {
+ if (treatHintCommentsAsTokens && sql.length() > (pos + 2) && sql.charAt(pos + 2) == '@') {
+ break;
+ }
+ skipMultiLineComment();
+ } else if (Character.isWhitespace(sql.charAt(pos))) {
+ pos++;
+ } else {
+ break;
+ }
+ }
+ }
+
+ /**
+ * Skips through a single-line comment from the current position. The single-line comment is
+ * started by a prefix with the given length (e.g. either '#' or '--').
+ */
+ @VisibleForTesting
+ boolean skipSingleLineComment(int prefixLength) {
+ int endIndex = sql.indexOf('\n', pos + prefixLength);
+ if (endIndex == -1) {
+ pos = sql.length();
+ return true;
+ }
+ pos = endIndex + 1;
+ return true;
+ }
+
+ /** Skips through a multi-line comment from the current position. */
+ @VisibleForTesting
+ boolean skipMultiLineComment() {
+ int level = 1;
+ pos += 2;
+ while (pos < sql.length()) {
+ if (statementParser.supportsNestedComments()
+ && sql.charAt(pos) == SLASH
+ && sql.length() > (pos + 1)
+ && sql.charAt(pos + 1) == ASTERISK) {
+ level++;
+ }
+ if (sql.charAt(pos) == ASTERISK && sql.length() > (pos + 1) && sql.charAt(pos + 1) == SLASH) {
+ level--;
+ if (level == 0) {
+ pos += 2;
+ return true;
+ }
+ }
+ pos++;
+ }
+ pos = sql.length();
+ return false;
+ }
+}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerStatementParser.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerStatementParser.java
index 892672ad0df..fdd10bbf5ae 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerStatementParser.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerStatementParser.java
@@ -41,6 +41,11 @@ public SpannerStatementParser() throws CompileException {
ClientSideStatements.getInstance(Dialect.GOOGLE_STANDARD_SQL).getCompiledStatements()));
}
+ @Override
+ Dialect getDialect() {
+ return Dialect.GOOGLE_STANDARD_SQL;
+ }
+
/**
* Indicates whether the parser supports the {@code EXPLAIN} clause. The Spanner parser does
* support it.
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/StatementHintParser.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/StatementHintParser.java
new file mode 100644
index 00000000000..d6d4a7fa48c
--- /dev/null
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/StatementHintParser.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright 2024 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.connection;
+
+import com.google.cloud.Tuple;
+import com.google.cloud.spanner.Dialect;
+import com.google.cloud.spanner.ErrorCode;
+import com.google.cloud.spanner.Options;
+import com.google.cloud.spanner.Options.ReadQueryUpdateTransactionOption;
+import com.google.cloud.spanner.Options.RpcPriority;
+import com.google.cloud.spanner.SpannerExceptionFactory;
+import com.google.cloud.spanner.connection.SimpleParser.Result;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.spanner.v1.RequestOptions.Priority;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/** A simple parser for extracting statement hints from SQL strings. */
+class StatementHintParser {
+ private static final char[] GOOGLE_SQL_START_HINT_TOKENS = new char[] {'@', '{'};
+ private static final char[] POSTGRESQL_START_HINT_TOKENS = new char[] {'/', '*', '@'};
+ private static final char[] GOOGLE_SQL_END_HINT_TOKENS = new char[] {'}'};
+ private static final char[] POSTGRESQL_END_HINT_TOKENS = new char[] {'*', '/'};
+ private static final String STATEMENT_TAG_HINT_NAME = "STATEMENT_TAG";
+ private static final String RPC_PRIORITY_HINT_NAME = "RPC_PRIORITY";
+ private static final ImmutableSet CLIENT_SIDE_STATEMENT_HINT_NAMES =
+ ImmutableSet.of(STATEMENT_TAG_HINT_NAME, RPC_PRIORITY_HINT_NAME);
+
+ static final Map NO_HINTS = ImmutableMap.of();
+
+ private final boolean hasStatementHints;
+
+ private final Map hints;
+
+ private final String sqlWithoutClientSideHints;
+
+ StatementHintParser(Dialect dialect, String sql) {
+ this(CLIENT_SIDE_STATEMENT_HINT_NAMES, dialect, sql);
+ }
+
+ StatementHintParser(
+ ImmutableSet clientSideStatementHintNames, Dialect dialect, String sql) {
+ SimpleParser parser =
+ new SimpleParser(
+ dialect,
+ sql,
+ /* pos = */ 0,
+ /* treatHintCommentsAsTokens = */ dialect == Dialect.POSTGRESQL);
+ this.hasStatementHints = parser.peekTokens(getStartHintTokens(dialect));
+ if (this.hasStatementHints) {
+ Tuple> hints = extract(parser, clientSideStatementHintNames);
+ this.sqlWithoutClientSideHints = hints.x();
+ this.hints = hints.y();
+ } else {
+ this.sqlWithoutClientSideHints = sql;
+ this.hints = NO_HINTS;
+ }
+ }
+
+ private static char[] getStartHintTokens(Dialect dialect) {
+ switch (dialect) {
+ case POSTGRESQL:
+ return POSTGRESQL_START_HINT_TOKENS;
+ case GOOGLE_STANDARD_SQL:
+ default:
+ return GOOGLE_SQL_START_HINT_TOKENS;
+ }
+ }
+
+ private static char[] getEndHintTokens(Dialect dialect) {
+ switch (dialect) {
+ case POSTGRESQL:
+ return POSTGRESQL_END_HINT_TOKENS;
+ case GOOGLE_STANDARD_SQL:
+ default:
+ return GOOGLE_SQL_END_HINT_TOKENS;
+ }
+ }
+
+ /**
+ * Extracts any query/update options from client-side hints in the given statement. Currently,
+ * this method supports following client-side hints:
+ *
+ *
+ *
STATEMENT_TAG
+ *
RPC_PRIORITY
+ *
+ */
+ static ReadQueryUpdateTransactionOption[] convertHintsToOptions(Map hints) {
+ ReadQueryUpdateTransactionOption[] result = new ReadQueryUpdateTransactionOption[hints.size()];
+ int index = 0;
+ for (Entry hint : hints.entrySet()) {
+ result[index++] = convertHintToOption(hint.getKey(), hint.getValue());
+ }
+ return result;
+ }
+
+ private static ReadQueryUpdateTransactionOption convertHintToOption(String hint, String value) {
+ Preconditions.checkNotNull(value);
+ switch (Preconditions.checkNotNull(hint).toUpperCase(Locale.ENGLISH)) {
+ case STATEMENT_TAG_HINT_NAME:
+ return Options.tag(value);
+ case RPC_PRIORITY_HINT_NAME:
+ try {
+ Priority priority = Priority.valueOf(value);
+ return Options.priority(RpcPriority.fromProto(priority));
+ } catch (IllegalArgumentException illegalArgumentException) {
+ throw SpannerExceptionFactory.newSpannerException(
+ ErrorCode.INVALID_ARGUMENT,
+ "Invalid RPC priority value: " + value,
+ illegalArgumentException);
+ }
+ default:
+ throw SpannerExceptionFactory.newSpannerException(
+ ErrorCode.INVALID_ARGUMENT, "Invalid hint name: " + hint);
+ }
+ }
+
+ boolean hasStatementHints() {
+ return this.hasStatementHints;
+ }
+
+ String getSqlWithoutClientSideHints() {
+ return this.sqlWithoutClientSideHints;
+ }
+
+ Map getClientSideStatementHints() {
+ return this.hints;
+ }
+
+ private static Tuple> extract(
+ SimpleParser parser, ImmutableSet clientSideStatementHintNames) {
+ String updatedSql = parser.getSql();
+ int posBeforeHintToken = parser.getPos();
+ int removedHintsLength = 0;
+ boolean allClientSideHints = true;
+ // This method is only called if the parser has hints, so it is safe to ignore this result.
+ parser.eatTokens(getStartHintTokens(parser.getDialect()));
+ ImmutableMap.Builder builder = ImmutableMap.builder();
+ while (parser.hasMoreTokens()) {
+ int posBeforeHint = parser.getPos();
+ boolean foundClientSideHint = false;
+ Result hintName = parser.eatIdentifier();
+ if (!hintName.isValid()) {
+ return Tuple.of(parser.getSql(), NO_HINTS);
+ }
+ if (!parser.eatToken('=')) {
+ return Tuple.of(parser.getSql(), NO_HINTS);
+ }
+ Result hintValue = eatHintLiteral(parser);
+ if (!hintValue.isValid()) {
+ return Tuple.of(parser.getSql(), NO_HINTS);
+ }
+ if (clientSideStatementHintNames.contains(hintName.getValue().toUpperCase(Locale.ENGLISH))) {
+ builder.put(hintName.getValue(), hintValue.getValue());
+ foundClientSideHint = true;
+ } else {
+ allClientSideHints = false;
+ }
+ boolean endOfHints = parser.peekTokens(getEndHintTokens(parser.getDialect()));
+ if (!endOfHints && !parser.eatToken(',')) {
+ return Tuple.of(parser.getSql(), NO_HINTS);
+ }
+ if (foundClientSideHint) {
+ // Remove the client-side hint from the SQL string that is sent to Spanner.
+ updatedSql =
+ updatedSql.substring(0, posBeforeHint - removedHintsLength)
+ + parser.getSql().substring(parser.getPos());
+ removedHintsLength += parser.getPos() - posBeforeHint;
+ }
+ if (endOfHints) {
+ break;
+ }
+ }
+ if (!parser.eatTokens(getEndHintTokens(parser.getDialect()))) {
+ return Tuple.of(parser.getSql(), NO_HINTS);
+ }
+ if (allClientSideHints) {
+ // Only client-side hints found. Remove the entire hint block.
+ updatedSql =
+ parser.getSql().substring(0, posBeforeHintToken)
+ + parser.getSql().substring(parser.getPos());
+ }
+ return Tuple.of(updatedSql, builder.build());
+ }
+
+ /** Eats a hint literal. This is a literal that could be a quoted string, or an identifier. */
+ private static Result eatHintLiteral(SimpleParser parser) {
+ if (parser.peekToken('\'')) {
+ return parser.eatSingleQuotedString();
+ }
+ return parser.eatIdentifier();
+ }
+}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java
index 8974c4287bb..53e360b801b 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java
@@ -54,7 +54,6 @@
import com.google.api.gax.rpc.UnavailableException;
import com.google.api.gax.rpc.WatchdogProvider;
import com.google.api.pathtemplate.PathTemplate;
-import com.google.cloud.NoCredentials;
import com.google.cloud.RetryHelper;
import com.google.cloud.RetryHelper.RetryHelperException;
import com.google.cloud.grpc.GcpManagedChannelBuilder;
@@ -344,19 +343,11 @@ public GapicSpannerRpc(final SpannerOptions options) {
// This sets the response compressor (Server -> Client).
.withEncoding(compressorName))
.setHeaderProvider(headerProviderWithUserAgent)
- .setAllowNonDefaultServiceAccount(true)
- // Attempts direct access to spanner service over gRPC to improve throughput,
- // whether the attempt is allowed is totally controlled by service owner.
- // We'll only attempt DirectPath if we are using real credentials.
- // NoCredentials is used for plain text connections, for example when connecting to
- // the emulator.
- .setAttemptDirectPath(
- options.isAttemptDirectPath()
- && !Objects.equals(
- options.getScopedCredentials(), NoCredentials.getInstance()));
+ .setAllowNonDefaultServiceAccount(true);
String directPathXdsEnv = System.getenv("GOOGLE_SPANNER_ENABLE_DIRECT_ACCESS");
boolean isAttemptDirectPathXds = Boolean.parseBoolean(directPathXdsEnv);
if (isAttemptDirectPathXds) {
+ defaultChannelProviderBuilder.setAttemptDirectPath(true);
defaultChannelProviderBuilder.setAttemptDirectPathXds();
}
if (options.isUseVirtualThreads()) {
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/v1/stub/SpannerStubSettings.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/v1/stub/SpannerStubSettings.java
index dbc521b5ecb..4a60eb3ef9b 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/v1/stub/SpannerStubSettings.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/v1/stub/SpannerStubSettings.java
@@ -299,15 +299,6 @@ public SpannerStub createStub() throws IOException {
"Transport not supported: %s", getTransportChannelProvider().getTransportName()));
}
- /** Returns the endpoint set by the user or the the service's default endpoint. */
- @Override
- public String getEndpoint() {
- if (super.getEndpoint() != null) {
- return super.getEndpoint();
- }
- return getDefaultEndpoint();
- }
-
/** Returns the default service name. */
@Override
public String getServiceName() {
@@ -808,15 +799,6 @@ public UnaryCallSettings.Builder rollbackSettings() {
return batchWriteSettings;
}
- /** Returns the endpoint set by the user or the the service's default endpoint. */
- @Override
- public String getEndpoint() {
- if (super.getEndpoint() != null) {
- return super.getEndpoint();
- }
- return getDefaultEndpoint();
- }
-
@Override
public SpannerStubSettings build() throws IOException {
return new SpannerStubSettings(this);
diff --git a/google-cloud-spanner/src/main/resources/META-INF/native-image/com.google.cloud.spanner.admin.instance.v1/reflect-config.json b/google-cloud-spanner/src/main/resources/META-INF/native-image/com.google.cloud.spanner.admin.instance.v1/reflect-config.json
index a01e9dbb010..92d5f0c9a0b 100644
--- a/google-cloud-spanner/src/main/resources/META-INF/native-image/com.google.cloud.spanner.admin.instance.v1/reflect-config.json
+++ b/google-cloud-spanner/src/main/resources/META-INF/native-image/com.google.cloud.spanner.admin.instance.v1/reflect-config.json
@@ -1817,6 +1817,15 @@
"allDeclaredClasses": true,
"allPublicClasses": true
},
+ {
+ "name": "com.google.spanner.admin.instance.v1.FulfillmentPeriod",
+ "queryAllDeclaredConstructors": true,
+ "queryAllPublicConstructors": true,
+ "queryAllDeclaredMethods": true,
+ "allPublicMethods": true,
+ "allDeclaredClasses": true,
+ "allPublicClasses": true
+ },
{
"name": "com.google.spanner.admin.instance.v1.GetInstanceConfigRequest",
"queryAllDeclaredConstructors": true,
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AbstractLatencyBenchmark.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AbstractLatencyBenchmark.java
index b57f3aa723d..f50ef5e2090 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AbstractLatencyBenchmark.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AbstractLatencyBenchmark.java
@@ -38,6 +38,9 @@ public abstract class AbstractLatencyBenchmark {
Integer.valueOf(
MoreObjects.firstNonNull(System.getenv("SPANNER_TEST_JMH_NUM_PARALLEL_THREADS"), "30"));
+ static final int NUM_GRPC_CHANNELS =
+ Integer.valueOf(
+ MoreObjects.firstNonNull(System.getenv("SPANNER_TEST_JMH_NUM_GRPC_CHANNELS"), "4"));
/**
* Total number of reads per test run for 1 thread. Increasing the value here will increase the
* duration of the benchmark. For ex - With PARALLEL_THREADS = 2, TOTAL_READS_PER_RUN = 200, there
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AbstractMockServerTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AbstractMockServerTest.java
index 760f4e8a659..bcc455ff9b1 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AbstractMockServerTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AbstractMockServerTest.java
@@ -31,7 +31,7 @@ abstract class AbstractMockServerTest {
protected static Server server;
protected static LocalChannelProvider channelProvider;
- private Spanner spanner;
+ protected Spanner spanner;
@BeforeClass
public static void startMockServer() throws IOException {
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 c1012d7ff8f..07f7967c2aa 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
@@ -33,6 +33,7 @@
import com.google.spanner.v1.BatchCreateSessionsRequest;
import com.google.spanner.v1.BeginTransactionRequest;
import com.google.spanner.v1.CommitRequest;
+import com.google.spanner.v1.CreateSessionRequest;
import com.google.spanner.v1.ExecuteBatchDmlRequest;
import com.google.spanner.v1.ExecuteSqlRequest;
import io.grpc.Status;
@@ -44,12 +45,17 @@
import java.util.concurrent.Executors;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.atomic.AtomicInteger;
+import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@RunWith(JUnit4.class)
public class AsyncRunnerTest extends AbstractAsyncTransactionTest {
+ @After
+ public void clearRequests() {
+ mockSpanner.clearRequests();
+ }
@Test
public void testAsyncRunner_doesNotReturnCommitTimestampBeforeCommit() {
@@ -195,15 +201,28 @@ public void asyncRunnerUpdateAbortedWithoutGettingResult() throws Exception {
executor);
assertThat(result.get()).isNull();
assertThat(attempt.get()).isEqualTo(2);
- assertThat(mockSpanner.getRequestTypes())
- .containsExactly(
- BatchCreateSessionsRequest.class,
- ExecuteSqlRequest.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);
+ if (isMultiplexedSessionsEnabled()) {
+ assertThat(mockSpanner.getRequestTypes())
+ .containsExactly(
+ CreateSessionRequest.class,
+ BatchCreateSessionsRequest.class,
+ ExecuteSqlRequest.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);
+ } else {
+ assertThat(mockSpanner.getRequestTypes())
+ .containsExactly(
+ BatchCreateSessionsRequest.class,
+ ExecuteSqlRequest.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);
+ }
}
@Test
@@ -241,9 +260,18 @@ public void asyncRunnerWaitsUntilAsyncUpdateHasFinished() throws Exception {
},
executor);
res.get();
- assertThat(mockSpanner.getRequestTypes())
- .containsExactly(
- BatchCreateSessionsRequest.class, ExecuteSqlRequest.class, CommitRequest.class);
+ if (isMultiplexedSessionsEnabled()) {
+ assertThat(mockSpanner.getRequestTypes())
+ .containsExactly(
+ CreateSessionRequest.class,
+ BatchCreateSessionsRequest.class,
+ ExecuteSqlRequest.class,
+ CommitRequest.class);
+ } else {
+ assertThat(mockSpanner.getRequestTypes())
+ .containsExactly(
+ BatchCreateSessionsRequest.class, ExecuteSqlRequest.class, CommitRequest.class);
+ }
}
@Test
@@ -377,15 +405,28 @@ public void asyncRunnerBatchUpdateAbortedWithoutGettingResult() throws Exception
executor);
assertThat(result.get()).isNull();
assertThat(attempt.get()).isEqualTo(2);
- assertThat(mockSpanner.getRequestTypes())
- .containsExactly(
- BatchCreateSessionsRequest.class,
- ExecuteSqlRequest.class,
- ExecuteBatchDmlRequest.class,
- CommitRequest.class,
- ExecuteSqlRequest.class,
- ExecuteBatchDmlRequest.class,
- CommitRequest.class);
+ if (isMultiplexedSessionsEnabled()) {
+ assertThat(mockSpanner.getRequestTypes())
+ .containsExactly(
+ CreateSessionRequest.class,
+ BatchCreateSessionsRequest.class,
+ ExecuteSqlRequest.class,
+ ExecuteBatchDmlRequest.class,
+ CommitRequest.class,
+ ExecuteSqlRequest.class,
+ ExecuteBatchDmlRequest.class,
+ CommitRequest.class);
+ } else {
+ assertThat(mockSpanner.getRequestTypes())
+ .containsExactly(
+ BatchCreateSessionsRequest.class,
+ ExecuteSqlRequest.class,
+ ExecuteBatchDmlRequest.class,
+ CommitRequest.class,
+ ExecuteSqlRequest.class,
+ ExecuteBatchDmlRequest.class,
+ CommitRequest.class);
+ }
}
@Test
@@ -423,9 +464,18 @@ public void asyncRunnerWaitsUntilAsyncBatchUpdateHasFinished() throws Exception
},
executor);
res.get();
- assertThat(mockSpanner.getRequestTypes())
- .containsExactly(
- BatchCreateSessionsRequest.class, ExecuteBatchDmlRequest.class, CommitRequest.class);
+ if (isMultiplexedSessionsEnabled()) {
+ assertThat(mockSpanner.getRequestTypes())
+ .containsExactly(
+ CreateSessionRequest.class,
+ BatchCreateSessionsRequest.class,
+ ExecuteBatchDmlRequest.class,
+ CommitRequest.class);
+ } else {
+ assertThat(mockSpanner.getRequestTypes())
+ .containsExactly(
+ BatchCreateSessionsRequest.class, ExecuteBatchDmlRequest.class, CommitRequest.class);
+ }
}
@Test
@@ -520,4 +570,11 @@ public void asyncRunnerRead() throws Exception {
executor);
assertThat(val.get()).containsExactly("v1", "v2", "v3");
}
+
+ private boolean isMultiplexedSessionsEnabled() {
+ if (spanner.getOptions() == null || spanner.getOptions().getSessionPoolOptions() == null) {
+ return false;
+ }
+ return spanner.getOptions().getSessionPoolOptions().getUseMultiplexedSession();
+ }
}
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 58b8e65974b..09d14cee3bf 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
@@ -49,6 +49,7 @@
import com.google.spanner.v1.BatchCreateSessionsRequest;
import com.google.spanner.v1.BeginTransactionRequest;
import com.google.spanner.v1.CommitRequest;
+import com.google.spanner.v1.CreateSessionRequest;
import com.google.spanner.v1.ExecuteBatchDmlRequest;
import com.google.spanner.v1.ExecuteSqlRequest;
import com.google.spanner.v1.RollbackRequest;
@@ -344,18 +345,34 @@ public void asyncTransactionManagerFireAndForgetInvalidUpdate() throws Exception
}
}
}
- 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);
+ if (isMultiplexedSessionsEnabled()) {
+ assertThat(mockSpanner.getRequestTypes())
+ .containsExactly(
+ CreateSessionRequest.class,
+ 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);
+ } else {
+ 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
@@ -549,9 +566,18 @@ public void asyncTransactionManagerWaitsUntilAsyncUpdateHasFinished() throws Exc
executor)
.commitAsync()
.get();
- assertThat(mockSpanner.getRequestTypes())
- .containsExactly(
- BatchCreateSessionsRequest.class, ExecuteSqlRequest.class, CommitRequest.class);
+ if (isMultiplexedSessionsEnabled()) {
+ assertThat(mockSpanner.getRequestTypes())
+ .containsExactly(
+ CreateSessionRequest.class,
+ BatchCreateSessionsRequest.class,
+ ExecuteSqlRequest.class,
+ CommitRequest.class);
+ } else {
+ assertThat(mockSpanner.getRequestTypes())
+ .containsExactly(
+ BatchCreateSessionsRequest.class, ExecuteSqlRequest.class, CommitRequest.class);
+ }
break;
} catch (AbortedException e) {
txn = mgr.resetForRetryAsync();
@@ -655,12 +681,22 @@ public void asyncTransactionManagerFireAndForgetInvalidBatchUpdate() throws Exce
}
}
}
- assertThat(mockSpanner.getRequestTypes())
- .containsExactly(
- BatchCreateSessionsRequest.class,
- ExecuteBatchDmlRequest.class,
- ExecuteBatchDmlRequest.class,
- CommitRequest.class);
+ if (isMultiplexedSessionsEnabled()) {
+ assertThat(mockSpanner.getRequestTypes())
+ .containsExactly(
+ CreateSessionRequest.class,
+ BatchCreateSessionsRequest.class,
+ ExecuteBatchDmlRequest.class,
+ ExecuteBatchDmlRequest.class,
+ CommitRequest.class);
+ } else {
+ assertThat(mockSpanner.getRequestTypes())
+ .containsExactly(
+ BatchCreateSessionsRequest.class,
+ ExecuteBatchDmlRequest.class,
+ ExecuteBatchDmlRequest.class,
+ CommitRequest.class);
+ }
}
@Test
@@ -693,13 +729,24 @@ public void asyncTransactionManagerBatchUpdateAborted() throws Exception {
assertThat(attempt.get()).isEqualTo(2);
// There should only be 1 CommitRequest, as the first attempt should abort already after the
// ExecuteBatchDmlRequest.
- assertThat(mockSpanner.getRequestTypes())
- .containsExactly(
- BatchCreateSessionsRequest.class,
- ExecuteBatchDmlRequest.class,
- BeginTransactionRequest.class,
- ExecuteBatchDmlRequest.class,
- CommitRequest.class);
+ if (isMultiplexedSessionsEnabled()) {
+ assertThat(mockSpanner.getRequestTypes())
+ .containsExactly(
+ CreateSessionRequest.class,
+ BatchCreateSessionsRequest.class,
+ ExecuteBatchDmlRequest.class,
+ BeginTransactionRequest.class,
+ ExecuteBatchDmlRequest.class,
+ CommitRequest.class);
+ } else {
+ assertThat(mockSpanner.getRequestTypes())
+ .containsExactly(
+ BatchCreateSessionsRequest.class,
+ ExecuteBatchDmlRequest.class,
+ BeginTransactionRequest.class,
+ ExecuteBatchDmlRequest.class,
+ CommitRequest.class);
+ }
}
@Test
@@ -730,13 +777,24 @@ public void asyncTransactionManagerBatchUpdateAbortedBeforeFirstStatement() thro
assertThat(attempt.get()).isEqualTo(2);
// There should only be 1 CommitRequest, as the first attempt should abort already after the
// ExecuteBatchDmlRequest.
- assertThat(mockSpanner.getRequestTypes())
- .containsExactly(
- BatchCreateSessionsRequest.class,
- ExecuteBatchDmlRequest.class,
- BeginTransactionRequest.class,
- ExecuteBatchDmlRequest.class,
- CommitRequest.class);
+ if (isMultiplexedSessionsEnabled()) {
+ assertThat(mockSpanner.getRequestTypes())
+ .containsExactly(
+ CreateSessionRequest.class,
+ BatchCreateSessionsRequest.class,
+ ExecuteBatchDmlRequest.class,
+ BeginTransactionRequest.class,
+ ExecuteBatchDmlRequest.class,
+ CommitRequest.class);
+ } else {
+ assertThat(mockSpanner.getRequestTypes())
+ .containsExactly(
+ BatchCreateSessionsRequest.class,
+ ExecuteBatchDmlRequest.class,
+ BeginTransactionRequest.class,
+ ExecuteBatchDmlRequest.class,
+ CommitRequest.class);
+ }
}
@Test
@@ -785,14 +843,26 @@ public void asyncTransactionManagerWithBatchUpdateCommitAborted() throws Excepti
} finally {
mockSpanner.putStatementResult(StatementResult.update(UPDATE_STATEMENT, UPDATE_COUNT));
}
- assertThat(mockSpanner.getRequestTypes())
- .containsExactly(
- BatchCreateSessionsRequest.class,
- ExecuteBatchDmlRequest.class,
- CommitRequest.class,
- BeginTransactionRequest.class,
- ExecuteBatchDmlRequest.class,
- CommitRequest.class);
+ if (isMultiplexedSessionsEnabled()) {
+ assertThat(mockSpanner.getRequestTypes())
+ .containsExactly(
+ CreateSessionRequest.class,
+ BatchCreateSessionsRequest.class,
+ ExecuteBatchDmlRequest.class,
+ CommitRequest.class,
+ BeginTransactionRequest.class,
+ ExecuteBatchDmlRequest.class,
+ CommitRequest.class);
+ } else {
+ assertThat(mockSpanner.getRequestTypes())
+ .containsExactly(
+ BatchCreateSessionsRequest.class,
+ ExecuteBatchDmlRequest.class,
+ CommitRequest.class,
+ BeginTransactionRequest.class,
+ ExecuteBatchDmlRequest.class,
+ CommitRequest.class);
+ }
}
@Test
@@ -831,23 +901,46 @@ public void asyncTransactionManagerBatchUpdateAbortedWithoutGettingResult() thro
Iterable> requests = mockSpanner.getRequestTypes();
int size = Iterables.size(requests);
assertThat(size).isIn(Range.closed(5, 6));
- if (size == 5) {
- assertThat(requests)
- .containsExactly(
- BatchCreateSessionsRequest.class,
- ExecuteBatchDmlRequest.class,
- BeginTransactionRequest.class,
- ExecuteBatchDmlRequest.class,
- CommitRequest.class);
+ if (isMultiplexedSessionsEnabled()) {
+ if (size == 6) {
+ assertThat(requests)
+ .containsExactly(
+ CreateSessionRequest.class,
+ BatchCreateSessionsRequest.class,
+ ExecuteBatchDmlRequest.class,
+ BeginTransactionRequest.class,
+ ExecuteBatchDmlRequest.class,
+ CommitRequest.class);
+ } else {
+ assertThat(requests)
+ .containsExactly(
+ CreateSessionRequest.class,
+ BatchCreateSessionsRequest.class,
+ ExecuteBatchDmlRequest.class,
+ CommitRequest.class,
+ BeginTransactionRequest.class,
+ ExecuteBatchDmlRequest.class,
+ CommitRequest.class);
+ }
} else {
- assertThat(requests)
- .containsExactly(
- BatchCreateSessionsRequest.class,
- ExecuteBatchDmlRequest.class,
- CommitRequest.class,
- BeginTransactionRequest.class,
- ExecuteBatchDmlRequest.class,
- CommitRequest.class);
+ if (size == 5) {
+ assertThat(requests)
+ .containsExactly(
+ BatchCreateSessionsRequest.class,
+ ExecuteBatchDmlRequest.class,
+ BeginTransactionRequest.class,
+ ExecuteBatchDmlRequest.class,
+ CommitRequest.class);
+ } else {
+ assertThat(requests)
+ .containsExactly(
+ BatchCreateSessionsRequest.class,
+ ExecuteBatchDmlRequest.class,
+ CommitRequest.class,
+ BeginTransactionRequest.class,
+ ExecuteBatchDmlRequest.class,
+ CommitRequest.class);
+ }
}
}
@@ -875,9 +968,18 @@ public void asyncTransactionManagerWithBatchUpdateCommitFails() throws Exception
assertThat(e.getErrorCode()).isEqualTo(ErrorCode.RESOURCE_EXHAUSTED);
assertThat(e.getMessage()).contains("mutation limit exceeded");
}
- assertThat(mockSpanner.getRequestTypes())
- .containsExactly(
- BatchCreateSessionsRequest.class, ExecuteBatchDmlRequest.class, CommitRequest.class);
+ if (isMultiplexedSessionsEnabled()) {
+ assertThat(mockSpanner.getRequestTypes())
+ .containsExactly(
+ CreateSessionRequest.class,
+ BatchCreateSessionsRequest.class,
+ ExecuteBatchDmlRequest.class,
+ CommitRequest.class);
+ } else {
+ assertThat(mockSpanner.getRequestTypes())
+ .containsExactly(
+ BatchCreateSessionsRequest.class, ExecuteBatchDmlRequest.class, CommitRequest.class);
+ }
}
@Test
@@ -901,9 +1003,18 @@ public void asyncTransactionManagerWaitsUntilAsyncBatchUpdateHasFinished() throw
}
}
}
- assertThat(mockSpanner.getRequestTypes())
- .containsExactly(
- BatchCreateSessionsRequest.class, ExecuteBatchDmlRequest.class, CommitRequest.class);
+ if (isMultiplexedSessionsEnabled()) {
+ assertThat(mockSpanner.getRequestTypes())
+ .containsExactly(
+ CreateSessionRequest.class,
+ BatchCreateSessionsRequest.class,
+ ExecuteBatchDmlRequest.class,
+ CommitRequest.class);
+ } else {
+ assertThat(mockSpanner.getRequestTypes())
+ .containsExactly(
+ BatchCreateSessionsRequest.class, ExecuteBatchDmlRequest.class, CommitRequest.class);
+ }
}
@Test
@@ -1034,4 +1145,11 @@ public void onSuccess(Long aLong) {
assertThat(res.get(10L, TimeUnit.SECONDS)).isNull();
}
}
+
+ private boolean isMultiplexedSessionsEnabled() {
+ if (spanner.getOptions() == null || spanner.getOptions().getSessionPoolOptions() == null) {
+ return false;
+ }
+ return spanner.getOptions().getSessionPoolOptions().getUseMultiplexedSession();
+ }
}
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BaseSessionPoolTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BaseSessionPoolTest.java
index 743d21b587c..939114a7f60 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BaseSessionPoolTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BaseSessionPoolTest.java
@@ -85,15 +85,32 @@ SessionImpl mockSession() {
return session;
}
- SessionImpl buildMockSession(ReadContext context) {
- SpannerImpl spanner = mock(SpannerImpl.class);
+ SessionImpl mockMultiplexedSession() {
+ final SessionImpl session = mock(SessionImpl.class);
+ Map options = new HashMap<>();
+ when(session.getIsMultiplexed()).thenReturn(true);
+ when(session.getOptions()).thenReturn(options);
+ when(session.getName())
+ .thenReturn(
+ "projects/dummy/instances/dummy/database/dummy/sessions/session" + sessionIndex);
+ when(session.asyncClose()).thenReturn(ApiFutures.immediateFuture(Empty.getDefaultInstance()));
+ when(session.writeWithOptions(any(Iterable.class)))
+ .thenReturn(new CommitResponse(com.google.spanner.v1.CommitResponse.getDefaultInstance()));
+ when(session.writeAtLeastOnceWithOptions(any(Iterable.class)))
+ .thenReturn(new CommitResponse(com.google.spanner.v1.CommitResponse.getDefaultInstance()));
+ sessionIndex++;
+ return session;
+ }
+
+ SessionImpl buildMockSession(SpannerImpl spanner, ReadContext context) {
Map options = new HashMap<>();
options.put(Option.CHANNEL_HINT, channelHint.getAndIncrement());
final SessionImpl session =
new SessionImpl(
spanner,
- "projects/dummy/instances/dummy/databases/dummy/sessions/session" + sessionIndex,
- options) {
+ new SessionReference(
+ "projects/dummy/instances/dummy/databases/dummy/sessions/session" + sessionIndex,
+ options)) {
@Override
public ReadContext singleUse(TimestampBound bound) {
// The below stubs are added so that we can mock keep-alive.
@@ -122,16 +139,17 @@ public CommitResponse writeWithOptions(
return session;
}
- SessionImpl buildMockMultiplexedSession(ReadContext context, Timestamp creationTime) {
- SpannerImpl spanner = mock(SpannerImpl.class);
+ SessionImpl buildMockMultiplexedSession(
+ SpannerImpl spanner, ReadContext context, Timestamp creationTime) {
Map options = new HashMap<>();
final SessionImpl session =
new SessionImpl(
spanner,
- "projects/dummy/instances/dummy/databases/dummy/sessions/session" + sessionIndex,
- creationTime,
- true,
- options) {
+ new SessionReference(
+ "projects/dummy/instances/dummy/databases/dummy/sessions/session" + sessionIndex,
+ creationTime,
+ true,
+ options)) {
@Override
public ReadContext singleUse(TimestampBound bound) {
// The below stubs are added so that we can mock keep-alive.
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BatchClientImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BatchClientImplTest.java
index b7c4834044a..8d05df538a5 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BatchClientImplTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BatchClientImplTest.java
@@ -86,6 +86,9 @@ public void setUp() {
GrpcTransportOptions transportOptions = mock(GrpcTransportOptions.class);
when(transportOptions.getExecutorFactory()).thenReturn(mock(ExecutorFactory.class));
when(spannerOptions.getTransportOptions()).thenReturn(transportOptions);
+ SessionPoolOptions sessionPoolOptions = mock(SessionPoolOptions.class);
+ when(sessionPoolOptions.getPoolMaintainerClock()).thenReturn(Clock.INSTANCE);
+ when(spannerOptions.getSessionPoolOptions()).thenReturn(sessionPoolOptions);
@SuppressWarnings("resource")
SpannerImpl spanner = new SpannerImpl(gapicRpc, spannerOptions);
client = new BatchClientImpl(spanner.getSessionClient(db));
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BatchCreateSessionsSlowTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BatchCreateSessionsSlowTest.java
index b67123e0a58..4bcc5d07401 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BatchCreateSessionsSlowTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BatchCreateSessionsSlowTest.java
@@ -20,6 +20,7 @@
import static com.google.cloud.spanner.MockSpannerTestUtil.SELECT1;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
+import static org.junit.Assume.assumeFalse;
import com.google.api.gax.grpc.testing.LocalChannelProvider;
import com.google.cloud.NoCredentials;
@@ -85,15 +86,22 @@ public static void stopServer() throws InterruptedException {
@Before
public void setUp() {
+ SessionPoolOptions sessionPoolOptions =
+ SessionPoolOptions.newBuilder().setFailOnSessionLeak().build();
spanner =
SpannerOptions.newBuilder()
.setProjectId(TEST_PROJECT)
.setDatabaseRole(TEST_DATABASE_ROLE)
.setChannelProvider(channelProvider)
.setCredentials(NoCredentials.getInstance())
- .setSessionPoolOption(SessionPoolOptions.newBuilder().setFailOnSessionLeak().build())
+ .setSessionPoolOption(sessionPoolOptions)
.build()
.getService();
+ // BatchCreateSessions RPC is not invoked when multiplexed sessions is enabled and just RO
+ // transactions is used.
+ // Use a different transaction shape (for ex - RW transactions) which is presently unsupported
+ // with multiplexed sessions.
+ assumeFalse(sessionPoolOptions.getUseMultiplexedSession());
}
@After
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ChannelUsageTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ChannelUsageTest.java
index 5af4ad4d41f..4b1d3361cc6 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ChannelUsageTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ChannelUsageTest.java
@@ -18,6 +18,7 @@
import static io.grpc.Grpc.TRANSPORT_ATTR_REMOTE_ADDR;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assume.assumeFalse;
import com.google.cloud.NoCredentials;
import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult;
@@ -220,6 +221,9 @@ private SpannerOptions createSpannerOptions() {
@Test
public void testCreatesNumChannels() {
try (Spanner spanner = createSpannerOptions().getService()) {
+ assumeFalse(
+ "GRPC-GCP is currently not supported with multiplexed sessions",
+ isMultiplexedSessionsEnabled(spanner));
DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of("p", "i", "d"));
try (ResultSet resultSet = client.singleUse().executeQuery(SELECT1)) {
while (resultSet.next()) {}
@@ -231,6 +235,9 @@ public void testCreatesNumChannels() {
@Test
public void testUsesAllChannels() throws InterruptedException, ExecutionException {
try (Spanner spanner = createSpannerOptions().getService()) {
+ assumeFalse(
+ "GRPC-GCP is currently not supported with multiplexed sessions",
+ isMultiplexedSessionsEnabled(spanner));
DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of("p", "i", "d"));
ListeningExecutorService executor =
MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(numChannels * 2));
@@ -257,4 +264,11 @@ public void testUsesAllChannels() throws InterruptedException, ExecutionExceptio
}
assertEquals(numChannels, executeSqlLocalIps.size());
}
+
+ private boolean isMultiplexedSessionsEnabled(Spanner spanner) {
+ if (spanner.getOptions() == null || spanner.getOptions().getSessionPoolOptions() == null) {
+ return false;
+ }
+ return spanner.getOptions().getSessionPoolOptions().getUseMultiplexedSession();
+ }
}
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 057d85bb346..2a0c9c77c36 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
@@ -32,6 +32,8 @@
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -2318,7 +2320,9 @@ public void singleUse() {
assertThat(checkedOut).isEmpty();
try (ResultSet rs = client.singleUse().executeQuery(SELECT1)) {
assertThat(rs.next()).isTrue();
- assertThat(checkedOut).hasSize(1);
+ if (!isMultiplexedSessionsEnabled()) {
+ assertThat(checkedOut).hasSize(1);
+ }
assertThat(rs.getLong(0)).isEqualTo(1L);
assertThat(rs.next()).isFalse();
}
@@ -3009,6 +3013,8 @@ public void testDatabaseOrInstanceDoesNotExistOnCreate() {
"Instance", SpannerExceptionFactory.INSTANCE_RESOURCE_TYPE, INSTANCE_NAME)
};
for (StatusRuntimeException exception : exceptions) {
+ mockSpanner.setCreateSessionExecutionTime(
+ SimulatedExecutionTime.ofStickyException(exception));
mockSpanner.setBatchCreateSessionsExecutionTime(
SimulatedExecutionTime.ofStickyException(exception));
// Ensure there are no sessions in the pool by default.
@@ -3175,6 +3181,8 @@ public void testGetInvalidatedClientMultipleTimes() {
"Instance", SpannerExceptionFactory.INSTANCE_RESOURCE_TYPE, INSTANCE_NAME)
};
for (StatusRuntimeException exception : exceptions) {
+ mockSpanner.setCreateSessionExecutionTime(
+ SimulatedExecutionTime.ofStickyException(exception));
mockSpanner.setBatchCreateSessionsExecutionTime(
SimulatedExecutionTime.ofStickyException(exception));
try (Spanner spanner =
@@ -3648,6 +3656,9 @@ public void testBatchCreateSessionsPermissionDenied() {
mockSpanner.setBatchCreateSessionsExecutionTime(
SimulatedExecutionTime.ofStickyException(
Status.PERMISSION_DENIED.withDescription("Not permitted").asRuntimeException()));
+ mockSpanner.setCreateSessionExecutionTime(
+ SimulatedExecutionTime.ofStickyException(
+ Status.PERMISSION_DENIED.withDescription("Not permitted").asRuntimeException()));
try (Spanner spanner =
SpannerOptions.newBuilder()
.setProjectId("my-project")
@@ -3662,6 +3673,9 @@ public void testBatchCreateSessionsPermissionDenied() {
// Actually trying to get any results will cause an exception.
SpannerException e = assertThrows(SpannerException.class, rs::next);
assertEquals(ErrorCode.PERMISSION_DENIED, e.getErrorCode());
+ } finally {
+ mockSpanner.setBatchCreateSessionsExecutionTime(SimulatedExecutionTime.none());
+ mockSpanner.setCreateSessionExecutionTime(SimulatedExecutionTime.none());
}
}
@@ -3747,6 +3761,9 @@ public void testSpecificTimeout() {
@Test
public void testBatchCreateSessionsFailure_shouldNotPropagateToCloseMethod() {
+ assumeFalse(
+ "BatchCreateSessions RPC is not invoked for multiplexed sessions",
+ isMultiplexedSessionsEnabled());
try {
// Simulate session creation failures on the backend.
mockSpanner.setBatchCreateSessionsExecutionTime(
@@ -3765,6 +3782,28 @@ public void testBatchCreateSessionsFailure_shouldNotPropagateToCloseMethod() {
}
}
+ @Test
+ public void testCreateSessionsFailure_shouldNotPropagateToCloseMethod() {
+ assumeTrue(
+ "CreateSessions is not invoked for regular sessions", isMultiplexedSessionsEnabled());
+ try {
+ // Simulate session creation failures on the backend.
+ mockSpanner.setCreateSessionExecutionTime(
+ SimulatedExecutionTime.ofStickyException(Status.RESOURCE_EXHAUSTED.asRuntimeException()));
+ DatabaseClient client =
+ spannerWithEmptySessionPool.getDatabaseClient(
+ DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE));
+ // This will not cause any failure as getting a session from the pool is guaranteed to be
+ // non-blocking, and any exceptions will be delayed until actual query execution.
+ try (ResultSet rs = client.singleUse().executeQuery(SELECT1)) {
+ SpannerException e = assertThrows(SpannerException.class, rs::next);
+ assertThat(e.getErrorCode()).isEqualTo(ErrorCode.RESOURCE_EXHAUSTED);
+ }
+ } finally {
+ mockSpanner.setCreateSessionExecutionTime(SimulatedExecutionTime.none());
+ }
+ }
+
@Test
public void testReadWriteTransaction_usesOptions() {
SessionPool pool = mock(SessionPool.class);
@@ -5237,4 +5276,11 @@ private ListValue getRows(Dialect dialect) {
return valuesBuilder.build();
}
+
+ private boolean isMultiplexedSessionsEnabled() {
+ if (spanner.getOptions() == null || spanner.getOptions().getSessionPoolOptions() == null) {
+ return false;
+ }
+ return spanner.getOptions().getSessionPoolOptions().getUseMultiplexedSession();
+ }
}
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DefaultBenchmark.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DefaultBenchmark.java
index d966d58dbd5..7579c4328d2 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DefaultBenchmark.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DefaultBenchmark.java
@@ -31,7 +31,6 @@
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
-import org.openjdk.jmh.annotations.AuxCounters;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
@@ -69,8 +68,7 @@
@Warmup(iterations = 0)
public class DefaultBenchmark extends AbstractLatencyBenchmark {
- @State(Scope.Thread)
- @AuxCounters(org.openjdk.jmh.annotations.AuxCounters.Type.EVENTS)
+ @State(Scope.Benchmark)
public static class BenchmarkState {
// TODO(developer): Add your values here for PROJECT_ID, INSTANCE_ID, DATABASE_ID
@@ -97,6 +95,7 @@ public void setup() throws Exception {
.setWaitForMinSessions(org.threeten.bp.Duration.ofSeconds(20))
.build())
.setHost(SERVER_URL)
+ .setNumChannels(NUM_GRPC_CHANNELS)
.build();
spanner = options.getService();
client =
@@ -123,7 +122,8 @@ public void burstQueries(final BenchmarkState server) throws Exception {
MoreExecutors.listeningDecorator(Executors.newScheduledThreadPool(PARALLEL_THREADS));
List>> results = new ArrayList<>(PARALLEL_THREADS);
for (int i = 0; i < PARALLEL_THREADS; i++) {
- results.add(service.submit(() -> runBenchmarksForQueries(server, TOTAL_READS_PER_RUN)));
+ results.add(
+ service.submit(() -> runBenchmarksForSingleUseQueries(server, TOTAL_READS_PER_RUN)));
}
collectResultsAndPrint(service, results, TOTAL_READS_PER_RUN);
}
@@ -140,7 +140,8 @@ public void burstQueriesAndWrites(final BenchmarkState server) throws Exception
MoreExecutors.listeningDecorator(Executors.newScheduledThreadPool(PARALLEL_THREADS));
List>> results = new ArrayList<>(PARALLEL_THREADS);
for (int i = 0; i < PARALLEL_THREADS; i++) {
- results.add(service.submit(() -> runBenchmarksForQueries(server, TOTAL_READS_PER_RUN)));
+ results.add(
+ service.submit(() -> runBenchmarksForSingleUseQueries(server, TOTAL_READS_PER_RUN)));
}
for (int i = 0; i < PARALLEL_THREADS; i++) {
results.add(service.submit(() -> runBenchmarkForUpdates(server, TOTAL_WRITES_PER_RUN)));
@@ -167,25 +168,25 @@ public void burstUpdates(final BenchmarkState server) throws Exception {
collectResultsAndPrint(service, results, TOTAL_WRITES_PER_RUN);
}
- private List runBenchmarksForQueries(
+ private List runBenchmarksForSingleUseQueries(
final BenchmarkState server, int numberOfOperations) {
List results = new ArrayList<>(numberOfOperations);
// Execute one query to make sure everything has been warmed up.
executeWarmup(server);
for (int i = 0; i < numberOfOperations; i++) {
- results.add(executeQuery(server));
+ results.add(executeSingleUseQuery(server));
}
return results;
}
private void executeWarmup(final BenchmarkState server) {
for (int i = 0; i < WARMUP_REQUEST_COUNT; i++) {
- executeQuery(server);
+ executeSingleUseQuery(server);
}
}
- private java.time.Duration executeQuery(final BenchmarkState server) {
+ private java.time.Duration executeSingleUseQuery(final BenchmarkState server) {
Stopwatch watch = Stopwatch.createStarted();
try (ResultSet rs = server.client.singleUse().executeQuery(getRandomisedReadStatement())) {
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/FailOnOverkillTraceComponentImpl.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/FailOnOverkillTraceComponentImpl.java
index 4d9b4ddd805..9e54af9c4e7 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/FailOnOverkillTraceComponentImpl.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/FailOnOverkillTraceComponentImpl.java
@@ -45,6 +45,7 @@
import io.opencensus.trace.propagation.PropagationComponent;
import io.opencensus.trace.propagation.TextFormat;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.EnumSet;
import java.util.LinkedHashMap;
import java.util.List;
@@ -62,7 +63,8 @@ public class FailOnOverkillTraceComponentImpl extends TraceComponent {
private final Clock clock = ZeroTimeClock.getInstance();
private final ExportComponent exportComponent = new TestExportComponent();
private final TraceConfig traceConfig = new TestTraceConfig();
- private static final Map spans = new LinkedHashMap<>();
+ private static final Map spans =
+ Collections.synchronizedMap(new LinkedHashMap<>());
private static final List annotations = new ArrayList<>();
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/GceTestEnvConfig.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/GceTestEnvConfig.java
index 7a27123bdc3..efb012ba8e2 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/GceTestEnvConfig.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/GceTestEnvConfig.java
@@ -97,6 +97,7 @@ public GceTestEnvConfig() {
customChannelProviderBuilder
.setEndpoint(DIRECT_PATH_ENDPOINT)
.setAttemptDirectPath(true)
+ .setAttemptDirectPathXds()
.setInterceptorProvider(interceptorProvider);
builder.setChannelProvider(customChannelProviderBuilder.build());
}
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 62a25f9dc4d..69004a4913b 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
@@ -18,6 +18,7 @@
import com.google.cloud.spanner.SessionPool.PooledSession;
import com.google.cloud.spanner.SessionPool.PooledSessionFuture;
+import com.google.cloud.spanner.SessionPool.SessionFutureWrapper;
import com.google.cloud.spanner.testing.RemoteSpannerHelper;
/**
@@ -86,6 +87,20 @@ PooledSessionFuture getSession() {
return session;
}
+ @Override
+ SessionFutureWrapper getMultiplexedSession() {
+ SessionFutureWrapper session = super.getMultiplexedSession();
+ if (invalidateNextSession) {
+ session.get().get().getDelegate().close();
+ session.get().get().setAllowReplacing(false);
+ awaitDeleted(session.get().get().getDelegate());
+ session.get().get().setAllowReplacing(allowReplacing);
+ invalidateNextSession = false;
+ }
+ session.get().get().setAllowReplacing(allowReplacing);
+ return session;
+ }
+
/**
* Deleting a session server side takes some time. This method checks and waits until the
* session really has been deleted.
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 18973bd2d9d..07e8bdae1bc 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
@@ -585,6 +585,8 @@ private static void checkStreamException(
private ConcurrentMap partialStatementResults =
new ConcurrentHashMap<>();
private ConcurrentMap sessions = new ConcurrentHashMap<>();
+ private ConcurrentMap multiplexedSessions = new ConcurrentHashMap<>();
+
private ConcurrentMap sessionLastUsed = new ConcurrentHashMap<>();
private ConcurrentMap transactions = new ConcurrentHashMap<>();
private final Queue transactionsStarted = new ConcurrentLinkedQueue<>();
@@ -828,7 +830,7 @@ public void batchCreateSessions(
response.addSession(session);
numSessionsCreated.incrementAndGet();
} else {
- sessions.remove(name);
+ removeSession(name);
}
} else {
// Someone else tried to create a session with the same id. This should not be possible
@@ -839,12 +841,12 @@ public void batchCreateSessions(
responseObserver.onCompleted();
} catch (StatusRuntimeException e) {
if (name != null) {
- sessions.remove(name);
+ removeSession(name);
}
responseObserver.onError(e);
} catch (Throwable e) {
if (name != null) {
- sessions.remove(name);
+ removeSession(name);
}
responseObserver.onError(
Status.INTERNAL
@@ -872,7 +874,7 @@ public void createSession(
.setApproximateLastUseTime(now)
.setMultiplexed(requestSession.getMultiplexed())
.build();
- Session prev = sessions.putIfAbsent(name, session);
+ Session prev = addSession(session);
if (prev == null) {
sessionLastUsed.put(name, Instant.now());
numSessionsCreated.incrementAndGet();
@@ -883,10 +885,10 @@ public void createSession(
responseObserver.onError(Status.ALREADY_EXISTS.asRuntimeException());
}
} catch (StatusRuntimeException e) {
- sessions.remove(name);
+ removeSession(name);
responseObserver.onError(e);
} catch (Throwable e) {
- sessions.remove(name);
+ removeSession(name);
responseObserver.onError(
Status.INTERNAL
.withDescription("Create session failed: " + e.getMessage())
@@ -900,7 +902,7 @@ public void getSession(GetSessionRequest request, StreamObserver respon
Preconditions.checkNotNull(request.getName());
try {
getSessionExecutionTime.simulateExecutionTime(exceptions, stickyGlobalExceptions, freezeLock);
- Session session = sessions.get(request.getName());
+ Session session = getSession(request.getName());
if (session == null) {
setSessionNotFound(request.getName(), responseObserver);
} else {
@@ -969,7 +971,7 @@ public void deleteSession(DeleteSessionRequest request, StreamObserver re
try {
deleteSessionExecutionTime.simulateExecutionTime(
exceptions, stickyGlobalExceptions, freezeLock);
- Session session = sessions.get(request.getName());
+ Session session = getSession(request.getName());
if (session != null) {
try {
doDeleteSession(session);
@@ -986,7 +988,7 @@ public void deleteSession(DeleteSessionRequest request, StreamObserver re
}
void doDeleteSession(Session session) {
- sessions.remove(session.getName());
+ removeSession(session.getName());
transactionCounters.remove(session.getName());
sessionLastUsed.remove(session.getName());
}
@@ -995,7 +997,7 @@ void doDeleteSession(Session session) {
public void executeSql(ExecuteSqlRequest request, StreamObserver responseObserver) {
requests.add(request);
Preconditions.checkNotNull(request.getSession());
- Session session = sessions.get(request.getSession());
+ Session session = getSession(request.getSession());
if (session == null) {
setSessionNotFound(request.getSession(), responseObserver);
return;
@@ -1081,7 +1083,7 @@ public void executeBatchDml(
ExecuteBatchDmlRequest request, StreamObserver responseObserver) {
requests.add(request);
Preconditions.checkNotNull(request.getSession());
- Session session = sessions.get(request.getSession());
+ Session session = getSession(request.getSession());
if (session == null) {
setSessionNotFound(request.getSession(), responseObserver);
return;
@@ -1184,7 +1186,7 @@ public void executeStreamingSql(
requests.add(request);
}
Preconditions.checkNotNull(request.getSession());
- Session session = sessions.get(request.getSession());
+ Session session = getSession(request.getSession());
if (session == null) {
setSessionNotFound(request.getSession(), responseObserver);
return;
@@ -1586,7 +1588,7 @@ private void throwTransactionAborted(ByteString transactionId) {
public void read(final ReadRequest request, StreamObserver responseObserver) {
requests.add(request);
Preconditions.checkNotNull(request.getSession());
- Session session = sessions.get(request.getSession());
+ Session session = getSession(request.getSession());
if (session == null) {
setSessionNotFound(request.getSession(), responseObserver);
return;
@@ -1619,7 +1621,7 @@ public void streamingRead(
final ReadRequest request, StreamObserver responseObserver) {
requests.add(request);
Preconditions.checkNotNull(request.getSession());
- Session session = sessions.get(request.getSession());
+ Session session = getSession(request.getSession());
if (session == null) {
setSessionNotFound(request.getSession(), responseObserver);
return;
@@ -1822,7 +1824,7 @@ public void beginTransaction(
BeginTransactionRequest request, StreamObserver responseObserver) {
requests.add(request);
Preconditions.checkNotNull(request.getSession());
- Session session = sessions.get(request.getSession());
+ Session session = getSession(request.getSession());
if (session == null) {
setSessionNotFound(request.getSession(), responseObserver);
return;
@@ -1884,7 +1886,10 @@ private void setReadTimestamp(TransactionOptions options, Transaction.Builder bu
}
private void simulateAbort(Session session, ByteString transactionId) {
- ensureMostRecentTransaction(session, transactionId);
+ if (!session.getMultiplexed()) {
+ // multiplexed sessions allow concurrent transactions on a single session.
+ ensureMostRecentTransaction(session, transactionId);
+ }
if (isReadWriteTransaction(transactionId)) {
if (abortNextStatement.getAndSet(false) || abortProbability > random.nextDouble()) {
rollbackTransaction(transactionId);
@@ -1933,7 +1938,7 @@ private void ensureMostRecentTransaction(Session session, ByteString transaction
public void commit(CommitRequest request, StreamObserver responseObserver) {
requests.add(request);
Preconditions.checkNotNull(request.getSession());
- Session session = sessions.get(request.getSession());
+ Session session = getSession(request.getSession());
if (session == null) {
setSessionNotFound(request.getSession(), responseObserver);
return;
@@ -1995,7 +2000,7 @@ public void batchWrite(
BatchWriteRequest request, StreamObserver responseObserver) {
requests.add(request);
Preconditions.checkNotNull(request.getSession());
- Session session = sessions.get(request.getSession());
+ Session session = getSession(request.getSession());
if (session == null) {
setSessionNotFound(request.getSession(), responseObserver);
return;
@@ -2023,7 +2028,7 @@ private void commitTransaction(ByteString transactionId) {
public void rollback(RollbackRequest request, StreamObserver responseObserver) {
requests.add(request);
Preconditions.checkNotNull(request.getTransactionId());
- Session session = sessions.get(request.getSession());
+ Session session = getSession(request.getSession());
if (session == null) {
setSessionNotFound(request.getSession(), responseObserver);
return;
@@ -2100,7 +2105,7 @@ private void partition(
TransactionSelector transactionSelector,
PartitionOptions options,
StreamObserver responseObserver) {
- Session session = sessions.get(sessionName);
+ Session session = getSession(sessionName);
if (session == null) {
setSessionNotFound(sessionName, responseObserver);
return;
@@ -2133,6 +2138,10 @@ public int numSessionsCreated() {
return numSessionsCreated.get();
}
+ public Map getSessions() {
+ return sessions;
+ }
+
@Override
public List getRequests() {
return new ArrayList<>(this.requests);
@@ -2399,4 +2408,31 @@ public SimulatedExecutionTime getStreamingReadExecutionTime() {
public void setStreamingReadExecutionTime(SimulatedExecutionTime streamingReadExecutionTime) {
this.streamingReadExecutionTime = Preconditions.checkNotNull(streamingReadExecutionTime);
}
+
+ Session addSession(Session session) {
+ Session prev;
+ if (session.getMultiplexed()) {
+ prev = multiplexedSessions.putIfAbsent(session.getName(), session);
+ } else {
+ prev = sessions.putIfAbsent(session.getName(), session);
+ }
+ return prev;
+ }
+
+ void removeSession(String name) {
+ if (multiplexedSessions.containsKey(name)) {
+ multiplexedSessions.remove(name);
+ } else {
+ sessions.remove(name);
+ }
+ }
+
+ Session getSession(String name) {
+ if (multiplexedSessions.containsKey(name)) {
+ return multiplexedSessions.get(name);
+ } else if (sessions.containsKey(name)) {
+ return sessions.get(name);
+ }
+ return null;
+ }
}
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionMaintainerTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionMaintainerTest.java
index 2c3ada10803..457004a18fb 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionMaintainerTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionMaintainerTest.java
@@ -28,7 +28,7 @@
import static org.mockito.MockitoAnnotations.initMocks;
import com.google.cloud.Timestamp;
-import com.google.cloud.spanner.SessionPool.MultiplexedSession;
+import com.google.cloud.spanner.SessionPool.CachedSession;
import com.google.cloud.spanner.SessionPool.MultiplexedSessionInitializationConsumer;
import com.google.cloud.spanner.SessionPool.MultiplexedSessionMaintainerConsumer;
import com.google.cloud.spanner.SessionPool.Position;
@@ -37,8 +37,11 @@
import io.opentelemetry.api.OpenTelemetry;
import java.util.ArrayList;
import java.util.List;
+import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
+import java.util.stream.Collectors;
+import org.junit.Assume;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -57,7 +60,7 @@ public class MultiplexedSessionMaintainerTest extends BaseSessionPoolTest {
private DatabaseId db = DatabaseId.of("projects/p/instances/i/databases/unused");
private SessionPoolOptions options;
private FakeClock clock = new FakeClock();
- private List multiplexedSessionsRemoved = new ArrayList<>();
+ private List multiplexedSessionsRemoved = new ArrayList<>();
@Before
public void setUp() {
@@ -75,13 +78,16 @@ public void setUp() {
.setIncStep(1)
.setKeepAliveIntervalMinutes(2)
.setUseMultiplexedSession(true)
+ .setPoolMaintainerClock(clock)
.build();
+ when(spannerOptions.getSessionPoolOptions()).thenReturn(options);
+ Assume.assumeTrue(options.getUseMultiplexedSession());
multiplexedSessionsRemoved.clear();
}
@Test
public void testMaintainMultiplexedSession_whenNewSessionCreated_assertThatStaleSessionIsRemoved()
- throws InterruptedException {
+ throws Exception {
doAnswer(
invocation -> {
MultiplexedSessionInitializationConsumer consumer =
@@ -92,11 +98,12 @@ public void testMaintainMultiplexedSession_whenNewSessionCreated_assertThatStale
Instant.ofEpochMilli(clock.currentTimeMillis.get()).getEpochSecond(), 0);
consumer.onSessionReady(
setupMockSession(
- buildMockMultiplexedSession(mockContext, timestamp.toProto()), mockContext));
+ buildMockMultiplexedSession(client, mockContext, timestamp.toProto()),
+ mockContext));
return null;
})
.when(sessionClient)
- .createMultiplexedSession(any(MultiplexedSessionInitializationConsumer.class));
+ .asyncCreateMultiplexedSession(any(MultiplexedSessionInitializationConsumer.class));
doAnswer(
invocation -> {
MultiplexedSessionMaintainerConsumer consumer =
@@ -107,16 +114,17 @@ public void testMaintainMultiplexedSession_whenNewSessionCreated_assertThatStale
Instant.ofEpochMilli(clock.currentTimeMillis.get()).getEpochSecond(), 0);
consumer.onSessionReady(
setupMockSession(
- buildMockMultiplexedSession(mockContext, timestamp.toProto()), mockContext));
+ buildMockMultiplexedSession(client, mockContext, timestamp.toProto()),
+ mockContext));
return null;
})
.when(sessionClient)
- .createMultiplexedSession(any(MultiplexedSessionMaintainerConsumer.class));
+ .asyncCreateMultiplexedSession(any(MultiplexedSessionMaintainerConsumer.class));
SessionPool pool = createPool();
// Run one maintenance loop.
- SessionFutureWrapper session1 = pool.getMultiplexedSessionWithFallback();
+ CachedSession session1 = pool.getMultiplexedSessionWithFallback().get().get();
runMaintenanceLoop(clock, pool, 1);
assertTrue(multiplexedSessionsRemoved.isEmpty());
@@ -126,10 +134,11 @@ public void testMaintainMultiplexedSession_whenNewSessionCreated_assertThatStale
// Run second maintenance loop. the first session would now be stale since it has now existed
// for more than 7 days.
runMaintenanceLoop(clock, pool, 1);
- SessionFutureWrapper session2 = pool.getMultiplexedSessionWithFallback();
- assertNotEquals(session1.get().getName(), session2.get().getName());
+
+ CachedSession session2 = pool.getMultiplexedSessionWithFallback().get().get();
+ assertNotEquals(session1.getName(), session2.getName());
assertEquals(1, multiplexedSessionsRemoved.size());
- assertTrue(multiplexedSessionsRemoved.contains(session1.get().get()));
+ assertTrue(getNameOfSessionRemoved().contains(session1.getName()));
// Advance clock by 8 days
clock.currentTimeMillis.addAndGet(Duration.ofDays(8).toMillis());
@@ -138,10 +147,10 @@ public void testMaintainMultiplexedSession_whenNewSessionCreated_assertThatStale
// for more than 7 days
runMaintenanceLoop(clock, pool, 1);
- SessionFutureWrapper session3 = pool.getMultiplexedSessionWithFallback();
- assertNotEquals(session2.get().getName(), session3.get().getName());
+ CachedSession session3 = pool.getMultiplexedSessionWithFallback().get().get();
+ assertNotEquals(session2.getName(), session3.getName());
assertEquals(2, multiplexedSessionsRemoved.size());
- assertTrue(multiplexedSessionsRemoved.contains(session2.get().get()));
+ assertTrue(getNameOfSessionRemoved().contains(session2.getName()));
}
@Test
@@ -157,11 +166,12 @@ public void testMaintainMultiplexedSession_whenNewSessionCreated_assertThatStale
Instant.ofEpochMilli(clock.currentTimeMillis.get()).getEpochSecond(), 0);
consumer.onSessionReady(
setupMockSession(
- buildMockMultiplexedSession(mockContext, timestamp.toProto()), mockContext));
+ buildMockMultiplexedSession(client, mockContext, timestamp.toProto()),
+ mockContext));
return null;
})
.when(sessionClient)
- .createMultiplexedSession(any(MultiplexedSessionInitializationConsumer.class));
+ .asyncCreateMultiplexedSession(any(MultiplexedSessionInitializationConsumer.class));
SessionPool pool = createPool();
// Run one maintenance loop.
@@ -192,11 +202,12 @@ public void testMaintainMultiplexedSession_whenNewSessionCreated_assertThatStale
Instant.ofEpochMilli(clock.currentTimeMillis.get()).getEpochSecond(), 0);
consumer.onSessionReady(
setupMockSession(
- buildMockMultiplexedSession(mockContext, timestamp.toProto()), mockContext));
+ buildMockMultiplexedSession(client, mockContext, timestamp.toProto()),
+ mockContext));
return null;
})
.when(sessionClient)
- .createMultiplexedSession(any(MultiplexedSessionInitializationConsumer.class));
+ .asyncCreateMultiplexedSession(any(MultiplexedSessionInitializationConsumer.class));
doAnswer(
invocation -> {
MultiplexedSessionMaintainerConsumer consumer =
@@ -206,7 +217,7 @@ public void testMaintainMultiplexedSession_whenNewSessionCreated_assertThatStale
return null;
})
.when(sessionClient)
- .createMultiplexedSession(any(MultiplexedSessionMaintainerConsumer.class));
+ .asyncCreateMultiplexedSession(any(MultiplexedSessionMaintainerConsumer.class));
SessionPool pool = createPool();
// Advance clock by 8 days
@@ -217,7 +228,7 @@ public void testMaintainMultiplexedSession_whenNewSessionCreated_assertThatStale
runMaintenanceLoop(clock, pool, 1);
assertTrue(multiplexedSessionsRemoved.isEmpty());
verify(sessionClient, times(1))
- .createMultiplexedSession(any(MultiplexedSessionMaintainerConsumer.class));
+ .asyncCreateMultiplexedSession(any(MultiplexedSessionMaintainerConsumer.class));
// Advance clock by 10s and now mock session creation to be successful.
clock.currentTimeMillis.addAndGet(Duration.ofSeconds(10).toMillis());
@@ -231,11 +242,12 @@ public void testMaintainMultiplexedSession_whenNewSessionCreated_assertThatStale
Instant.ofEpochMilli(clock.currentTimeMillis.get()).getEpochSecond(), 0);
consumer.onSessionReady(
setupMockSession(
- buildMockMultiplexedSession(mockContext, timestamp.toProto()), mockContext));
+ buildMockMultiplexedSession(client, mockContext, timestamp.toProto()),
+ mockContext));
return null;
})
.when(sessionClient)
- .createMultiplexedSession(any(MultiplexedSessionMaintainerConsumer.class));
+ .asyncCreateMultiplexedSession(any(MultiplexedSessionMaintainerConsumer.class));
// Run one maintenance loop. Attempt should be ignored as it has not been 10 minutes since last
// attempt.
runMaintenanceLoop(clock, pool, 1);
@@ -243,7 +255,7 @@ public void testMaintainMultiplexedSession_whenNewSessionCreated_assertThatStale
assertTrue(multiplexedSessionsRemoved.isEmpty());
assertEquals(session1.get().getName(), session2.get().getName());
verify(sessionClient, times(1))
- .createMultiplexedSession(any(MultiplexedSessionMaintainerConsumer.class));
+ .asyncCreateMultiplexedSession(any(MultiplexedSessionMaintainerConsumer.class));
// Advance clock by 15 minutes
clock.currentTimeMillis.addAndGet(Duration.ofMinutes(15).toMillis());
@@ -251,10 +263,10 @@ public void testMaintainMultiplexedSession_whenNewSessionCreated_assertThatStale
// the last attempt.
runMaintenanceLoop(clock, pool, 1);
SessionFutureWrapper session3 = pool.getMultiplexedSessionWithFallback();
- assertTrue(multiplexedSessionsRemoved.contains(session1.get().get()));
+ assertTrue(getNameOfSessionRemoved().contains(session1.get().get().getName()));
assertNotEquals(session1.get().getName(), session3.get().getName());
verify(sessionClient, times(2))
- .createMultiplexedSession(any(MultiplexedSessionMaintainerConsumer.class));
+ .asyncCreateMultiplexedSession(any(MultiplexedSessionMaintainerConsumer.class));
}
private SessionImpl setupMockSession(final SessionImpl session, final ReadContext mockContext) {
@@ -283,4 +295,10 @@ private SessionPool createPool() {
};
return pool;
}
+
+ Set getNameOfSessionRemoved() {
+ return multiplexedSessionsRemoved.stream()
+ .map(session -> session.getName())
+ .collect(Collectors.toSet());
+ }
}
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionPoolTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionPoolTest.java
index d2aae568451..5e96e519fc7 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionPoolTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionPoolTest.java
@@ -36,6 +36,7 @@
import io.opentelemetry.api.OpenTelemetry;
import java.io.PrintWriter;
import java.io.StringWriter;
+import org.junit.Assume;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
@@ -81,6 +82,8 @@ public void setUp() {
.setMaxSessions(2)
.setUseMultiplexedSession(true)
.build();
+ when(spannerOptions.getSessionPoolOptions()).thenReturn(options);
+ Assume.assumeTrue(options.getUseMultiplexedSession());
}
@Test
@@ -97,7 +100,7 @@ public void testGetMultiplexedSession_whenSessionInitializationSucceeded_assertS
assertNotNull(multiplexedSessionFuture.get());
}
verify(sessionClient, times(1))
- .createMultiplexedSession(any(MultiplexedSessionInitializationConsumer.class));
+ .asyncCreateMultiplexedSession(any(MultiplexedSessionInitializationConsumer.class));
}
@Test
@@ -136,7 +139,7 @@ public void testGetMultiplexedSession_whenSessionCreationFailed_assertErrorForWa
return null;
})
.when(sessionClient)
- .createMultiplexedSession(any(MultiplexedSessionInitializationConsumer.class));
+ .asyncCreateMultiplexedSession(any(MultiplexedSessionInitializationConsumer.class));
options =
options
.toBuilder()
@@ -151,7 +154,7 @@ public void testGetMultiplexedSession_whenSessionCreationFailed_assertErrorForWa
for (int i = 0; i < 5; i++) {
SpannerException e =
assertThrows(
- SpannerException.class, () -> pool.getMultiplexedSessionWithFallback().get());
+ SpannerException.class, () -> pool.getMultiplexedSessionWithFallback().get().get());
assertEquals(ErrorCode.DEADLINE_EXCEEDED, e.getErrorCode());
}
// assert that all 5 requests failed with exception
@@ -168,6 +171,6 @@ private void setupMockMultiplexedSessionCreation() {
return null;
})
.when(sessionClient)
- .createMultiplexedSession(any(MultiplexedSessionInitializationConsumer.class));
+ .asyncCreateMultiplexedSession(any(MultiplexedSessionInitializationConsumer.class));
}
}
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionsBenchmark.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionsBenchmark.java
new file mode 100644
index 00000000000..ff976141d96
--- /dev/null
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionsBenchmark.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.spanner;
+
+import static com.google.cloud.spanner.BenchmarkingUtilityScripts.collectResults;
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import com.google.common.base.Stopwatch;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningScheduledExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadLocalRandom;
+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 measuring existing latencies of various APIs using the Java Client with
+ * multiplexed sessions. The benchmarks are bound to the Maven profile `benchmark` and can be
+ * executed like this:
+ * mvn clean test -DskipTests -Pbenchmark -Dbenchmark.name=MultiplexedSessionsBenchmark
+ * Test Table Schema :
+ *
+ *
CREATE TABLE FOO ( id INT64 NOT NULL, BAZ INT64, BAR INT64, ) PRIMARY KEY(id);
+ *
+ *
Below are a few considerations here: 1. We use all default options with multiplexed sessions
+ * for this test because that is what most customers would be using. 2. The test schema uses a
+ * numeric primary key. To ensure that the reads/updates are distributed across a large query space,
+ * we insert 10^5 records. Utility at {@link BenchmarkingUtilityScripts} can be used for loading
+ * data. 3. For queries, we make sure that the query is sampled randomly across a large query space.
+ * This ensure we don't cause hot-spots. 4. For avoid cold start issues, we execute 1 query/update
+ * and ignore its latency from the final reported metrics.
+ */
+@BenchmarkMode(Mode.AverageTime)
+@Fork(value = 1, warmups = 0)
+@Measurement(batchSize = 1, iterations = 1, timeUnit = TimeUnit.MILLISECONDS)
+@OutputTimeUnit(TimeUnit.SECONDS)
+@Warmup(iterations = 0)
+public class MultiplexedSessionsBenchmark extends AbstractLatencyBenchmark {
+
+ @State(Scope.Benchmark)
+ public static class BenchmarkState {
+
+ // TODO(developer): Add your values here for PROJECT_ID, INSTANCE_ID, DATABASE_ID
+ private static final String INSTANCE_ID = "";
+ private static final String DATABASE_ID = "";
+ private static final String SERVER_URL = "https://staging-wrenchworks.sandbox.googleapis.com";
+ private Spanner spanner;
+ private DatabaseClientImpl client;
+
+ @Param({"100"})
+ int minSessions;
+
+ @Param({"400"})
+ int maxSessions;
+
+ @Setup(Level.Iteration)
+ public void setup() throws Exception {
+ SpannerOptions.enableOpenTelemetryMetrics();
+ SpannerOptions.enableOpenTelemetryTraces();
+ SpannerOptions options =
+ SpannerOptions.newBuilder()
+ .setSessionPoolOption(
+ SessionPoolOptions.newBuilder()
+ .setMinSessions(minSessions)
+ .setMaxSessions(maxSessions)
+ .setWaitForMinSessions(org.threeten.bp.Duration.ofSeconds(20))
+ .setUseMultiplexedSession(true)
+ .build())
+ .setHost(SERVER_URL)
+ .setNumChannels(NUM_GRPC_CHANNELS)
+ .build();
+ spanner = options.getService();
+ client =
+ (DatabaseClientImpl)
+ spanner.getDatabaseClient(
+ DatabaseId.of(options.getProjectId(), INSTANCE_ID, DATABASE_ID));
+ }
+
+ @TearDown(Level.Iteration)
+ public void teardown() throws Exception {
+ spanner.close();
+ }
+ }
+
+ /** Measures the time needed to execute a burst of queries. */
+ @Benchmark
+ public void burstQueries(final BenchmarkState server) throws Exception {
+ final DatabaseClientImpl client = server.client;
+ SessionPool pool = client.pool;
+ assertThat(pool.totalSessions())
+ .isEqualTo(server.spanner.getOptions().getSessionPoolOptions().getMinSessions());
+
+ ListeningScheduledExecutorService service =
+ MoreExecutors.listeningDecorator(Executors.newScheduledThreadPool(PARALLEL_THREADS));
+ List>> results = new ArrayList<>(PARALLEL_THREADS);
+ for (int i = 0; i < PARALLEL_THREADS; i++) {
+ results.add(
+ service.submit(() -> runBenchmarksForSingleUseQueries(server, TOTAL_READS_PER_RUN)));
+ }
+ collectResultsAndPrint(service, results, TOTAL_READS_PER_RUN);
+ }
+
+ private List runBenchmarksForSingleUseQueries(
+ final BenchmarkState server, int numberOfOperations) {
+ List results = new ArrayList<>(numberOfOperations);
+ // Execute one query to make sure everything has been warmed up.
+ executeWarmup(server);
+
+ for (int i = 0; i < numberOfOperations; i++) {
+ results.add(executeSingleUseQuery(server));
+ }
+ return results;
+ }
+
+ private void executeWarmup(final BenchmarkState server) {
+ for (int i = 0; i < WARMUP_REQUEST_COUNT; i++) {
+ executeSingleUseQuery(server);
+ }
+ }
+
+ private java.time.Duration executeSingleUseQuery(final BenchmarkState server) {
+ Stopwatch watch = Stopwatch.createStarted();
+
+ try (ResultSet rs = server.client.singleUse().executeQuery(getRandomisedReadStatement())) {
+ while (rs.next()) {
+ assertEquals(1, rs.getColumnCount());
+ assertNotNull(rs.getValue(0));
+ }
+ } catch (Throwable t) {
+ // ignore exception
+ System.out.println("Got exception = " + t);
+ }
+ return watch.elapsed();
+ }
+
+ static Statement getRandomisedReadStatement() {
+ int randomKey = ThreadLocalRandom.current().nextInt(TOTAL_RECORDS);
+ return Statement.newBuilder(SELECT_QUERY).bind(ID_COLUMN_NAME).to(randomKey).build();
+ }
+
+ void collectResultsAndPrint(
+ ListeningScheduledExecutorService service,
+ List>> results,
+ int numOperationsPerThread)
+ throws Exception {
+ final List collectResults =
+ collectResults(
+ service, results, numOperationsPerThread * PARALLEL_THREADS, Duration.ofMinutes(60));
+ printResults(collectResults);
+ }
+}
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/OpenTelemetrySpanTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/OpenTelemetrySpanTest.java
index 7152486c458..8a4859fa132 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/OpenTelemetrySpanTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/OpenTelemetrySpanTest.java
@@ -103,6 +103,10 @@ public class OpenTelemetrySpanTest {
private static final Statement INVALID_UPDATE_STATEMENT =
Statement.of("UPDATE NON_EXISTENT_TABLE SET BAR=1 WHERE BAZ=2");
+ private List expectedCreateMultiplexedSessionsRequestEvents =
+ ImmutableList.of("Request for 1 multiplexed session returned 1 session");
+
+ private int expectedCreateMultiplexedSessionsRequestEventsCount = 1;
private List expectedBatchCreateSessionsRequestEvents =
ImmutableList.of("Requesting 2 sessions", "Request for 2 sessions returned 2 sessions");
@@ -117,21 +121,6 @@ public class OpenTelemetrySpanTest {
private int expectedExecuteStreamingQueryEventsCount = 1;
- private List expectedReadOnlyTransactionSingleUseEvents =
- ImmutableList.of("Acquiring session", "Acquired session", "Using Session");
-
- private int expectedReadOnlyTransactionSingleUseEventsCount = 3;
-
- private List expectedReadOnlyTransactionMultiUseEvents =
- ImmutableList.of(
- "Acquiring session",
- "Acquired session",
- "Using Session",
- "Creating Transaction",
- "Transaction Creation Done");
-
- private int expectedReadOnlyTransactionMultiUseEventsCount = 5;
-
private List expectedReadWriteTransactionErrorEvents =
ImmutableList.of(
"Acquiring session",
@@ -167,33 +156,6 @@ public class OpenTelemetrySpanTest {
"Transaction Attempt Succeeded");
private int expectedReadWriteTransactionErrorWithBeginTransactionEventsCount = 11;
- private List expectedReadOnlyTransactionSpans =
- ImmutableList.of(
- "CloudSpannerOperation.BatchCreateSessionsRequest",
- "CloudSpannerOperation.ExecuteStreamingQuery",
- "CloudSpannerOperation.BatchCreateSessions",
- "CloudSpanner.ReadOnlyTransaction");
-
- private List expectedReadWriteTransactionWithCommitSpans =
- ImmutableList.of(
- "CloudSpannerOperation.BatchCreateSessionsRequest",
- "CloudSpannerOperation.Commit",
- "CloudSpannerOperation.BatchCreateSessions",
- "CloudSpanner.ReadWriteTransaction");
-
- private List expectedReadWriteTransactionSpans =
- ImmutableList.of(
- "CloudSpannerOperation.BatchCreateSessionsRequest",
- "CloudSpannerOperation.BatchCreateSessions",
- "CloudSpanner.ReadWriteTransaction");
-
- private List expectedReadWriteTransactionWithCommitAndBeginTransactionSpans =
- ImmutableList.of(
- "CloudSpannerOperation.BeginTransaction",
- "CloudSpannerOperation.BatchCreateSessionsRequest",
- "CloudSpannerOperation.Commit",
- "CloudSpannerOperation.BatchCreateSessions",
- "CloudSpanner.ReadWriteTransaction");
@BeforeClass
public static void setupOpenTelemetry() {
@@ -287,6 +249,25 @@ public void tearDown() {
@Test
public void singleUse() {
+ List expectedReadOnlyTransactionSingleUseEvents =
+ isMultiplexedSessionsEnabled()
+ ? ImmutableList.of("Using Session")
+ : ImmutableList.of("Acquiring session", "Acquired session", "Using Session");
+ List expectedReadOnlyTransactionSpans =
+ isMultiplexedSessionsEnabled()
+ ? ImmutableList.of(
+ "CloudSpannerOperation.CreateMultiplexedSession",
+ "CloudSpannerOperation.BatchCreateSessionsRequest",
+ "CloudSpannerOperation.ExecuteStreamingQuery",
+ "CloudSpannerOperation.BatchCreateSessions",
+ "CloudSpanner.ReadOnlyTransaction")
+ : ImmutableList.of(
+ "CloudSpannerOperation.BatchCreateSessionsRequest",
+ "CloudSpannerOperation.ExecuteStreamingQuery",
+ "CloudSpannerOperation.BatchCreateSessions",
+ "CloudSpanner.ReadOnlyTransaction");
+ int expectedReadOnlyTransactionSingleUseEventsCount = isMultiplexedSessionsEnabled() ? 1 : 3;
+
try (ResultSet rs = client.singleUse().executeQuery(SELECT1)) {
while (rs.next()) {
// Just consume the result set.
@@ -303,6 +284,12 @@ public void singleUse() {
spanItem -> {
actualSpanItems.add(spanItem.getName());
switch (spanItem.getName()) {
+ case "CloudSpannerOperation.CreateMultiplexedSession":
+ verifyRequestEvents(
+ spanItem,
+ expectedCreateMultiplexedSessionsRequestEvents,
+ expectedCreateMultiplexedSessionsRequestEventsCount);
+ break;
case "CloudSpannerOperation.BatchCreateSessionsRequest":
verifyRequestEvents(
spanItem,
@@ -337,6 +324,31 @@ public void singleUse() {
@Test
public void multiUse() {
+ List expectedReadOnlyTransactionSpans =
+ isMultiplexedSessionsEnabled()
+ ? ImmutableList.of(
+ "CloudSpannerOperation.CreateMultiplexedSession",
+ "CloudSpannerOperation.BatchCreateSessionsRequest",
+ "CloudSpannerOperation.ExecuteStreamingQuery",
+ "CloudSpannerOperation.BatchCreateSessions",
+ "CloudSpanner.ReadOnlyTransaction")
+ : ImmutableList.of(
+ "CloudSpannerOperation.BatchCreateSessionsRequest",
+ "CloudSpannerOperation.ExecuteStreamingQuery",
+ "CloudSpannerOperation.BatchCreateSessions",
+ "CloudSpanner.ReadOnlyTransaction");
+ List expectedReadOnlyTransactionMultiUseEvents =
+ isMultiplexedSessionsEnabled()
+ ? ImmutableList.of("Using Session", "Creating Transaction", "Transaction Creation Done")
+ : ImmutableList.of(
+ "Acquiring session",
+ "Acquired session",
+ "Using Session",
+ "Creating Transaction",
+ "Transaction Creation Done");
+
+ int expectedReadOnlyTransactionMultiUseEventsCount = isMultiplexedSessionsEnabled() ? 3 : 5;
+
try (ReadOnlyTransaction tx = client.readOnlyTransaction()) {
try (ResultSet rs = tx.executeQuery(SELECT1)) {
while (rs.next()) {
@@ -352,6 +364,12 @@ public void multiUse() {
spanItem -> {
actualSpanItems.add(spanItem.getName());
switch (spanItem.getName()) {
+ case "CloudSpannerOperation.CreateMultiplexedSession":
+ verifyRequestEvents(
+ spanItem,
+ expectedCreateMultiplexedSessionsRequestEvents,
+ expectedCreateMultiplexedSessionsRequestEventsCount);
+ break;
case "CloudSpannerOperation.BatchCreateSessionsRequest":
verifyRequestEvents(
spanItem,
@@ -386,6 +404,19 @@ public void multiUse() {
@Test
public void transactionRunner() {
+ List expectedReadWriteTransactionWithCommitSpans =
+ isMultiplexedSessionsEnabled()
+ ? ImmutableList.of(
+ "CloudSpannerOperation.CreateMultiplexedSession",
+ "CloudSpannerOperation.BatchCreateSessionsRequest",
+ "CloudSpannerOperation.Commit",
+ "CloudSpannerOperation.BatchCreateSessions",
+ "CloudSpanner.ReadWriteTransaction")
+ : ImmutableList.of(
+ "CloudSpannerOperation.BatchCreateSessionsRequest",
+ "CloudSpannerOperation.Commit",
+ "CloudSpannerOperation.BatchCreateSessions",
+ "CloudSpanner.ReadWriteTransaction");
TransactionRunner runner = client.readWriteTransaction();
runner.run(transaction -> transaction.executeUpdate(UPDATE_STATEMENT));
List actualSpanItems = new ArrayList<>();
@@ -395,6 +426,12 @@ public void transactionRunner() {
spanItem -> {
actualSpanItems.add(spanItem.getName());
switch (spanItem.getName()) {
+ case "CloudSpannerOperation.CreateMultiplexedSession":
+ verifyRequestEvents(
+ spanItem,
+ expectedCreateMultiplexedSessionsRequestEvents,
+ expectedCreateMultiplexedSessionsRequestEventsCount);
+ break;
case "CloudSpannerOperation.BatchCreateSessionsRequest":
verifyRequestEvents(
spanItem,
@@ -426,6 +463,17 @@ public void transactionRunner() {
@Test
public void transactionRunnerWithError() {
+ List expectedReadWriteTransactionSpans =
+ isMultiplexedSessionsEnabled()
+ ? ImmutableList.of(
+ "CloudSpannerOperation.CreateMultiplexedSession",
+ "CloudSpannerOperation.BatchCreateSessionsRequest",
+ "CloudSpannerOperation.BatchCreateSessions",
+ "CloudSpanner.ReadWriteTransaction")
+ : ImmutableList.of(
+ "CloudSpannerOperation.BatchCreateSessionsRequest",
+ "CloudSpannerOperation.BatchCreateSessions",
+ "CloudSpanner.ReadWriteTransaction");
TransactionRunner runner = client.readWriteTransaction();
SpannerException e =
assertThrows(
@@ -440,6 +488,12 @@ public void transactionRunnerWithError() {
spanItem -> {
actualSpanItems.add(spanItem.getName());
switch (spanItem.getName()) {
+ case "CloudSpannerOperation.CreateMultiplexedSession":
+ verifyRequestEvents(
+ spanItem,
+ expectedCreateMultiplexedSessionsRequestEvents,
+ expectedCreateMultiplexedSessionsRequestEventsCount);
+ break;
case "CloudSpannerOperation.BatchCreateSessionsRequest":
verifyRequestEvents(
spanItem,
@@ -468,6 +522,21 @@ public void transactionRunnerWithError() {
@Test
public void transactionRunnerWithFailedAndBeginTransaction() {
+ List expectedReadWriteTransactionWithCommitAndBeginTransactionSpans =
+ isMultiplexedSessionsEnabled()
+ ? ImmutableList.of(
+ "CloudSpannerOperation.CreateMultiplexedSession",
+ "CloudSpannerOperation.BeginTransaction",
+ "CloudSpannerOperation.BatchCreateSessionsRequest",
+ "CloudSpannerOperation.Commit",
+ "CloudSpannerOperation.BatchCreateSessions",
+ "CloudSpanner.ReadWriteTransaction")
+ : ImmutableList.of(
+ "CloudSpannerOperation.BeginTransaction",
+ "CloudSpannerOperation.BatchCreateSessionsRequest",
+ "CloudSpannerOperation.Commit",
+ "CloudSpannerOperation.BatchCreateSessions",
+ "CloudSpanner.ReadWriteTransaction");
Long updateCount =
client
.readWriteTransaction()
@@ -492,6 +561,12 @@ public void transactionRunnerWithFailedAndBeginTransaction() {
spanItem -> {
actualSpanItems.add(spanItem.getName());
switch (spanItem.getName()) {
+ case "CloudSpannerOperation.CreateMultiplexedSession":
+ verifyRequestEvents(
+ spanItem,
+ expectedCreateMultiplexedSessionsRequestEvents,
+ expectedCreateMultiplexedSessionsRequestEventsCount);
+ break;
case "CloudSpannerOperation.BatchCreateSessionsRequest":
verifyRequestEvents(
spanItem,
@@ -536,4 +611,11 @@ private static void verifySpans(List actualSpanItems, List expec
expectedSpansItems.stream().sorted().collect(Collectors.toList()),
actualSpanItems.stream().distinct().sorted().collect(Collectors.toList()));
}
+
+ private boolean isMultiplexedSessionsEnabled() {
+ if (spanner.getOptions() == null || spanner.getOptions().getSessionPoolOptions() == null) {
+ return false;
+ }
+ return spanner.getOptions().getSessionPoolOptions().getUseMultiplexedSession();
+ }
}
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReadAsyncTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReadAsyncTest.java
index 6801c82b66c..13dd8500b76 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReadAsyncTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReadAsyncTest.java
@@ -185,6 +185,8 @@ public void pointReadNotFound() throws Exception {
@Test
public void invalidDatabase() throws Exception {
+ mockSpanner.setCreateSessionExecutionTime(
+ SimulatedExecutionTime.stickyDatabaseNotFoundException("invalid-database"));
mockSpanner.setBatchCreateSessionsExecutionTime(
SimulatedExecutionTime.stickyDatabaseNotFoundException("invalid-database"));
DatabaseClient invalidClient =
@@ -258,14 +260,23 @@ public void closeTransactionBeforeEndOfAsyncQuery() throws Exception {
// Wait until at least one row has been fetched. At that moment there should be one session
// checked out.
dataReceived.await();
- assertThat(clientImpl.pool.getNumberOfSessionsInUse()).isEqualTo(1);
+
+ if (isMultiplexedSessionsEnabled()) {
+ assertThat(clientImpl.pool.getNumberOfSessionsInUse()).isEqualTo(0);
+ } else {
+ assertThat(clientImpl.pool.getNumberOfSessionsInUse()).isEqualTo(1);
+ }
}
// The read-only transaction is now closed, but the ready callback will continue to receive
// data. As it tries to put the data into a synchronous queue and the underlying buffer can also
// only hold 1 row, the async result set has not yet finished. The read-only transaction will
// release the session back into the pool when all async statements have finished. The number of
// sessions in use is therefore still 1.
- assertThat(clientImpl.pool.getNumberOfSessionsInUse()).isEqualTo(1);
+ if (isMultiplexedSessionsEnabled()) {
+ assertThat(clientImpl.pool.getNumberOfSessionsInUse()).isEqualTo(0);
+ } else {
+ assertThat(clientImpl.pool.getNumberOfSessionsInUse()).isEqualTo(1);
+ }
List resultList = new ArrayList<>();
do {
results.drainTo(resultList);
@@ -439,4 +450,11 @@ public void cancel() throws Exception {
assertThat(e.getErrorCode()).isEqualTo(ErrorCode.CANCELLED);
assertThat(values).containsExactly("v1");
}
+
+ private boolean isMultiplexedSessionsEnabled() {
+ if (spanner.getOptions() == null || spanner.getOptions().getSessionPoolOptions() == null) {
+ return false;
+ }
+ return spanner.getOptions().getSessionPoolOptions().getUseMultiplexedSession();
+ }
}
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResumableStreamIteratorTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResumableStreamIteratorTest.java
index d153696ab45..899bec4c622 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResumableStreamIteratorTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResumableStreamIteratorTest.java
@@ -26,6 +26,7 @@
import com.google.api.client.util.BackOff;
import com.google.cloud.spanner.v1.stub.SpannerStubSettings;
import com.google.common.collect.AbstractIterator;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.protobuf.ByteString;
import com.google.protobuf.Duration;
@@ -52,11 +53,13 @@
import org.junit.Before;
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;
import org.mockito.Mockito;
/** Unit tests for {@link ResumableStreamIterator}. */
-@RunWith(JUnit4.class)
+@RunWith(Parameterized.class)
public class ResumableStreamIteratorTest {
interface Starter {
AbstractResultSet.CloseableIterator startStream(
@@ -69,6 +72,14 @@ interface ResultSetStream {
void close();
}
+ @Parameter(0)
+ public ErrorCode errorCodeParameter;
+
+ @Parameters(name = "errorCodeParameter = {0}")
+ public static List data() {
+ return ImmutableList.of(ErrorCode.UNAVAILABLE, ErrorCode.RESOURCE_EXHAUSTED);
+ }
+
private static StatusRuntimeException statusWithRetryInfo(ErrorCode code) {
Metadata.Key key = ProtoUtils.keyForProto(RetryInfo.getDefaultInstance());
Metadata trailers = new Metadata();
@@ -223,7 +234,7 @@ public void restart() {
Mockito.when(s1.next())
.thenReturn(resultSet(ByteString.copyFromUtf8("r1"), "a"))
.thenReturn(resultSet(ByteString.copyFromUtf8("r2"), "b"))
- .thenThrow(new RetryableException(ErrorCode.UNAVAILABLE, "failed by test"));
+ .thenThrow(new RetryableException(errorCodeParameter, "failed by test"));
ResultSetStream s2 = Mockito.mock(ResultSetStream.class);
Mockito.when(starter.startStream(ByteString.copyFromUtf8("r2")))
@@ -244,7 +255,7 @@ public void restartWithHoldBack() {
.thenReturn(resultSet(ByteString.copyFromUtf8("r2"), "b"))
.thenReturn(resultSet(null, "X"))
.thenReturn(resultSet(null, "X"))
- .thenThrow(new RetryableException(ErrorCode.UNAVAILABLE, "failed by test"));
+ .thenThrow(new RetryableException(errorCodeParameter, "failed by test"));
ResultSetStream s2 = Mockito.mock(ResultSetStream.class);
Mockito.when(starter.startStream(ByteString.copyFromUtf8("r2")))
@@ -265,7 +276,7 @@ public void restartWithHoldBackMidStream() {
.thenReturn(resultSet(null, "b"))
.thenReturn(resultSet(null, "c"))
.thenReturn(resultSet(ByteString.copyFromUtf8("r2"), "d"))
- .thenThrow(new RetryableException(ErrorCode.UNAVAILABLE, "failed by test"));
+ .thenThrow(new RetryableException(errorCodeParameter, "failed by test"));
ResultSetStream s2 = Mockito.mock(ResultSetStream.class);
Mockito.when(starter.startStream(ByteString.copyFromUtf8("r2")))
@@ -360,7 +371,7 @@ public void bufferLimitRestart() {
Mockito.when(s1.next())
.thenReturn(resultSet(ByteString.copyFromUtf8("r1"), "a"))
.thenReturn(resultSet(ByteString.copyFromUtf8("r2"), "b"))
- .thenThrow(new RetryableException(ErrorCode.UNAVAILABLE, "failed by test"));
+ .thenThrow(new RetryableException(errorCodeParameter, "failed by test"));
ResultSetStream s2 = Mockito.mock(ResultSetStream.class);
Mockito.when(starter.startStream(ByteString.copyFromUtf8("r2")))
@@ -380,7 +391,7 @@ public void bufferLimitRestartWithinLimitAtStartOfResults() {
Mockito.when(starter.startStream(null)).thenReturn(new ResultSetIterator(s1));
Mockito.when(s1.next())
.thenReturn(resultSet(null, "XXXXXX"))
- .thenThrow(new RetryableException(ErrorCode.UNAVAILABLE, "failed by test"));
+ .thenThrow(new RetryableException(errorCodeParameter, "failed by test"));
ResultSetStream s2 = Mockito.mock(ResultSetStream.class);
Mockito.when(starter.startStream(null)).thenReturn(new ResultSetIterator(s2));
@@ -400,7 +411,7 @@ public void bufferLimitRestartWithinLimitMidResults() {
Mockito.when(s1.next())
.thenReturn(resultSet(ByteString.copyFromUtf8("r1"), "a"))
.thenReturn(resultSet(null, "XXXXXX"))
- .thenThrow(new RetryableException(ErrorCode.UNAVAILABLE, "failed by test"));
+ .thenThrow(new RetryableException(errorCodeParameter, "failed by test"));
ResultSetStream s2 = Mockito.mock(ResultSetStream.class);
Mockito.when(starter.startStream(ByteString.copyFromUtf8("r1")))
@@ -422,11 +433,11 @@ public void bufferLimitMissingTokensUnsafeToRetry() {
.thenReturn(resultSet(ByteString.copyFromUtf8("r1"), "a"))
.thenReturn(resultSet(null, "b"))
.thenReturn(resultSet(null, "c"))
- .thenThrow(new RetryableException(ErrorCode.UNAVAILABLE, "failed by test"));
+ .thenThrow(new RetryableException(errorCodeParameter, "failed by test"));
assertThat(consumeAtMost(3, resumableStreamIterator)).containsExactly("a", "b", "c").inOrder();
SpannerException e = assertThrows(SpannerException.class, () -> resumableStreamIterator.next());
- assertThat(e.getErrorCode()).isEqualTo(ErrorCode.UNAVAILABLE);
+ assertThat(e.getErrorCode()).isEqualTo(errorCodeParameter);
}
@Test
@@ -439,7 +450,7 @@ public void bufferLimitMissingTokensSafeToRetry() {
.thenReturn(resultSet(ByteString.copyFromUtf8("r1"), "a"))
.thenReturn(resultSet(null, "b"))
.thenReturn(resultSet(ByteString.copyFromUtf8("r3"), "c"))
- .thenThrow(new RetryableException(ErrorCode.UNAVAILABLE, "failed by test"));
+ .thenThrow(new RetryableException(errorCodeParameter, "failed by test"));
ResultSetStream s2 = Mockito.mock(ResultSetStream.class);
Mockito.when(starter.startStream(ByteString.copyFromUtf8("r3")))
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 85b91ed3981..42a62be33aa 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
@@ -20,6 +20,7 @@
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeFalse;
import com.google.api.core.ApiFuture;
import com.google.api.core.ApiFutures;
@@ -217,11 +218,12 @@ public void setUp() throws InterruptedException {
}
// This prevents repeated retries for a large number of sessions in the pool.
builder.setMinSessions(1);
+ SessionPoolOptions sessionPoolOptions = builder.build();
spanner =
SpannerOptions.newBuilder()
.setProjectId("[PROJECT]")
.setChannelProvider(channelProvider)
- .setSessionPoolOption(builder.build())
+ .setSessionPoolOption(sessionPoolOptions)
.setCredentials(NoCredentials.getInstance())
.build()
.getService();
@@ -259,6 +261,9 @@ private T assertThrowsSessionNotFoundIfShouldFail(Supplier supplier) {
@Test
public void singleUseSelect() throws InterruptedException {
+ assumeFalse(
+ "Multiplexed session do not throw a SessionNotFound errors. ",
+ spanner.getOptions().getSessionPoolOptions().getUseMultiplexedSession());
// This call will receive an invalidated session that will be replaced on the first call to
// rs.next().
try (ReadContext context = client.singleUse()) {
@@ -270,6 +275,9 @@ public void singleUseSelect() throws InterruptedException {
@Test
public void singleUseSelectAsync() throws Exception {
+ assumeFalse(
+ "Multiplexed session do not throw a SessionNotFound errors. ",
+ spanner.getOptions().getSessionPoolOptions().getUseMultiplexedSession());
ApiFuture> list;
try (AsyncResultSet rs = client.singleUse().executeQueryAsync(SELECT1AND2)) {
list = rs.toListAsync(TO_LONG, executor);
@@ -279,6 +287,9 @@ public void singleUseSelectAsync() throws Exception {
@Test
public void singleUseRead() throws InterruptedException {
+ assumeFalse(
+ "Multiplexed session do not throw a SessionNotFound errors. ",
+ spanner.getOptions().getSessionPoolOptions().getUseMultiplexedSession());
try (ReadContext context = client.singleUse()) {
try (ResultSet rs = context.read("FOO", KeySet.all(), Collections.singletonList("BAR"))) {
assertThrowsSessionNotFoundIfShouldFail(() -> rs.next());
@@ -288,6 +299,9 @@ public void singleUseRead() throws InterruptedException {
@Test
public void singleUseReadUsingIndex() throws InterruptedException {
+ assumeFalse(
+ "Multiplexed session do not throw a SessionNotFound errors. ",
+ spanner.getOptions().getSessionPoolOptions().getUseMultiplexedSession());
try (ReadContext context = client.singleUse()) {
try (ResultSet rs =
context.readUsingIndex("FOO", "IDX", KeySet.all(), Collections.singletonList("BAR"))) {
@@ -298,6 +312,9 @@ public void singleUseReadUsingIndex() throws InterruptedException {
@Test
public void singleUseReadRow() throws InterruptedException {
+ assumeFalse(
+ "Multiplexed session do not throw a SessionNotFound errors. ",
+ spanner.getOptions().getSessionPoolOptions().getUseMultiplexedSession());
try (ReadContext context = client.singleUse()) {
assertThrowsSessionNotFoundIfShouldFail(
() -> context.readRow("FOO", Key.of(), Collections.singletonList("BAR")));
@@ -306,6 +323,9 @@ public void singleUseReadRow() throws InterruptedException {
@Test
public void singleUseReadRowUsingIndex() throws InterruptedException {
+ assumeFalse(
+ "Multiplexed session do not throw a SessionNotFound errors. ",
+ spanner.getOptions().getSessionPoolOptions().getUseMultiplexedSession());
try (ReadContext context = client.singleUse()) {
assertThrowsSessionNotFoundIfShouldFail(
() ->
@@ -315,6 +335,9 @@ public void singleUseReadRowUsingIndex() throws InterruptedException {
@Test
public void singleUseReadOnlyTransactionSelect() throws InterruptedException {
+ assumeFalse(
+ "Multiplexed session do not throw a SessionNotFound errors. ",
+ spanner.getOptions().getSessionPoolOptions().getUseMultiplexedSession());
try (ReadContext context = client.singleUseReadOnlyTransaction()) {
try (ResultSet rs = context.executeQuery(SELECT1AND2)) {
assertThrowsSessionNotFoundIfShouldFail(() -> rs.next());
@@ -324,6 +347,9 @@ public void singleUseReadOnlyTransactionSelect() throws InterruptedException {
@Test
public void singleUseReadOnlyTransactionRead() throws InterruptedException {
+ assumeFalse(
+ "Multiplexed session do not throw a SessionNotFound errors. ",
+ spanner.getOptions().getSessionPoolOptions().getUseMultiplexedSession());
try (ReadContext context = client.singleUseReadOnlyTransaction()) {
try (ResultSet rs = context.read("FOO", KeySet.all(), Collections.singletonList("BAR"))) {
assertThrowsSessionNotFoundIfShouldFail(() -> rs.next());
@@ -333,6 +359,9 @@ public void singleUseReadOnlyTransactionRead() throws InterruptedException {
@Test
public void singlUseReadOnlyTransactionReadUsingIndex() throws InterruptedException {
+ assumeFalse(
+ "Multiplexed session do not throw a SessionNotFound errors. ",
+ spanner.getOptions().getSessionPoolOptions().getUseMultiplexedSession());
try (ReadContext context = client.singleUseReadOnlyTransaction()) {
try (ResultSet rs =
context.readUsingIndex("FOO", "IDX", KeySet.all(), Collections.singletonList("BAR"))) {
@@ -343,6 +372,9 @@ public void singlUseReadOnlyTransactionReadUsingIndex() throws InterruptedExcept
@Test
public void singleUseReadOnlyTransactionReadRow() throws InterruptedException {
+ assumeFalse(
+ "Multiplexed session do not throw a SessionNotFound errors. ",
+ spanner.getOptions().getSessionPoolOptions().getUseMultiplexedSession());
try (ReadContext context = client.singleUseReadOnlyTransaction()) {
assertThrowsSessionNotFoundIfShouldFail(
() -> context.readRow("FOO", Key.of(), Collections.singletonList("BAR")));
@@ -351,6 +383,9 @@ public void singleUseReadOnlyTransactionReadRow() throws InterruptedException {
@Test
public void singleUseReadOnlyTransactionReadRowUsingIndex() throws InterruptedException {
+ assumeFalse(
+ "Multiplexed session do not throw a SessionNotFound errors. ",
+ spanner.getOptions().getSessionPoolOptions().getUseMultiplexedSession());
try (ReadContext context = client.singleUseReadOnlyTransaction()) {
assertThrowsSessionNotFoundIfShouldFail(
() ->
@@ -360,6 +395,9 @@ public void singleUseReadOnlyTransactionReadRowUsingIndex() throws InterruptedEx
@Test
public void readOnlyTransactionSelect() throws InterruptedException {
+ assumeFalse(
+ "Multiplexed session do not throw a SessionNotFound errors. ",
+ spanner.getOptions().getSessionPoolOptions().getUseMultiplexedSession());
try (ReadContext context = client.readOnlyTransaction()) {
try (ResultSet rs = context.executeQuery(SELECT1AND2)) {
assertThrowsSessionNotFoundIfShouldFail(() -> rs.next());
@@ -369,6 +407,9 @@ public void readOnlyTransactionSelect() throws InterruptedException {
@Test
public void readOnlyTransactionRead() throws InterruptedException {
+ assumeFalse(
+ "Multiplexed session do not throw a SessionNotFound errors. ",
+ spanner.getOptions().getSessionPoolOptions().getUseMultiplexedSession());
try (ReadContext context = client.readOnlyTransaction()) {
try (ResultSet rs = context.read("FOO", KeySet.all(), Collections.singletonList("BAR"))) {
assertThrowsSessionNotFoundIfShouldFail(() -> rs.next());
@@ -378,6 +419,9 @@ public void readOnlyTransactionRead() throws InterruptedException {
@Test
public void readOnlyTransactionReadUsingIndex() throws InterruptedException {
+ assumeFalse(
+ "Multiplexed session do not throw a SessionNotFound errors. ",
+ spanner.getOptions().getSessionPoolOptions().getUseMultiplexedSession());
try (ReadContext context = client.readOnlyTransaction()) {
try (ResultSet rs =
context.readUsingIndex("FOO", "IDX", KeySet.all(), Collections.singletonList("BAR"))) {
@@ -388,6 +432,9 @@ public void readOnlyTransactionReadUsingIndex() throws InterruptedException {
@Test
public void readOnlyTransactionReadRow() throws InterruptedException {
+ assumeFalse(
+ "Multiplexed session do not throw a SessionNotFound errors. ",
+ spanner.getOptions().getSessionPoolOptions().getUseMultiplexedSession());
try (ReadContext context = client.readOnlyTransaction()) {
assertThrowsSessionNotFoundIfShouldFail(
() -> context.readRow("FOO", Key.of(), Collections.singletonList("BAR")));
@@ -396,6 +443,9 @@ public void readOnlyTransactionReadRow() throws InterruptedException {
@Test
public void readOnlyTransactionReadRowUsingIndex() throws InterruptedException {
+ assumeFalse(
+ "Multiplexed session do not throw a SessionNotFound errors. ",
+ spanner.getOptions().getSessionPoolOptions().getUseMultiplexedSession());
try (ReadContext context = client.readOnlyTransaction()) {
assertThrowsSessionNotFoundIfShouldFail(
() ->
@@ -405,6 +455,9 @@ public void readOnlyTransactionReadRowUsingIndex() throws InterruptedException {
@Test
public void readOnlyTransactionSelectNonRecoverable() throws InterruptedException {
+ assumeFalse(
+ "Multiplexed session do not throw a SessionNotFound errors. ",
+ spanner.getOptions().getSessionPoolOptions().getUseMultiplexedSession());
try (ReadContext context = client.readOnlyTransaction()) {
try (ResultSet rs = context.executeQuery(SELECT1AND2)) {
assertThrowsSessionNotFoundIfShouldFail(() -> rs.next());
@@ -419,6 +472,9 @@ public void readOnlyTransactionSelectNonRecoverable() throws InterruptedExceptio
@Test
public void readOnlyTransactionReadNonRecoverable() throws InterruptedException {
+ assumeFalse(
+ "Multiplexed session do not throw a SessionNotFound errors. ",
+ spanner.getOptions().getSessionPoolOptions().getUseMultiplexedSession());
try (ReadContext context = client.readOnlyTransaction()) {
try (ResultSet rs = context.read("FOO", KeySet.all(), Collections.singletonList("BAR"))) {
assertThrowsSessionNotFoundIfShouldFail(() -> rs.next());
@@ -432,6 +488,9 @@ public void readOnlyTransactionReadNonRecoverable() throws InterruptedException
@Test
public void readOnlyTransactionReadUsingIndexNonRecoverable() throws InterruptedException {
+ assumeFalse(
+ "Multiplexed session do not throw a SessionNotFound errors. ",
+ spanner.getOptions().getSessionPoolOptions().getUseMultiplexedSession());
try (ReadContext context = client.readOnlyTransaction()) {
try (ResultSet rs =
context.readUsingIndex("FOO", "IDX", KeySet.all(), Collections.singletonList("BAR"))) {
@@ -447,6 +506,9 @@ public void readOnlyTransactionReadUsingIndexNonRecoverable() throws Interrupted
@Test
public void readOnlyTransactionReadRowNonRecoverable() throws InterruptedException {
+ assumeFalse(
+ "Multiplexed session do not throw a SessionNotFound errors. ",
+ spanner.getOptions().getSessionPoolOptions().getUseMultiplexedSession());
try (ReadContext context = client.readOnlyTransaction()) {
assertThrowsSessionNotFoundIfShouldFail(
() -> context.readRow("FOO", Key.of(), Collections.singletonList("BAR")));
@@ -459,6 +521,9 @@ public void readOnlyTransactionReadRowNonRecoverable() throws InterruptedExcepti
@Test
public void readOnlyTransactionReadRowUsingIndexNonRecoverable() throws InterruptedException {
+ assumeFalse(
+ "Multiplexed session do not throw a SessionNotFound errors. ",
+ spanner.getOptions().getSessionPoolOptions().getUseMultiplexedSession());
try (ReadContext context = client.readOnlyTransaction()) {
assertThrowsSessionNotFoundIfShouldFail(
() ->
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionClientTests.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionClientTests.java
index 3094282f07b..c0ae8de97c9 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionClientTests.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionClientTests.java
@@ -125,6 +125,9 @@ public ScheduledExecutorService get() {
doNothing().when(span).end();
doNothing().when(span).addAnnotation("Starting Commit");
when(spanner.getRpc()).thenReturn(rpc);
+ SessionPoolOptions sessionPoolOptions = mock(SessionPoolOptions.class);
+ when(sessionPoolOptions.getPoolMaintainerClock()).thenReturn(Clock.INSTANCE);
+ when(spannerOptions.getSessionPoolOptions()).thenReturn(sessionPoolOptions);
}
@Test
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionImplTest.java
index 8236e509008..72befe8a2b4 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
@@ -98,7 +98,9 @@ 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));
+ SessionPoolOptions sessionPoolOptions = mock(SessionPoolOptions.class);
+ when(sessionPoolOptions.getPoolMaintainerClock()).thenReturn(Clock.INSTANCE);
+ when(spannerOptions.getSessionPoolOptions()).thenReturn(sessionPoolOptions);
when(spannerOptions.getOpenTelemetry()).thenReturn(OpenTelemetry.noop());
@SuppressWarnings("resource")
SpannerImpl spanner = new SpannerImpl(rpc, spannerOptions);
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 91352cc1a14..4672f03aeff 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
@@ -21,6 +21,7 @@
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
+import static org.junit.Assume.assumeFalse;
import com.google.api.gax.grpc.testing.LocalChannelProvider;
import com.google.cloud.NoCredentials;
@@ -106,19 +107,23 @@ public void tearDown() {
@Test
public void testIgnoreLeakedSession() {
for (boolean trackStackTraceofSessionCheckout : new boolean[] {true, false}) {
- SpannerOptions.Builder builder =
- SpannerOptions.newBuilder()
- .setProjectId("[PROJECT]")
- .setChannelProvider(channelProvider)
- .setCredentials(NoCredentials.getInstance());
- builder.setSessionPoolOption(
+ SessionPoolOptions sessionPoolOptions =
SessionPoolOptions.newBuilder()
.setMinSessions(0)
.setMaxSessions(2)
.setIncStep(1)
.setFailOnSessionLeak()
.setTrackStackTraceOfSessionCheckout(trackStackTraceofSessionCheckout)
- .build());
+ .build();
+ assumeFalse(
+ "Session Leaks do not occur with Multiplexed Sessions",
+ sessionPoolOptions.getUseMultiplexedSession());
+ SpannerOptions.Builder builder =
+ SpannerOptions.newBuilder()
+ .setProjectId("[PROJECT]")
+ .setChannelProvider(channelProvider)
+ .setCredentials(NoCredentials.getInstance());
+ builder.setSessionPoolOption(sessionPoolOptions);
Spanner spanner = builder.build().getService();
DatabaseClient client =
spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]"));
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolMaintainerMockServerTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolMaintainerMockServerTest.java
new file mode 100644
index 00000000000..c74806161f6
--- /dev/null
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolMaintainerMockServerTest.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright 2024 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.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeFalse;
+
+import com.google.cloud.NoCredentials;
+import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult;
+import com.google.common.base.Stopwatch;
+import com.google.protobuf.ListValue;
+import com.google.protobuf.Value;
+import com.google.spanner.v1.BatchCreateSessionsRequest;
+import com.google.spanner.v1.ExecuteSqlRequest;
+import com.google.spanner.v1.ResultSetMetadata;
+import com.google.spanner.v1.StructType;
+import com.google.spanner.v1.StructType.Field;
+import com.google.spanner.v1.Type;
+import com.google.spanner.v1.TypeCode;
+import java.util.concurrent.TimeUnit;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.threeten.bp.Duration;
+
+@RunWith(JUnit4.class)
+public class SessionPoolMaintainerMockServerTest extends AbstractMockServerTest {
+ private final FakeClock clock = new FakeClock();
+
+ @BeforeClass
+ public static void setupResults() {
+ mockSpanner.putStatementResult(
+ StatementResult.query(
+ Statement.of("SELECT 1"),
+ com.google.spanner.v1.ResultSet.newBuilder()
+ .setMetadata(
+ ResultSetMetadata.newBuilder()
+ .setRowType(
+ StructType.newBuilder()
+ .addFields(
+ Field.newBuilder()
+ .setName("C")
+ .setType(Type.newBuilder().setCode(TypeCode.INT64).build())
+ .build())
+ .build())
+ .build())
+ .addRows(
+ ListValue.newBuilder()
+ .addValues(Value.newBuilder().setStringValue("1").build())
+ .build())
+ .build()));
+ }
+
+ @Before
+ public void createSpannerInstance() {
+ clock.currentTimeMillis.set(System.currentTimeMillis());
+ spanner =
+ SpannerOptions.newBuilder()
+ .setProjectId("p")
+ .setChannelProvider(channelProvider)
+ .setCredentials(NoCredentials.getInstance())
+ .setSessionPoolOption(
+ SessionPoolOptions.newBuilder()
+ .setPoolMaintainerClock(clock)
+ .setWaitForMinSessions(Duration.ofSeconds(10L))
+ .setFailOnSessionLeak()
+ .build())
+ .build()
+ .getService();
+ }
+
+ @Test
+ public void testMaintain() {
+ int minSessions = spanner.getOptions().getSessionPoolOptions().getMinSessions();
+ DatabaseClientImpl client =
+ (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d"));
+ assertEquals(minSessions, mockSpanner.getSessions().size());
+ assertEquals(0, mockSpanner.countRequestsOfType(ExecuteSqlRequest.class));
+ clock.currentTimeMillis.addAndGet(Duration.ofMinutes(35).toMillis());
+ client.pool.poolMaintainer.maintainPool();
+ assertEquals(1, mockSpanner.countRequestsOfType(ExecuteSqlRequest.class));
+ client.pool.poolMaintainer.maintainPool();
+ assertEquals(2, mockSpanner.countRequestsOfType(ExecuteSqlRequest.class));
+ clock.currentTimeMillis.addAndGet(Duration.ofMinutes(21).toMillis());
+
+ // Most sessions are considered idle and are removed. Freeze the mock Spanner server to prevent
+ // the replenish action to fill the pool again before we check the number of sessions in the
+ // pool.
+ mockSpanner.freeze();
+ client.pool.poolMaintainer.maintainPool();
+ assertEquals(2, client.pool.totalSessions());
+ mockSpanner.unfreeze();
+
+ // The pool should be replenished.
+ client.pool.poolMaintainer.maintainPool();
+ assertEquals(minSessions, client.pool.getTotalSessionsPlusNumSessionsBeingCreated());
+ Stopwatch watch = Stopwatch.createStarted();
+ //noinspection StatementWithEmptyBody
+ while (client.pool.totalSessions() < minSessions
+ && watch.elapsed(TimeUnit.MILLISECONDS)
+ < spanner.getOptions().getSessionPoolOptions().getWaitForMinSessions().toMillis()) {
+ // wait for the pool to be replenished.
+ }
+ assertEquals(minSessions, client.pool.totalSessions());
+ }
+
+ @Test
+ public void testSessionNotFoundIsRetried() {
+ assumeFalse(
+ "Session not found errors are not relevant for multiplexed sessions",
+ spanner.getOptions().getSessionPoolOptions().getUseMultiplexedSession());
+
+ int minSessions = spanner.getOptions().getSessionPoolOptions().getMinSessions();
+ DatabaseClientImpl client =
+ (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d"));
+ assertEquals(minSessions, mockSpanner.getSessions().size());
+
+ // Remove all sessions from the backend.
+ mockSpanner.getSessions().clear();
+
+ // Sessions have been removed from the backend, but this will still succeed, as Session not
+ // found errors are retried by the client.
+ try (ResultSet resultSet = client.singleUse().executeQuery(Statement.of("SELECT 1"))) {
+ assertTrue(resultSet.next());
+ assertEquals(1L, resultSet.getLong(0));
+ assertFalse(resultSet.next());
+ }
+
+ int numRequests = mockSpanner.countRequestsOfType(ExecuteSqlRequest.class);
+ assertTrue(
+ String.format("Number of requests should be larger than 1, but was %d", numRequests),
+ numRequests > 1);
+ }
+
+ @Test
+ public void testMaintainerReplenishesPoolIfAllAreInvalid() {
+ int minSessions = spanner.getOptions().getSessionPoolOptions().getMinSessions();
+ DatabaseClientImpl client =
+ (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d"));
+ assertEquals(minSessions, mockSpanner.getSessions().size());
+
+ // Remove all sessions from the backend.
+ mockSpanner.getSessions().clear();
+ // Advance the clock of the maintainer to mark all sessions are eligible for maintenance.
+ clock.currentTimeMillis.addAndGet(Duration.ofMinutes(35).toMillis());
+ // Run the maintainer. This will ping one session, which again will cause it to be replaced.
+ client.pool.poolMaintainer.maintainPool();
+ assertEquals(1, mockSpanner.countRequestsOfType(ExecuteSqlRequest.class));
+
+ // The session will be replaced using a single BatchCreateSessions call.
+ Stopwatch watch = Stopwatch.createStarted();
+ //noinspection StatementWithEmptyBody
+ while (client.pool.totalSessions() < minSessions
+ && watch.elapsed(TimeUnit.MILLISECONDS)
+ < spanner.getOptions().getSessionPoolOptions().getWaitForMinSessions().toMillis()) {
+ // wait for the pool to be replenished.
+ }
+ assertEquals(minSessions, client.pool.totalSessions());
+ assertEquals(
+ spanner.getOptions().getNumChannels() + 1,
+ mockSpanner.countRequestsOfType(BatchCreateSessionsRequest.class));
+ }
+}
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 b489511b73b..9e55851ef4d 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
@@ -76,7 +76,9 @@ public void setUp() {
.setMaxSessions(5)
.setIncStep(1)
.setKeepAliveIntervalMinutes(2)
+ .setPoolMaintainerClock(clock)
.build();
+ when(spannerOptions.getSessionPoolOptions()).thenReturn(options);
idledSessions.clear();
pingedSessions.clear();
}
@@ -92,7 +94,7 @@ private void setupMockSessionCreation() {
for (int i = 0; i < sessionCount; i++) {
ReadContext mockContext = mock(ReadContext.class);
consumer.onSessionReady(
- setupMockSession(buildMockSession(mockContext), mockContext));
+ setupMockSession(buildMockSession(client, mockContext), mockContext));
}
});
return null;
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolOptionsTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolOptionsTest.java
index 1f4d591488b..76123d0ac68 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolOptionsTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolOptionsTest.java
@@ -22,6 +22,7 @@
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeFalse;
import com.google.cloud.spanner.SessionPoolOptions.InactiveTransactionRemovalOptions;
import java.util.ArrayList;
@@ -249,6 +250,8 @@ public void testRandomizePositionQPSThreshold() {
@Test
public void testUseMultiplexedSession() {
+ // skip these tests since this configuration can have dual behaviour in different test-runners
+ assumeFalse(SessionPoolOptions.newBuilder().build().getUseMultiplexedSession());
assertEquals(false, SessionPoolOptions.newBuilder().build().getUseMultiplexedSession());
assertEquals(
true,
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 02502399e10..6d2d1f19efe 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
@@ -68,7 +68,6 @@ public class SessionPoolStressTest extends BaseSessionPoolTest {
DatabaseId db = DatabaseId.of("projects/p/instances/i/databases/unused");
SessionPool pool;
- SessionPoolOptions options;
ExecutorService createExecutor = Executors.newSingleThreadExecutor();
final Object lock = new Object();
Random random = new Random();
@@ -109,7 +108,7 @@ private void setupSpanner(DatabaseId db) {
for (int s = 0; s < sessionCount; s++) {
SessionImpl session;
synchronized (lock) {
- session = getMockedSession(context);
+ session = getMockedSession(mockSpanner, context);
setupSession(session, context);
sessions.put(session.getName(), false);
if (sessions.size() > maxAliveSessions) {
@@ -128,15 +127,15 @@ private void setupSpanner(DatabaseId db) {
Mockito.anyInt(), Mockito.anyBoolean(), Mockito.any(SessionConsumer.class));
}
- SessionImpl getMockedSession(ReadContext context) {
- SpannerImpl spanner = mock(SpannerImpl.class);
+ SessionImpl getMockedSession(SpannerImpl spanner, ReadContext context) {
Map options = new HashMap<>();
options.put(Option.CHANNEL_HINT, channelHint.getAndIncrement());
final SessionImpl session =
new SessionImpl(
spanner,
- "projects/dummy/instances/dummy/databases/dummy/sessions/session" + sessionIndex,
- options) {
+ new SessionReference(
+ "projects/dummy/instances/dummy/databases/dummy/sessions/session" + sessionIndex,
+ options)) {
@Override
public ReadContext singleUse(TimestampBound bound) {
// The below stubs are added so that we can mock keep-alive.
@@ -208,6 +207,7 @@ public void stressTest() throws Exception {
int maxSessions = concurrentThreads / 2;
SessionPoolOptions.Builder builder =
SessionPoolOptions.newBuilder()
+ .setPoolMaintainerClock(clock)
.setMinSessions(minSessions)
.setMaxSessions(maxSessions)
.setInactiveTransactionRemovalOptions(
@@ -219,9 +219,11 @@ public void stressTest() throws Exception {
} else {
builder.setFailIfPoolExhausted();
}
+ SessionPoolOptions sessionPoolOptions = builder.build();
+ when(spannerOptions.getSessionPoolOptions()).thenReturn(sessionPoolOptions);
pool =
SessionPool.createPool(
- builder.build(),
+ sessionPoolOptions,
new TestExecutorFactory(),
mockSpanner.getSessionClient(db),
clock,
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 a391a66ec83..75580c25124 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
@@ -17,6 +17,7 @@
package com.google.cloud.spanner;
import static com.google.cloud.spanner.MetricRegistryConstants.GET_SESSION_TIMEOUTS;
+import static com.google.cloud.spanner.MetricRegistryConstants.IS_MULTIPLEXED_KEY;
import static com.google.cloud.spanner.MetricRegistryConstants.MAX_ALLOWED_SESSIONS;
import static com.google.cloud.spanner.MetricRegistryConstants.MAX_IN_USE_SESSIONS;
import static com.google.cloud.spanner.MetricRegistryConstants.METRIC_PREFIX;
@@ -31,6 +32,7 @@
import static com.google.cloud.spanner.MetricRegistryConstants.NUM_WRITE_SESSIONS;
import static com.google.cloud.spanner.MetricRegistryConstants.SPANNER_DEFAULT_LABEL_VALUES;
import static com.google.cloud.spanner.MetricRegistryConstants.SPANNER_LABEL_KEYS;
+import static com.google.cloud.spanner.MetricRegistryConstants.SPANNER_LABEL_KEYS_WITH_MULTIPLEXED_SESSIONS;
import static com.google.cloud.spanner.MetricRegistryConstants.SPANNER_LABEL_KEYS_WITH_TYPE;
import static com.google.cloud.spanner.SpannerOptionsTest.runWithSystemProperty;
import static com.google.common.truth.Truth.assertThat;
@@ -62,6 +64,7 @@
import com.google.cloud.spanner.MetricRegistryTestUtils.PointWithFunction;
import com.google.cloud.spanner.ReadContext.QueryAnalyzeMode;
import com.google.cloud.spanner.SessionClient.SessionConsumer;
+import com.google.cloud.spanner.SessionPool.MultiplexedSessionInitializationConsumer;
import com.google.cloud.spanner.SessionPool.PooledSession;
import com.google.cloud.spanner.SessionPool.PooledSessionFuture;
import com.google.cloud.spanner.SessionPool.Position;
@@ -139,9 +142,6 @@ public class SessionPoolTest extends BaseSessionPoolTest {
private final ExecutorService executor = Executors.newSingleThreadExecutor();
@Parameter public int minSessions;
- @Parameter(1)
- public boolean useMultiplexed;
-
@Mock SpannerImpl client;
@Mock SessionClient sessionClient;
@Mock SpannerOptions spannerOptions;
@@ -154,14 +154,9 @@ public class SessionPoolTest extends BaseSessionPoolTest {
private final TraceWrapper tracer =
new TraceWrapper(Tracing.getTracer(), OpenTelemetry.noop().getTracer(""));
- @Parameters(name = "min sessions = {0}, use multiplexed = {1}")
+ @Parameters(name = "min sessions = {0}")
public static Collection