diff --git a/.github/workflows/unmanaged_dependency_check.yaml b/.github/workflows/unmanaged_dependency_check.yaml
index f6594602a26..7eee7c46763 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.30.0
+ uses: googleapis/sdk-platform-java/java-shared-dependencies/unmanaged-dependency-check@google-cloud-shared-dependencies/v3.30.1
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 b20ec8ff352..532f888ecbb 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.30.0"
+ value: "gcr.io/cloud-devrel-public-resources/graalvm_sdk_platform_b:3.30.1"
}
env_vars: {
diff --git a/.kokoro/presubmit/graalvm-native.cfg b/.kokoro/presubmit/graalvm-native.cfg
index aad0db97859..1292cf163b1 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.30.0"
+ value: "gcr.io/cloud-devrel-public-resources/graalvm_sdk_platform_a:3.30.1"
}
env_vars: {
diff --git a/.readme-partials.yaml b/.readme-partials.yaml
index 5c4e1db63b7..d5be1cac251 100644
--- a/.readme-partials.yaml
+++ b/.readme-partials.yaml
@@ -144,12 +144,27 @@ custom_content: |
.build()
SpannerOptions options = SpannerOptions.newBuilder()
- // Inject OpenTelemetry object via Spanner Options or register OpenTelmetry object as Global
+ // Inject OpenTelemetry object via Spanner Options or register OpenTelemetry object as Global
.setOpenTelemetry(openTelemetry)
.build();
Spanner spanner = options.getService();
```
+
+ #### OpenTelemetry SQL Statement Tracing
+ The OpenTelemetry traces that are generated by the Java client include any request and transaction
+ tags that have been set. The traces can also include the SQL statements that are executed. Enable
+ this with the `enableExtendedTracing` option:
+
+ ```
+ SpannerOptions options = SpannerOptions.newBuilder()
+ .setOpenTelemetry(openTelemetry)
+ .setEnableExtendedTracing(true)
+ .build();
+ ```
+
+ This option can also be enabled by setting the environment variable
+ `SPANNER_ENABLE_EXTENDED_TRACING=true`.
### Instrument with OpenCensus
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a878fe0500f..f689e6160e6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,23 @@
# Changelog
+## [6.67.0](https://github.com/googleapis/java-spanner/compare/v6.66.0...v6.67.0) (2024-05-22)
+
+
+### Features
+
+* Add tracing for batchUpdate, executeUpdate, and connections ([#3097](https://github.com/googleapis/java-spanner/issues/3097)) ([45cdcfc](https://github.com/googleapis/java-spanner/commit/45cdcfcde02aa7976b017a90f81c2ccd28658c8f))
+
+
+### Performance Improvements
+
+* Minor optimizations to the standard query path ([#3101](https://github.com/googleapis/java-spanner/issues/3101)) ([ec820a1](https://github.com/googleapis/java-spanner/commit/ec820a16e2b3cb1a12a15231491b75cd73afaa13))
+
+
+### Dependencies
+
+* Update dependency com.google.cloud:google-cloud-monitoring to v3.44.0 ([#3099](https://github.com/googleapis/java-spanner/issues/3099)) ([da44e93](https://github.com/googleapis/java-spanner/commit/da44e932a39ac0124b63914f8ea926998c10ea2e))
+* Update dependency com.google.cloud:sdk-platform-java-config to v3.30.1 ([#3116](https://github.com/googleapis/java-spanner/issues/3116)) ([d205a73](https://github.com/googleapis/java-spanner/commit/d205a73714786a609673012b771e7a0722b3e1f2))
+
## [6.66.0](https://github.com/googleapis/java-spanner/compare/v6.65.1...v6.66.0) (2024-05-03)
diff --git a/README.md b/README.md
index 5333b8b4bda..038867956ae 100644
--- a/README.md
+++ b/README.md
@@ -42,7 +42,7 @@ If you are using Maven without the BOM, add this to your dependencies:
com.google.cloudgoogle-cloud-spanner
- 6.65.1
+ 6.66.0
```
@@ -50,20 +50,20 @@ If you are using Maven without the BOM, add this to your dependencies:
If you are using Gradle 5.x or later, add this to your dependencies:
```Groovy
-implementation platform('com.google.cloud:libraries-bom:26.38.0')
+implementation platform('com.google.cloud:libraries-bom:26.39.0')
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.65.1'
+implementation 'com.google.cloud:google-cloud-spanner:6.66.0'
```
If you are using SBT, add this to your dependencies:
```Scala
-libraryDependencies += "com.google.cloud" % "google-cloud-spanner" % "6.65.1"
+libraryDependencies += "com.google.cloud" % "google-cloud-spanner" % "6.66.0"
```
@@ -250,13 +250,28 @@ OpenTelemetry openTelemetry = OpenTelemetrySdk.builder()
.build()
SpannerOptions options = SpannerOptions.newBuilder()
-// Inject OpenTelemetry object via Spanner Options or register OpenTelmetry object as Global
+// Inject OpenTelemetry object via Spanner Options or register OpenTelemetry object as Global
.setOpenTelemetry(openTelemetry)
.build();
Spanner spanner = options.getService();
```
+#### OpenTelemetry SQL Statement Tracing
+The OpenTelemetry traces that are generated by the Java client include any request and transaction
+tags that have been set. The traces can also include the SQL statements that are executed. Enable
+this with the `enableExtendedTracing` option:
+
+```
+SpannerOptions options = SpannerOptions.newBuilder()
+ .setOpenTelemetry(openTelemetry)
+ .setEnableExtendedTracing(true)
+ .build();
+```
+
+This option can also be enabled by setting the environment variable
+`SPANNER_ENABLE_EXTENDED_TRACING=true`.
+
### Instrument with OpenCensus
> Note: OpenCensus project is deprecated. See [Sunsetting OpenCensus](https://opentelemetry.io/blog/2023/sunsetting-opencensus/).
@@ -436,6 +451,7 @@ Samples are in the [`samples/`](https://github.com/googleapis/java-spanner/tree/
| Add Json Column Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/AddJsonColumnSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/AddJsonColumnSample.java) |
| Add Jsonb Column Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/AddJsonbColumnSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/AddJsonbColumnSample.java) |
| Add Numeric Column Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/AddNumericColumnSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/AddNumericColumnSample.java) |
+| Add Proto Column Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/AddProtoColumnSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/AddProtoColumnSample.java) |
| Alter Sequence Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/AlterSequenceSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/AlterSequenceSample.java) |
| Alter Table With Foreign Key Delete Cascade Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/AlterTableWithForeignKeyDeleteCascadeSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/AlterTableWithForeignKeyDeleteCascadeSample.java) |
| Async Dml Example | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/AsyncDmlExample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/AsyncDmlExample.java) |
@@ -494,10 +510,12 @@ Samples are in the [`samples/`](https://github.com/googleapis/java-spanner/tree/
| Query With Json Parameter Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/QueryWithJsonParameterSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/QueryWithJsonParameterSample.java) |
| Query With Jsonb Parameter Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/QueryWithJsonbParameterSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/QueryWithJsonbParameterSample.java) |
| Query With Numeric Parameter Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/QueryWithNumericParameterSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/QueryWithNumericParameterSample.java) |
+| Query With Proto Parameter Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/QueryWithProtoParameterSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/QueryWithProtoParameterSample.java) |
| Quickstart Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/QuickstartSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/QuickstartSample.java) |
| Read Data With Database Role | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/ReadDataWithDatabaseRole.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/ReadDataWithDatabaseRole.java) |
| Restore Backup With Encryption Key | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/RestoreBackupWithEncryptionKey.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/RestoreBackupWithEncryptionKey.java) |
| Set Max Commit Delay Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/SetMaxCommitDelaySample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/SetMaxCommitDelaySample.java) |
+| Singer Proto | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/SingerProto.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/SingerProto.java) |
| Spanner Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/SpannerSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/SpannerSample.java) |
| Statement Timeout Example | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/StatementTimeoutExample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/StatementTimeoutExample.java) |
| Tag Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/TagSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/TagSample.java) |
@@ -509,6 +527,8 @@ Samples are in the [`samples/`](https://github.com/googleapis/java-spanner/tree/
| Update Json Data Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/UpdateJsonDataSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/UpdateJsonDataSample.java) |
| Update Jsonb Data Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/UpdateJsonbDataSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/UpdateJsonbDataSample.java) |
| Update Numeric Data Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/UpdateNumericDataSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/UpdateNumericDataSample.java) |
+| Update Proto Data Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/UpdateProtoDataSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/UpdateProtoDataSample.java) |
+| Update Proto Data Sample Using Dml | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/UpdateProtoDataSampleUsingDml.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/UpdateProtoDataSampleUsingDml.java) |
| Update Using Dml Returning Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/UpdateUsingDmlReturningSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/UpdateUsingDmlReturningSample.java) |
| Add And Drop Database Role | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/admin/archived/AddAndDropDatabaseRole.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/admin/archived/AddAndDropDatabaseRole.java) |
| Add Json Column Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/admin/archived/AddJsonColumnSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/admin/archived/AddJsonColumnSample.java) |
@@ -651,7 +671,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.65.1
+[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-spanner/6.66.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/benchmarks/pom.xml b/benchmarks/pom.xml
index bcd6137641f..4c5d4f6efbd 100644
--- a/benchmarks/pom.xml
+++ b/benchmarks/pom.xml
@@ -24,7 +24,7 @@
com.google.cloudgoogle-cloud-spanner-parent
- 6.66.0
+ 6.67.0
@@ -92,7 +92,7 @@
com.google.cloudgoogle-cloud-spanner
- 6.65.1
+ 6.66.0commons-cli
diff --git a/benchmarks/src/main/java/com/google/cloud/spanner/benchmark/BenchmarkRunner.java b/benchmarks/src/main/java/com/google/cloud/spanner/benchmark/BenchmarkRunner.java
index ded859c373c..7a731887a86 100644
--- a/benchmarks/src/main/java/com/google/cloud/spanner/benchmark/BenchmarkRunner.java
+++ b/benchmarks/src/main/java/com/google/cloud/spanner/benchmark/BenchmarkRunner.java
@@ -22,6 +22,7 @@
public interface BenchmarkRunner {
enum TransactionType {
READ_ONLY_SINGLE_USE,
+ READ_ONLY_MULTI_USE,
READ_WRITE
}
diff --git a/benchmarks/src/main/java/com/google/cloud/spanner/benchmark/JavaClientRunner.java b/benchmarks/src/main/java/com/google/cloud/spanner/benchmark/JavaClientRunner.java
index b5332e5a87c..6fc0842f376 100644
--- a/benchmarks/src/main/java/com/google/cloud/spanner/benchmark/JavaClientRunner.java
+++ b/benchmarks/src/main/java/com/google/cloud/spanner/benchmark/JavaClientRunner.java
@@ -20,6 +20,7 @@
import com.google.cloud.opentelemetry.trace.TraceExporter;
import com.google.cloud.spanner.DatabaseClient;
import com.google.cloud.spanner.DatabaseId;
+import com.google.cloud.spanner.ReadOnlyTransaction;
import com.google.cloud.spanner.ResultSet;
import com.google.cloud.spanner.SessionPoolOptions;
import com.google.cloud.spanner.SessionPoolOptionsHelper;
@@ -161,7 +162,10 @@ private Duration executeTransaction(
Stopwatch watch = Stopwatch.createStarted();
switch (transactionType) {
case READ_ONLY_SINGLE_USE:
- executeReadOnlyTransaction(client);
+ executeSingleUseReadOnlyTransaction(client);
+ break;
+ case READ_ONLY_MULTI_USE:
+ executeMultiUseReadOnlyTransaction(client);
break;
case READ_WRITE:
executeReadWriteTransaction(client);
@@ -172,7 +176,7 @@ private Duration executeTransaction(
return elapsedTime;
}
- private void executeReadOnlyTransaction(DatabaseClient client) {
+ private void executeSingleUseReadOnlyTransaction(DatabaseClient client) {
try (ResultSet resultSet = client.singleUse().executeQuery(getRandomisedReadStatement())) {
while (resultSet.next()) {
for (int i = 0; i < resultSet.getColumnCount(); i++) {
@@ -186,6 +190,34 @@ private void executeReadOnlyTransaction(DatabaseClient client) {
}
}
+ private void executeMultiUseReadOnlyTransaction(DatabaseClient client) {
+ try (ReadOnlyTransaction transaction = client.readOnlyTransaction()) {
+ ResultSet resultSet = transaction.executeQuery(getRandomisedReadStatement());
+ iterateResultSet(resultSet);
+
+ ResultSet resultSet1 = transaction.executeQuery(getRandomisedReadStatement());
+ iterateResultSet(resultSet1);
+
+ ResultSet resultSet2 = transaction.executeQuery(getRandomisedReadStatement());
+ iterateResultSet(resultSet2);
+
+ ResultSet resultSet3 = transaction.executeQuery(getRandomisedReadStatement());
+ iterateResultSet(resultSet3);
+ }
+ }
+
+ private void iterateResultSet(ResultSet resultSet) {
+ while (resultSet.next()) {
+ for (int i = 0; i < resultSet.getColumnCount(); i++) {
+ if (resultSet.isNull(i)) {
+ numNullValues++;
+ } else {
+ numNonNullValues++;
+ }
+ }
+ }
+ }
+
private void executeReadWriteTransaction(DatabaseClient client) {
client
.readWriteTransaction()
diff --git a/google-cloud-spanner-bom/pom.xml b/google-cloud-spanner-bom/pom.xml
index f73943c2924..375aafe0817 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.66.0
+ 6.67.0pomcom.google.cloudsdk-platform-java-config
- 3.30.0
+ 3.30.1Google Cloud Spanner BOM
@@ -53,43 +53,43 @@
com.google.cloudgoogle-cloud-spanner
- 6.66.0
+ 6.67.0com.google.cloudgoogle-cloud-spannertest-jar
- 6.66.0
+ 6.67.0com.google.api.grpcgrpc-google-cloud-spanner-v1
- 6.66.0
+ 6.67.0com.google.api.grpcgrpc-google-cloud-spanner-admin-instance-v1
- 6.66.0
+ 6.67.0com.google.api.grpcgrpc-google-cloud-spanner-admin-database-v1
- 6.66.0
+ 6.67.0com.google.api.grpcproto-google-cloud-spanner-admin-instance-v1
- 6.66.0
+ 6.67.0com.google.api.grpcproto-google-cloud-spanner-v1
- 6.66.0
+ 6.67.0com.google.api.grpcproto-google-cloud-spanner-admin-database-v1
- 6.66.0
+ 6.67.0
diff --git a/google-cloud-spanner-executor/pom.xml b/google-cloud-spanner-executor/pom.xml
index 21239775cee..ab7d060a86c 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.66.0
+ 6.67.0jarGoogle Cloud Spanner Executorcom.google.cloudgoogle-cloud-spanner-parent
- 6.66.0
+ 6.67.0
diff --git a/google-cloud-spanner/clirr-ignored-differences.xml b/google-cloud-spanner/clirr-ignored-differences.xml
index 92dcab6e2ce..e659fd125ce 100644
--- a/google-cloud-spanner/clirr-ignored-differences.xml
+++ b/google-cloud-spanner/clirr-ignored-differences.xml
@@ -668,5 +668,12 @@
com/google/cloud/spanner/connection/Connectioncom.google.cloud.spanner.connection.DdlInTransactionMode getDdlInTransactionMode()
+
+
+
+ 7012
+ com/google/cloud/spanner/SpannerOptions$SpannerEnvironment
+ boolean isEnableExtendedTracing()
+
diff --git a/google-cloud-spanner/pom.xml b/google-cloud-spanner/pom.xml
index 12f15f1eb31..ba3a19bd16a 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.66.0
+ 6.67.0jarGoogle Cloud Spannerhttps://github.com/googleapis/java-spanner
@@ -11,12 +11,11 @@
com.google.cloudgoogle-cloud-spanner-parent
- 6.66.0
+ 6.67.0google-cloud-spanner0.31.1
- 1.37.0com.google.cloud.spanner.GceTestEnvConfigprojects/gcloud-devel/instances/spanner-testing-east1gcloud-devel
@@ -414,25 +413,26 @@
io.opentelemetryopentelemetry-sdk
- ${opentelemetry.version}
+ test
+
+
+ io.opentelemetry
+ opentelemetry-sdk-commontestio.opentelemetryopentelemetry-sdk-metrics
- ${opentelemetry.version}testio.opentelemetryopentelemetry-sdk-trace
- ${opentelemetry.version}testio.opentelemetryopentelemetry-sdk-testing
- ${opentelemetry.version}test
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 4d17ba4e1b7..a424a93115a 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
@@ -45,6 +45,7 @@
import com.google.spanner.v1.DirectedReadOptions;
import com.google.spanner.v1.ExecuteBatchDmlRequest;
import com.google.spanner.v1.ExecuteSqlRequest;
+import com.google.spanner.v1.ExecuteSqlRequest.Builder;
import com.google.spanner.v1.ExecuteSqlRequest.QueryMode;
import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions;
import com.google.spanner.v1.PartialResultSet;
@@ -457,7 +458,7 @@ void initTransaction() {
// A per-transaction sequence number used to identify this ExecuteSqlRequests. Required for DML,
// ignored for query by the server.
- private AtomicLong seqNo = new AtomicLong();
+ private final AtomicLong seqNo = new AtomicLong();
// Allow up to 512MB to be buffered (assuming 1MB chunks). In practice, restart tokens are sent
// much more frequently.
@@ -488,6 +489,10 @@ long getSeqNo() {
return seqNo.incrementAndGet();
}
+ protected boolean isReadOnly() {
+ return true;
+ }
+
protected boolean isRouteToLeader() {
return false;
}
@@ -622,19 +627,18 @@ private ResultSet executeQueryInternal(
@VisibleForTesting
QueryOptions buildQueryOptions(QueryOptions requestOptions) {
// Shortcut for the most common return value.
- if (defaultQueryOptions.equals(QueryOptions.getDefaultInstance()) && requestOptions == null) {
- return QueryOptions.getDefaultInstance();
+ if (requestOptions == null) {
+ return defaultQueryOptions;
}
- // Create a builder based on the default query options.
- QueryOptions.Builder builder = defaultQueryOptions.toBuilder();
- // Then overwrite with specific options for this query.
- if (requestOptions != null) {
- builder.mergeFrom(requestOptions);
- }
- return builder.build();
+ return defaultQueryOptions.toBuilder().mergeFrom(requestOptions).build();
}
RequestOptions buildRequestOptions(Options options) {
+ // Shortcut for the most common return value.
+ if (!(options.hasPriority() || options.hasTag() || getTransactionTag() != null)) {
+ return RequestOptions.getDefaultInstance();
+ }
+
RequestOptions.Builder builder = RequestOptions.newBuilder();
if (options.hasPriority()) {
builder.setPriority(options.priority());
@@ -655,16 +659,7 @@ ExecuteSqlRequest.Builder getExecuteSqlRequestBuilder(
.setSql(statement.getSql())
.setQueryMode(queryMode)
.setSession(session.getName());
- Map stmtParameters = statement.getParameters();
- if (!stmtParameters.isEmpty()) {
- com.google.protobuf.Struct.Builder paramsBuilder = builder.getParamsBuilder();
- for (Map.Entry param : stmtParameters.entrySet()) {
- paramsBuilder.putFields(param.getKey(), Value.toProto(param.getValue()));
- if (param.getValue() != null && param.getValue().getType() != null) {
- builder.putParamTypes(param.getKey(), param.getValue().getType().toProto());
- }
- }
- }
+ addParameters(builder, statement.getParameters());
if (withTransactionSelector) {
TransactionSelector selector = getTransactionSelector();
if (selector != null) {
@@ -679,12 +674,26 @@ ExecuteSqlRequest.Builder getExecuteSqlRequestBuilder(
} else if (defaultDirectedReadOptions != null) {
builder.setDirectedReadOptions(defaultDirectedReadOptions);
}
- builder.setSeqno(getSeqNo());
+ if (!isReadOnly()) {
+ builder.setSeqno(getSeqNo());
+ }
builder.setQueryOptions(buildQueryOptions(statement.getQueryOptions()));
builder.setRequestOptions(buildRequestOptions(options));
return builder;
}
+ static void addParameters(ExecuteSqlRequest.Builder builder, Map stmtParameters) {
+ if (!stmtParameters.isEmpty()) {
+ com.google.protobuf.Struct.Builder paramsBuilder = builder.getParamsBuilder();
+ for (Map.Entry param : stmtParameters.entrySet()) {
+ paramsBuilder.putFields(param.getKey(), Value.toProto(param.getValue()));
+ if (param.getValue() != null && param.getValue().getType() != null) {
+ builder.putParamTypes(param.getKey(), param.getValue().getType().toProto());
+ }
+ }
+ }
+ }
+
ExecuteBatchDmlRequest.Builder getExecuteBatchDmlRequestBuilder(
Iterable statements, Options options) {
ExecuteBatchDmlRequest.Builder builder =
@@ -735,6 +744,7 @@ ResultSet executeQueryInternalWithOptions(
SpannerImpl.QUERY,
span,
tracer,
+ tracer.createStatementAttributes(statement, options),
rpc.getExecuteQueryRetrySettings(),
rpc.getExecuteQueryRetryableCodes()) {
@Override
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 b2d6b19a528..a20bcd9e925 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
@@ -92,7 +92,7 @@ public Timestamp write(final Iterable mutations) throws SpannerExcepti
public CommitResponse writeWithOptions(
final Iterable mutations, final TransactionOption... options)
throws SpannerException {
- ISpan span = tracer.spanBuilder(READ_WRITE_TRANSACTION);
+ ISpan span = tracer.spanBuilder(READ_WRITE_TRANSACTION, options);
try (IScope s = tracer.withSpan(span)) {
return runWithSessionRetry(session -> session.writeWithOptions(mutations, options));
} catch (RuntimeException e) {
@@ -112,7 +112,7 @@ public Timestamp writeAtLeastOnce(final Iterable mutations) throws Spa
public CommitResponse writeAtLeastOnceWithOptions(
final Iterable mutations, final TransactionOption... options)
throws SpannerException {
- ISpan span = tracer.spanBuilder(READ_WRITE_TRANSACTION);
+ ISpan span = tracer.spanBuilder(READ_WRITE_TRANSACTION, options);
try (IScope s = tracer.withSpan(span)) {
return runWithSessionRetry(
session -> session.writeAtLeastOnceWithOptions(mutations, options));
@@ -128,7 +128,7 @@ public CommitResponse writeAtLeastOnceWithOptions(
public ServerStream batchWriteAtLeastOnce(
final Iterable mutationGroups, final TransactionOption... options)
throws SpannerException {
- ISpan span = tracer.spanBuilder(READ_WRITE_TRANSACTION);
+ ISpan span = tracer.spanBuilder(READ_WRITE_TRANSACTION, options);
try (IScope s = tracer.withSpan(span)) {
return runWithSessionRetry(session -> session.batchWriteAtLeastOnce(mutationGroups, options));
} catch (RuntimeException e) {
@@ -213,7 +213,7 @@ public ReadOnlyTransaction readOnlyTransaction(TimestampBound bound) {
@Override
public TransactionRunner readWriteTransaction(TransactionOption... options) {
- ISpan span = tracer.spanBuilder(READ_WRITE_TRANSACTION);
+ ISpan span = tracer.spanBuilder(READ_WRITE_TRANSACTION, options);
try (IScope s = tracer.withSpan(span)) {
return getSession().readWriteTransaction(options);
} catch (RuntimeException e) {
@@ -225,7 +225,7 @@ public TransactionRunner readWriteTransaction(TransactionOption... options) {
@Override
public TransactionManager transactionManager(TransactionOption... options) {
- ISpan span = tracer.spanBuilder(READ_WRITE_TRANSACTION);
+ ISpan span = tracer.spanBuilder(READ_WRITE_TRANSACTION, options);
try (IScope s = tracer.withSpan(span)) {
return getSession().transactionManager(options);
} catch (RuntimeException e) {
@@ -237,7 +237,7 @@ public TransactionManager transactionManager(TransactionOption... options) {
@Override
public AsyncRunner runAsync(TransactionOption... options) {
- ISpan span = tracer.spanBuilder(READ_WRITE_TRANSACTION);
+ ISpan span = tracer.spanBuilder(READ_WRITE_TRANSACTION, options);
try (IScope s = tracer.withSpan(span)) {
return getSession().runAsync(options);
} catch (RuntimeException e) {
@@ -249,7 +249,7 @@ public AsyncRunner runAsync(TransactionOption... options) {
@Override
public AsyncTransactionManager transactionManagerAsync(TransactionOption... options) {
- ISpan span = tracer.spanBuilder(READ_WRITE_TRANSACTION);
+ ISpan span = tracer.spanBuilder(READ_WRITE_TRANSACTION, options);
try (IScope s = tracer.withSpan(span)) {
return getSession().transactionManagerAsync(options);
} catch (RuntimeException e) {
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/LatencyTest.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/LatencyTest.java
new file mode 100644
index 00000000000..4f70c32d2b4
--- /dev/null
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/LatencyTest.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2021 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
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.cloud.spanner;
+
+import com.google.auth.oauth2.GoogleCredentials;
+import com.google.cloud.spanner.SpannerOptions.FixedCloseableExecutorProvider;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.ThreadLocalRandom;
+import org.threeten.bp.Duration;
+
+public class LatencyTest {
+
+ public static void main(String[] args) throws Exception {
+ ThreadFactory threadFactory =
+ ThreadFactoryUtil.tryCreateVirtualThreadFactory("spanner-async-worker");
+ if (threadFactory == null) {
+ return;
+ }
+ ScheduledExecutorService service = Executors.newScheduledThreadPool(0, threadFactory);
+ Spanner spanner =
+ SpannerOptions.newBuilder()
+ .setCredentials(
+ GoogleCredentials.fromStream(
+ Files.newInputStream(
+ Paths.get("/Users/loite/Downloads/appdev-soda-spanner-staging.json"))))
+ .setSessionPoolOption(
+ SessionPoolOptions.newBuilder()
+ .setWaitForMinSessions(Duration.ofSeconds(5L))
+ // .setUseMultiplexedSession(true)
+ .build())
+ .setUseVirtualThreads(true)
+ .setAsyncExecutorProvider(FixedCloseableExecutorProvider.create(service))
+ .build()
+ .getService();
+ DatabaseClient client =
+ spanner.getDatabaseClient(
+ DatabaseId.of("appdev-soda-spanner-staging", "knut-test-ycsb", "latencytest"));
+ for (int i = 0; i < 1000000; i++) {
+ try (AsyncResultSet resultSet =
+ client
+ .singleUse()
+ .executeQueryAsync(
+ Statement.newBuilder("select col_varchar from latency_test where col_bigint=$1")
+ .bind("p1")
+ .to(ThreadLocalRandom.current().nextLong(100000L))
+ .build())) {
+ while (resultSet.next()) {
+ for (int col = 0; col < resultSet.getColumnCount(); col++) {
+ if (resultSet.getValue(col) == null) {
+ throw new IllegalStateException();
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Options.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Options.java
index 3dbd0c1cda3..58123ae36b8 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Options.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Options.java
@@ -355,6 +355,10 @@ static final class TagOption extends InternalOption implements ReadQueryUpdateTr
this.tag = tag;
}
+ String getTag() {
+ return tag;
+ }
+
@Override
void appendToOptions(Options options) {
options.tag = tag;
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/PartitionedDmlTransaction.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/PartitionedDmlTransaction.java
index 949265ea28a..82b7f06b7d2 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/PartitionedDmlTransaction.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/PartitionedDmlTransaction.java
@@ -221,14 +221,6 @@ private ByteString initTransaction(final Options options) {
private void setParameters(
final ExecuteSqlRequest.Builder requestBuilder,
final Map statementParameters) {
- if (!statementParameters.isEmpty()) {
- com.google.protobuf.Struct.Builder paramsBuilder = requestBuilder.getParamsBuilder();
- for (Map.Entry param : statementParameters.entrySet()) {
- paramsBuilder.putFields(param.getKey(), Value.toProto(param.getValue()));
- if (param.getValue() != null && param.getValue().getType() != null) {
- requestBuilder.putParamTypes(param.getKey(), param.getValue().getType().toProto());
- }
- }
- }
+ AbstractReadContext.addParameters(requestBuilder, statementParameters);
}
}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResumableStreamIterator.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResumableStreamIterator.java
index 590797c0999..d6d72aac33c 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResumableStreamIterator.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResumableStreamIterator.java
@@ -34,6 +34,7 @@
import com.google.protobuf.ByteString;
import com.google.spanner.v1.PartialResultSet;
import io.grpc.Context;
+import io.opentelemetry.api.common.Attributes;
import java.io.IOException;
import java.util.LinkedList;
import java.util.Objects;
@@ -59,7 +60,7 @@ abstract class ResumableStreamIterator extends AbstractIterator retryableCodes;
private static final Logger logger = Logger.getLogger(ResumableStreamIterator.class.getName());
- private final BackOff backOff;
+ private BackOff backOff;
private final LinkedList buffer = new LinkedList<>();
private final int maxBufferSize;
private final ISpan span;
@@ -81,13 +82,30 @@ protected ResumableStreamIterator(
TraceWrapper tracer,
RetrySettings streamingRetrySettings,
Set retryableCodes) {
+ this(
+ maxBufferSize,
+ streamName,
+ parent,
+ tracer,
+ Attributes.empty(),
+ streamingRetrySettings,
+ retryableCodes);
+ }
+
+ protected ResumableStreamIterator(
+ int maxBufferSize,
+ String streamName,
+ ISpan parent,
+ TraceWrapper tracer,
+ Attributes attributes,
+ RetrySettings streamingRetrySettings,
+ Set retryableCodes) {
checkArgument(maxBufferSize >= 0);
this.maxBufferSize = maxBufferSize;
this.tracer = tracer;
- this.span = tracer.spanBuilderWithExplicitParent(streamName, parent);
+ this.span = tracer.spanBuilderWithExplicitParent(streamName, parent, attributes);
this.streamingRetrySettings = Preconditions.checkNotNull(streamingRetrySettings);
this.retryableCodes = Preconditions.checkNotNull(retryableCodes);
- this.backOff = newBackOff();
}
private ExponentialBackOff newBackOff() {
@@ -252,7 +270,10 @@ protected PartialResultSet computeNext() {
if (delay != -1) {
backoffSleep(context, delay);
} else {
- backoffSleep(context, backOff);
+ if (this.backOff == null) {
+ this.backOff = newBackOff();
+ }
+ backoffSleep(context, this.backOff);
}
}
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 ab985cebf45..358944e8f36 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
@@ -99,6 +99,15 @@ interface SessionTransaction {
void close();
}
+ private static final Map[] CHANNEL_HINT_OPTIONS =
+ new Map[SpannerOptions.MAX_CHANNELS];
+
+ static {
+ for (int i = 0; i < CHANNEL_HINT_OPTIONS.length; i++) {
+ CHANNEL_HINT_OPTIONS[i] = optionMap(SessionOption.channelHint(i));
+ }
+ }
+
static final int NO_CHANNEL_HINT = -1;
private final SpannerImpl spanner;
@@ -125,7 +134,7 @@ interface SessionTransaction {
if (channelHint == NO_CHANNEL_HINT) {
return sessionReference.getOptions();
}
- return optionMap(SessionOption.channelHint(channelHint));
+ return CHANNEL_HINT_OPTIONS[channelHint % CHANNEL_HINT_OPTIONS.length];
}
@Override
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 382bef1b5a2..9e75e5e48c6 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
@@ -755,7 +755,7 @@ Builder setPoolMaintainerClock(Clock poolMaintainerClock) {
* SessionPoolOptions#maxSessions} based on the traffic load. Failing to do so will result in
* higher latencies.
*/
- Builder setUseMultiplexedSession(boolean useMultiplexedSession) {
+ public Builder setUseMultiplexedSession(boolean useMultiplexedSession) {
this.useMultiplexedSession = useMultiplexedSession;
return this;
}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java
index 86b5de01c69..6aa0d646a84 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java
@@ -63,11 +63,12 @@ class SpannerImpl extends BaseService implements Spanner {
final TraceWrapper tracer =
new TraceWrapper(
Tracing.getTracer(),
- this.getOptions()
+ getOptions()
.getOpenTelemetry()
.getTracer(
MetricRegistryConstants.INSTRUMENTATION_SCOPE,
- GaxProperties.getLibraryVersion(this.getOptions().getClass())));
+ GaxProperties.getLibraryVersion(this.getOptions().getClass())),
+ getOptions().isEnableExtendedTracing());
static final String CREATE_MULTIPLEXED_SESSION = "CloudSpannerOperation.CreateMultiplexedSession";
static final String CREATE_SESSION = "CloudSpannerOperation.CreateSession";
@@ -80,6 +81,8 @@ class SpannerImpl extends BaseService implements Spanner {
static final String QUERY = "CloudSpannerOperation.ExecuteStreamingQuery";
static final String READ = "CloudSpannerOperation.ExecuteStreamingRead";
static final String BATCH_WRITE = "CloudSpannerOperation.BatchWrite";
+ static final String UPDATE = "CloudSpannerOperation.ExecuteUpdate";
+ static final String BATCH_UPDATE = "CloudSpannerOperation.BatchUpdate";
private static final Object CLIENT_ID_LOCK = new Object();
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java
index a16be179ce3..639943d9970 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java
@@ -99,7 +99,7 @@ public class SpannerOptions extends ServiceOptions {
ImmutableSet.of(
"https://www.googleapis.com/auth/spanner.admin",
"https://www.googleapis.com/auth/spanner.data");
- private static final int MAX_CHANNELS = 256;
+ static final int MAX_CHANNELS = 256;
@VisibleForTesting static final int DEFAULT_CHANNELS = 4;
// Set the default number of channels to GRPC_GCP_ENABLED_DEFAULT_CHANNELS when gRPC-GCP extension
// is enabled, to make sure there are sufficient channels available to move the sessions to a
@@ -149,6 +149,7 @@ public class SpannerOptions extends ServiceOptions {
private final DirectedReadOptions directedReadOptions;
private final boolean useVirtualThreads;
private final OpenTelemetry openTelemetry;
+ private final boolean enableExtendedTracing;
enum TracingFramework {
OPEN_CENSUS,
@@ -653,6 +654,7 @@ protected SpannerOptions(Builder builder) {
directedReadOptions = builder.directedReadOptions;
useVirtualThreads = builder.useVirtualThreads;
openTelemetry = builder.openTelemetry;
+ enableExtendedTracing = builder.enableExtendedTracing;
}
/**
@@ -665,7 +667,9 @@ public interface SpannerEnvironment {
* set.
*/
@Nonnull
- String getOptimizerVersion();
+ default String getOptimizerVersion() {
+ return "";
+ }
/**
* The optimizer statistics package to use. Must return an empty string to indicate that no
@@ -673,7 +677,11 @@ public interface SpannerEnvironment {
*/
@Nonnull
default String getOptimizerStatisticsPackage() {
- throw new UnsupportedOperationException("Unimplemented");
+ return "";
+ }
+
+ default boolean isEnableExtendedTracing() {
+ return false;
}
}
@@ -686,19 +694,27 @@ private static class SpannerEnvironmentImpl implements SpannerEnvironment {
private static final String SPANNER_OPTIMIZER_VERSION_ENV_VAR = "SPANNER_OPTIMIZER_VERSION";
private static final String SPANNER_OPTIMIZER_STATISTICS_PACKAGE_ENV_VAR =
"SPANNER_OPTIMIZER_STATISTICS_PACKAGE";
+ private static final String SPANNER_ENABLE_EXTENDED_TRACING = "SPANNER_ENABLE_EXTENDED_TRACING";
private SpannerEnvironmentImpl() {}
+ @Nonnull
@Override
public String getOptimizerVersion() {
return MoreObjects.firstNonNull(System.getenv(SPANNER_OPTIMIZER_VERSION_ENV_VAR), "");
}
+ @Nonnull
@Override
public String getOptimizerStatisticsPackage() {
return MoreObjects.firstNonNull(
System.getenv(SPANNER_OPTIMIZER_STATISTICS_PACKAGE_ENV_VAR), "");
}
+
+ @Override
+ public boolean isEnableExtendedTracing() {
+ return Boolean.parseBoolean(System.getenv(SPANNER_ENABLE_EXTENDED_TRACING));
+ }
}
/** Builder for {@link SpannerOptions} instances. */
@@ -762,6 +778,7 @@ public static class Builder
private DirectedReadOptions directedReadOptions;
private boolean useVirtualThreads = false;
private OpenTelemetry openTelemetry;
+ private boolean enableExtendedTracing = SpannerOptions.environment.isEnableExtendedTracing();
private static String createCustomClientLibToken(String token) {
return token + " " + ServiceOptions.getGoogApiClientLibName();
@@ -825,6 +842,7 @@ protected Builder() {
this.attemptDirectPath = options.attemptDirectPath;
this.directedReadOptions = options.directedReadOptions;
this.useVirtualThreads = options.useVirtualThreads;
+ this.enableExtendedTracing = options.enableExtendedTracing;
}
@Override
@@ -1321,6 +1339,19 @@ protected Builder setUseVirtualThreads(boolean useVirtualThreads) {
return this;
}
+ /**
+ * Sets whether to enable extended OpenTelemetry tracing. Enabling this option will add the
+ * following additional attributes to the traces that are generated by the client:
+ *
+ *
+ *
db.statement: Contains the SQL statement that is being executed.
+ *
+ */
+ public Builder setEnableExtendedTracing(boolean enableExtendedTracing) {
+ this.enableExtendedTracing = enableExtendedTracing;
+ return this;
+ }
+
@SuppressWarnings("rawtypes")
@Override
public SpannerOptions build() {
@@ -1563,6 +1594,18 @@ public boolean isUseVirtualThreads() {
return useVirtualThreads;
}
+ /**
+ * Returns whether extended OpenTelemetry tracing is enabled. Enabling this option will add the
+ * following additional attributes to the traces that are generated by the client:
+ *
+ *
+ *
db.statement: Contains the SQL statement that is being executed.
+ *
+ */
+ public boolean isEnableExtendedTracing() {
+ return enableExtendedTracing;
+ }
+
/** Returns the default query options to use for the specific database. */
public QueryOptions getDefaultQueryOptions(DatabaseId databaseId) {
// Use the specific query options for the database if any have been specified. These have
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TraceWrapper.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TraceWrapper.java
index 25796968e9e..77dbc010c4d 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TraceWrapper.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TraceWrapper.java
@@ -16,48 +16,76 @@
package com.google.cloud.spanner;
+import com.google.cloud.spanner.Options.TagOption;
+import com.google.cloud.spanner.Options.TransactionOption;
import com.google.cloud.spanner.SpannerOptions.TracingFramework;
import io.opencensus.trace.BlankSpan;
import io.opencensus.trace.Span;
import io.opencensus.trace.Tracer;
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.common.AttributesBuilder;
import io.opentelemetry.context.Context;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
class TraceWrapper {
+ private static final AttributeKey TRANSACTION_TAG_KEY =
+ AttributeKey.stringKey("transaction.tag");
+ private static final AttributeKey STATEMENT_TAG_KEY =
+ AttributeKey.stringKey("statement.tag");
+ private static final AttributeKey DB_STATEMENT_KEY =
+ AttributeKey.stringKey("db.statement");
+ private static final AttributeKey> DB_STATEMENT_ARRAY_KEY =
+ AttributeKey.stringArrayKey("db.statement");
private final Tracer openCensusTracer;
private final io.opentelemetry.api.trace.Tracer openTelemetryTracer;
+ private final boolean enableExtendedTracing;
- TraceWrapper(Tracer openCensusTracer, io.opentelemetry.api.trace.Tracer openTelemetryTracer) {
+ TraceWrapper(
+ Tracer openCensusTracer,
+ io.opentelemetry.api.trace.Tracer openTelemetryTracer,
+ boolean enableExtendedTracing) {
this.openTelemetryTracer = openTelemetryTracer;
this.openCensusTracer = openCensusTracer;
+ this.enableExtendedTracing = enableExtendedTracing;
}
ISpan spanBuilder(String spanName) {
+ return spanBuilder(spanName, Attributes.empty());
+ }
+
+ ISpan spanBuilder(String spanName, TransactionOption... options) {
+ return spanBuilder(spanName, createTransactionAttributes(options));
+ }
+
+ ISpan spanBuilder(String spanName, Attributes attributes) {
if (SpannerOptions.getActiveTracingFramework().equals(TracingFramework.OPEN_TELEMETRY)) {
- return new OpenTelemetrySpan(openTelemetryTracer.spanBuilder(spanName).startSpan());
+ return new OpenTelemetrySpan(
+ openTelemetryTracer.spanBuilder(spanName).setAllAttributes(attributes).startSpan());
} else {
return new OpenCensusSpan(openCensusTracer.spanBuilder(spanName).startSpan());
}
}
ISpan spanBuilderWithExplicitParent(String spanName, ISpan parentSpan) {
+ return spanBuilderWithExplicitParent(spanName, parentSpan, Attributes.empty());
+ }
+
+ ISpan spanBuilderWithExplicitParent(String spanName, ISpan parentSpan, Attributes attributes) {
if (SpannerOptions.getActiveTracingFramework().equals(TracingFramework.OPEN_TELEMETRY)) {
OpenTelemetrySpan otParentSpan = (OpenTelemetrySpan) parentSpan;
- io.opentelemetry.api.trace.Span otSpan;
-
+ io.opentelemetry.api.trace.SpanBuilder otSpan =
+ openTelemetryTracer.spanBuilder(spanName).setAllAttributes(attributes);
if (otParentSpan != null && otParentSpan.getOpenTelemetrySpan() != null) {
- otSpan =
- openTelemetryTracer
- .spanBuilder(spanName)
- .setParent(Context.current().with(otParentSpan.getOpenTelemetrySpan()))
- .startSpan();
- } else {
- otSpan = openTelemetryTracer.spanBuilder(spanName).startSpan();
+ otSpan = otSpan.setParent(Context.current().with(otParentSpan.getOpenTelemetrySpan()));
}
-
- return new OpenTelemetrySpan(otSpan);
-
+ return new OpenTelemetrySpan(otSpan.startSpan());
} else {
OpenCensusSpan parentOcSpan = (OpenCensusSpan) parentSpan;
Span ocSpan =
@@ -106,4 +134,50 @@ IScope withSpan(ISpan span) {
return new OpenCensusScope(openCensusTracer.withSpan(openCensusSpan.getOpenCensusSpan()));
}
}
+
+ Attributes createTransactionAttributes(TransactionOption... options) {
+ if (options != null && options.length > 0) {
+ Optional tagOption =
+ Arrays.stream(options)
+ .filter(option -> option instanceof TagOption)
+ .map(option -> (TagOption) option)
+ .findAny();
+ if (tagOption.isPresent()) {
+ return Attributes.of(TRANSACTION_TAG_KEY, tagOption.get().getTag());
+ }
+ }
+ return Attributes.empty();
+ }
+
+ Attributes createStatementAttributes(Statement statement, Options options) {
+ if (this.enableExtendedTracing || (options != null && options.hasTag())) {
+ AttributesBuilder builder = Attributes.builder();
+ if (this.enableExtendedTracing) {
+ builder.put(DB_STATEMENT_KEY, statement.getSql());
+ }
+ if (options != null && options.hasTag()) {
+ builder.put(STATEMENT_TAG_KEY, options.tag());
+ }
+ return builder.build();
+ }
+ return Attributes.empty();
+ }
+
+ Attributes createStatementBatchAttributes(Iterable statements, Options options) {
+ if (this.enableExtendedTracing || (options != null && options.hasTag())) {
+ AttributesBuilder builder = Attributes.builder();
+ if (this.enableExtendedTracing) {
+ builder.put(
+ DB_STATEMENT_ARRAY_KEY,
+ StreamSupport.stream(statements.spliterator(), false)
+ .map(Statement::getSql)
+ .collect(Collectors.toList()));
+ }
+ if (options != null && options.hasTag()) {
+ builder.put(STATEMENT_TAG_KEY, options.tag());
+ }
+ return builder.build();
+ }
+ return Attributes.empty();
+ }
}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java
index 4deeeb92af8..a5401480e06 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java
@@ -18,6 +18,8 @@
import static com.google.cloud.spanner.SpannerExceptionFactory.newSpannerBatchUpdateException;
import static com.google.cloud.spanner.SpannerExceptionFactory.newSpannerException;
+import static com.google.cloud.spanner.SpannerImpl.BATCH_UPDATE;
+import static com.google.cloud.spanner.SpannerImpl.UPDATE;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
@@ -216,6 +218,11 @@ private TransactionContextImpl(Builder builder) {
session.getOptions(), ThreadLocalRandom.current().nextLong(Long.MAX_VALUE));
}
+ @Override
+ protected boolean isReadOnly() {
+ return false;
+ }
+
@Override
protected boolean isRouteToLeader() {
return true;
@@ -722,7 +729,7 @@ public com.google.cloud.spanner.ResultSet analyzeUpdateStatement(
}
private ResultSet internalAnalyzeStatement(
- Statement statement, QueryAnalyzeMode analyzeMode, UpdateOption... options) {
+ Statement statement, QueryAnalyzeMode analyzeMode, UpdateOption... updateOptions) {
Preconditions.checkNotNull(analyzeMode);
QueryMode queryMode;
switch (analyzeMode) {
@@ -736,20 +743,28 @@ private ResultSet internalAnalyzeStatement(
throw SpannerExceptionFactory.newSpannerException(
ErrorCode.INVALID_ARGUMENT, "Unknown analyze mode: " + analyzeMode);
}
+ final Options options = Options.fromUpdateOptions(updateOptions);
return internalExecuteUpdate(statement, queryMode, options);
}
@Override
- public long executeUpdate(Statement statement, UpdateOption... options) {
- ResultSet resultSet = internalExecuteUpdate(statement, QueryMode.NORMAL, options);
- // For standard DML, using the exact row count.
- return resultSet.getStats().getRowCountExact();
+ public long executeUpdate(Statement statement, UpdateOption... updateOptions) {
+ final Options options = Options.fromUpdateOptions(updateOptions);
+ ISpan span =
+ tracer.spanBuilderWithExplicitParent(
+ UPDATE, this.span, this.tracer.createStatementAttributes(statement, options));
+ try (IScope ignore = tracer.withSpan(span)) {
+ ResultSet resultSet = internalExecuteUpdate(statement, QueryMode.NORMAL, options);
+ // For standard DML, using the exact row count.
+ return resultSet.getStats().getRowCountExact();
+ } finally {
+ span.end();
+ }
}
private ResultSet internalExecuteUpdate(
- Statement statement, QueryMode queryMode, UpdateOption... updateOptions) {
+ Statement statement, QueryMode queryMode, Options options) {
beforeReadOrQuery();
- final Options options = Options.fromUpdateOptions(updateOptions);
if (options.withExcludeTxnFromChangeStreams() != null) {
throw newSpannerException(
ErrorCode.INVALID_ARGUMENT, DML_INVALID_EXCLUDE_CHANGE_STREAMS_OPTION_MESSAGE);
@@ -778,70 +793,80 @@ private ResultSet internalExecuteUpdate(
@Override
public ApiFuture executeUpdateAsync(Statement statement, UpdateOption... updateOptions) {
- beforeReadOrQuery();
final Options options = Options.fromUpdateOptions(updateOptions);
- if (options.withExcludeTxnFromChangeStreams() != null) {
- throw newSpannerException(
- ErrorCode.INVALID_ARGUMENT, DML_INVALID_EXCLUDE_CHANGE_STREAMS_OPTION_MESSAGE);
- }
- final ExecuteSqlRequest.Builder builder =
- getExecuteSqlRequestBuilder(
- statement, QueryMode.NORMAL, options, /* withTransactionSelector = */ true);
- final ApiFuture resultSet;
- try {
- // Register the update as an async operation that must finish before the transaction may
- // commit.
- increaseAsyncOperations();
- resultSet = rpc.executeQueryAsync(builder.build(), session.getOptions(), isRouteToLeader());
- session.markUsed(clock.instant());
- } catch (Throwable t) {
- decreaseAsyncOperations();
- throw t;
- }
- ApiFuture updateCount =
- ApiFutures.transform(
- resultSet,
- input -> {
- if (!input.hasStats()) {
- throw SpannerExceptionFactory.newSpannerException(
- ErrorCode.INVALID_ARGUMENT,
- "DML response missing stats possibly due to non-DML statement as input");
- }
- if (builder.getTransaction().hasBegin()
- && !(input.getMetadata().hasTransaction()
- && input.getMetadata().getTransaction().getId() != ByteString.EMPTY)) {
- throw SpannerExceptionFactory.newSpannerException(
- ErrorCode.FAILED_PRECONDITION, NO_TRANSACTION_RETURNED_MSG);
+ ISpan span =
+ tracer.spanBuilderWithExplicitParent(
+ UPDATE, this.span, this.tracer.createStatementAttributes(statement, options));
+ try (IScope ignore = tracer.withSpan(span)) {
+ beforeReadOrQuery();
+ if (options.withExcludeTxnFromChangeStreams() != null) {
+ throw newSpannerException(
+ ErrorCode.INVALID_ARGUMENT, DML_INVALID_EXCLUDE_CHANGE_STREAMS_OPTION_MESSAGE);
+ }
+ final ExecuteSqlRequest.Builder builder =
+ getExecuteSqlRequestBuilder(
+ statement, QueryMode.NORMAL, options, /* withTransactionSelector = */ true);
+ final ApiFuture resultSet;
+ try {
+ // Register the update as an async operation that must finish before the transaction may
+ // commit.
+ increaseAsyncOperations();
+ resultSet =
+ rpc.executeQueryAsync(builder.build(), session.getOptions(), isRouteToLeader());
+ session.markUsed(clock.instant());
+ } catch (Throwable t) {
+ decreaseAsyncOperations();
+ throw t;
+ }
+ ApiFuture updateCount =
+ ApiFutures.transform(
+ resultSet,
+ input -> {
+ if (!input.hasStats()) {
+ throw SpannerExceptionFactory.newSpannerException(
+ ErrorCode.INVALID_ARGUMENT,
+ "DML response missing stats possibly due to non-DML statement as input");
+ }
+ if (builder.getTransaction().hasBegin()
+ && !(input.getMetadata().hasTransaction()
+ && input.getMetadata().getTransaction().getId() != ByteString.EMPTY)) {
+ throw SpannerExceptionFactory.newSpannerException(
+ ErrorCode.FAILED_PRECONDITION, NO_TRANSACTION_RETURNED_MSG);
+ }
+ // For standard DML, using the exact row count.
+ return input.getStats().getRowCountExact();
+ },
+ MoreExecutors.directExecutor());
+ updateCount =
+ ApiFutures.catching(
+ updateCount,
+ Throwable.class,
+ input -> {
+ SpannerException e = SpannerExceptionFactory.asSpannerException(input);
+ SpannerException exceptionToThrow =
+ onError(e, builder.getTransaction().hasBegin());
+ span.setStatus(exceptionToThrow);
+ throw exceptionToThrow;
+ },
+ MoreExecutors.directExecutor());
+ updateCount.addListener(
+ () -> {
+ try {
+ if (resultSet.get().getMetadata().hasTransaction()) {
+ onTransactionMetadata(
+ resultSet.get().getMetadata().getTransaction(),
+ builder.getTransaction().hasBegin());
}
- // For standard DML, using the exact row count.
- return input.getStats().getRowCountExact();
- },
- MoreExecutors.directExecutor());
- updateCount =
- ApiFutures.catching(
- updateCount,
- Throwable.class,
- input -> {
- SpannerException e = SpannerExceptionFactory.asSpannerException(input);
- throw onError(e, builder.getTransaction().hasBegin());
- },
- MoreExecutors.directExecutor());
- updateCount.addListener(
- () -> {
- try {
- if (resultSet.get().getMetadata().hasTransaction()) {
- onTransactionMetadata(
- resultSet.get().getMetadata().getTransaction(),
- builder.getTransaction().hasBegin());
+ } catch (Throwable e) {
+ // Ignore this error here as it is handled by the future that is returned by the
+ // executeUpdateAsync method.
}
- } catch (Throwable e) {
- // Ignore this error here as it is handled by the future that is returned by the
- // executeUpdateAsync method.
- }
- decreaseAsyncOperations();
- },
- MoreExecutors.directExecutor());
- return updateCount;
+ span.end();
+ decreaseAsyncOperations();
+ },
+ MoreExecutors.directExecutor());
+ return updateCount;
+ }
}
private SpannerException createAbortedExceptionForBatchDml(ExecuteBatchDmlResponse response) {
@@ -859,104 +884,131 @@ private SpannerException createAbortedExceptionForBatchDml(ExecuteBatchDmlRespon
@Override
public long[] batchUpdate(Iterable statements, UpdateOption... updateOptions) {
- beforeReadOrQuery();
final Options options = Options.fromUpdateOptions(updateOptions);
- if (options.withExcludeTxnFromChangeStreams() != null) {
- throw newSpannerException(
- ErrorCode.INVALID_ARGUMENT, DML_INVALID_EXCLUDE_CHANGE_STREAMS_OPTION_MESSAGE);
- }
- final ExecuteBatchDmlRequest.Builder builder =
- getExecuteBatchDmlRequestBuilder(statements, options);
- try {
- com.google.spanner.v1.ExecuteBatchDmlResponse response =
- rpc.executeBatchDml(builder.build(), session.getOptions());
- session.markUsed(clock.instant());
- long[] results = new long[response.getResultSetsCount()];
- for (int i = 0; i < response.getResultSetsCount(); ++i) {
- results[i] = response.getResultSets(i).getStats().getRowCountExact();
- if (response.getResultSets(i).getMetadata().hasTransaction()) {
- onTransactionMetadata(
- response.getResultSets(i).getMetadata().getTransaction(),
- builder.getTransaction().hasBegin());
- }
+ ISpan span =
+ tracer.spanBuilderWithExplicitParent(
+ BATCH_UPDATE,
+ this.span,
+ this.tracer.createStatementBatchAttributes(statements, options));
+ try (IScope ignore = tracer.withSpan(span)) {
+ beforeReadOrQuery();
+ if (options.withExcludeTxnFromChangeStreams() != null) {
+ throw newSpannerException(
+ ErrorCode.INVALID_ARGUMENT, DML_INVALID_EXCLUDE_CHANGE_STREAMS_OPTION_MESSAGE);
}
+ final ExecuteBatchDmlRequest.Builder builder =
+ getExecuteBatchDmlRequestBuilder(statements, options);
+ try {
+ com.google.spanner.v1.ExecuteBatchDmlResponse response =
+ rpc.executeBatchDml(builder.build(), session.getOptions());
+ session.markUsed(clock.instant());
+ long[] results = new long[response.getResultSetsCount()];
+ for (int i = 0; i < response.getResultSetsCount(); ++i) {
+ results[i] = response.getResultSets(i).getStats().getRowCountExact();
+ if (response.getResultSets(i).getMetadata().hasTransaction()) {
+ onTransactionMetadata(
+ response.getResultSets(i).getMetadata().getTransaction(),
+ builder.getTransaction().hasBegin());
+ }
+ }
- // If one of the DML statements was aborted, we should throw an aborted exception.
- // In all other cases, we should throw a BatchUpdateException.
- if (response.getStatus().getCode() == Code.ABORTED_VALUE) {
- throw createAbortedExceptionForBatchDml(response);
- } else if (response.getStatus().getCode() != 0) {
- throw newSpannerBatchUpdateException(
- ErrorCode.fromRpcStatus(response.getStatus()),
- response.getStatus().getMessage(),
- results);
+ // If one of the DML statements was aborted, we should throw an aborted exception.
+ // In all other cases, we should throw a BatchUpdateException.
+ if (response.getStatus().getCode() == Code.ABORTED_VALUE) {
+ throw createAbortedExceptionForBatchDml(response);
+ } else if (response.getStatus().getCode() != 0) {
+ throw newSpannerBatchUpdateException(
+ ErrorCode.fromRpcStatus(response.getStatus()),
+ response.getStatus().getMessage(),
+ results);
+ }
+ return results;
+ } catch (Throwable e) {
+ throw onError(
+ SpannerExceptionFactory.asSpannerException(e), builder.getTransaction().hasBegin());
}
- return results;
- } catch (Throwable e) {
- throw onError(
- SpannerExceptionFactory.asSpannerException(e), builder.getTransaction().hasBegin());
+ } catch (Throwable throwable) {
+ span.setStatus(throwable);
+ throw throwable;
+ } finally {
+ span.end();
}
}
@Override
public ApiFuture batchUpdateAsync(
Iterable statements, UpdateOption... updateOptions) {
- beforeReadOrQuery();
final Options options = Options.fromUpdateOptions(updateOptions);
- if (options.withExcludeTxnFromChangeStreams() != null) {
- throw newSpannerException(
- ErrorCode.INVALID_ARGUMENT, DML_INVALID_EXCLUDE_CHANGE_STREAMS_OPTION_MESSAGE);
- }
- final ExecuteBatchDmlRequest.Builder builder =
- getExecuteBatchDmlRequestBuilder(statements, options);
- ApiFuture response;
- try {
- // Register the update as an async operation that must finish before the transaction may
- // commit.
- increaseAsyncOperations();
- response = rpc.executeBatchDmlAsync(builder.build(), session.getOptions());
- session.markUsed(clock.instant());
- } catch (Throwable t) {
- decreaseAsyncOperations();
- throw t;
- }
- ApiFuture updateCounts =
- ApiFutures.transform(
- response,
- batchDmlResponse -> {
- long[] results = new long[batchDmlResponse.getResultSetsCount()];
- for (int i = 0; i < batchDmlResponse.getResultSetsCount(); ++i) {
- results[i] = batchDmlResponse.getResultSets(i).getStats().getRowCountExact();
- if (batchDmlResponse.getResultSets(i).getMetadata().hasTransaction()) {
- onTransactionMetadata(
- batchDmlResponse.getResultSets(i).getMetadata().getTransaction(),
- builder.getTransaction().hasBegin());
+ ISpan span =
+ tracer.spanBuilderWithExplicitParent(
+ BATCH_UPDATE,
+ this.span,
+ this.tracer.createStatementBatchAttributes(statements, options));
+ try (IScope ignore = tracer.withSpan(span)) {
+ beforeReadOrQuery();
+ if (options.withExcludeTxnFromChangeStreams() != null) {
+ throw newSpannerException(
+ ErrorCode.INVALID_ARGUMENT, DML_INVALID_EXCLUDE_CHANGE_STREAMS_OPTION_MESSAGE);
+ }
+ final ExecuteBatchDmlRequest.Builder builder =
+ getExecuteBatchDmlRequestBuilder(statements, options);
+ ApiFuture response;
+ try {
+ // Register the update as an async operation that must finish before the transaction may
+ // commit.
+ increaseAsyncOperations();
+ response = rpc.executeBatchDmlAsync(builder.build(), session.getOptions());
+ session.markUsed(clock.instant());
+ } catch (Throwable t) {
+ decreaseAsyncOperations();
+ throw t;
+ }
+ ApiFuture updateCounts =
+ ApiFutures.transform(
+ response,
+ batchDmlResponse -> {
+ long[] results = new long[batchDmlResponse.getResultSetsCount()];
+ for (int i = 0; i < batchDmlResponse.getResultSetsCount(); ++i) {
+ results[i] = batchDmlResponse.getResultSets(i).getStats().getRowCountExact();
+ if (batchDmlResponse.getResultSets(i).getMetadata().hasTransaction()) {
+ onTransactionMetadata(
+ batchDmlResponse.getResultSets(i).getMetadata().getTransaction(),
+ builder.getTransaction().hasBegin());
+ }
}
- }
- // If one of the DML statements was aborted, we should throw an aborted exception.
- // In all other cases, we should throw a BatchUpdateException.
- if (batchDmlResponse.getStatus().getCode() == Code.ABORTED_VALUE) {
- throw createAbortedExceptionForBatchDml(batchDmlResponse);
- } else if (batchDmlResponse.getStatus().getCode() != 0) {
- throw newSpannerBatchUpdateException(
- ErrorCode.fromRpcStatus(batchDmlResponse.getStatus()),
- batchDmlResponse.getStatus().getMessage(),
- results);
- }
- return results;
- },
- MoreExecutors.directExecutor());
- updateCounts =
- ApiFutures.catching(
- updateCounts,
- Throwable.class,
- input -> {
- SpannerException e = SpannerExceptionFactory.asSpannerException(input);
- throw onError(e, builder.getTransaction().hasBegin());
- },
- MoreExecutors.directExecutor());
- updateCounts.addListener(this::decreaseAsyncOperations, MoreExecutors.directExecutor());
- return updateCounts;
+ // If one of the DML statements was aborted, we should throw an aborted exception.
+ // In all other cases, we should throw a BatchUpdateException.
+ if (batchDmlResponse.getStatus().getCode() == Code.ABORTED_VALUE) {
+ throw createAbortedExceptionForBatchDml(batchDmlResponse);
+ } else if (batchDmlResponse.getStatus().getCode() != 0) {
+ throw newSpannerBatchUpdateException(
+ ErrorCode.fromRpcStatus(batchDmlResponse.getStatus()),
+ batchDmlResponse.getStatus().getMessage(),
+ results);
+ }
+ return results;
+ },
+ MoreExecutors.directExecutor());
+ updateCounts =
+ ApiFutures.catching(
+ updateCounts,
+ Throwable.class,
+ input -> {
+ SpannerException e = SpannerExceptionFactory.asSpannerException(input);
+ SpannerException exceptionToThrow =
+ onError(e, builder.getTransaction().hasBegin());
+ span.setStatus(exceptionToThrow);
+ throw exceptionToThrow;
+ },
+ MoreExecutors.directExecutor());
+ updateCounts.addListener(
+ () -> {
+ span.end();
+ decreaseAsyncOperations();
+ },
+ MoreExecutors.directExecutor());
+ return updateCounts;
+ }
}
private ListenableAsyncResultSet wrap(ListenableAsyncResultSet delegate) {
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AbstractBaseUnitOfWork.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AbstractBaseUnitOfWork.java
index bd43daf9e57..698e20dca26 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AbstractBaseUnitOfWork.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AbstractBaseUnitOfWork.java
@@ -45,6 +45,8 @@
import io.grpc.Context;
import io.grpc.MethodDescriptor;
import io.grpc.Status;
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.trace.Span;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
@@ -63,10 +65,16 @@
/** Base for all {@link Connection}-based transactions and batches. */
abstract class AbstractBaseUnitOfWork implements UnitOfWork {
+ static final String DB_STATEMENT = "db.statement";
+ static final AttributeKey DB_STATEMENT_KEY = AttributeKey.stringKey(DB_STATEMENT);
+ static final AttributeKey> DB_STATEMENT_ARRAY_KEY =
+ AttributeKey.stringArrayKey(DB_STATEMENT);
+
private final StatementExecutor statementExecutor;
private final StatementTimeout statementTimeout;
protected final String transactionTag;
protected final RpcPriority rpcPriority;
+ protected final Span span;
/** Class for keeping track of the stacktrace of the caller of an async statement. */
static final class SpannerAsyncExecutionException extends RuntimeException {
@@ -100,6 +108,7 @@ abstract static class Builder, T extends AbstractBaseUni
private StatementTimeout statementTimeout = new StatementTimeout();
private String transactionTag;
private RpcPriority rpcPriority;
+ private Span span;
Builder() {}
@@ -130,6 +139,11 @@ B setRpcPriority(@Nullable RpcPriority rpcPriority) {
return self();
}
+ B setSpan(@Nullable Span span) {
+ this.span = span;
+ return self();
+ }
+
abstract T build();
}
@@ -139,8 +153,22 @@ B setRpcPriority(@Nullable RpcPriority rpcPriority) {
this.statementTimeout = builder.statementTimeout;
this.transactionTag = builder.transactionTag;
this.rpcPriority = builder.rpcPriority;
+ this.span = Preconditions.checkNotNull(builder.span);
+ }
+
+ ApiFuture asyncEndUnitOfWorkSpan() {
+ return this.statementExecutor.submit(this::endUnitOfWorkSpan);
}
+ private Void endUnitOfWorkSpan() {
+ if (this.span != null) {
+ this.span.end();
+ }
+ return null;
+ }
+
+ abstract boolean isSingleUse();
+
/**
* Returns a descriptive name for the type of transaction / unit of work. This is used in error
* messages.
@@ -346,6 +374,9 @@ public void run() {
currentlyRunningStatementFuture = null;
}
}
+ if (isSingleUse()) {
+ endUnitOfWorkSpan();
+ }
}
},
MoreExecutors.directExecutor());
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 da1ad2051c7..f84f379e79a 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
@@ -29,6 +29,7 @@
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.spanner.v1.SpannerGrpc;
+import io.opentelemetry.context.Scope;
import java.util.LinkedList;
import java.util.Objects;
import javax.annotation.Nonnull;
@@ -94,6 +95,11 @@ public String toString() {
super(builder);
}
+ @Override
+ boolean isSingleUse() {
+ return false;
+ }
+
@Override
public Type getType() {
return Type.TRANSACTION;
@@ -124,16 +130,18 @@ public ApiFuture executeQueryAsync(
final AnalyzeMode analyzeMode,
final QueryOption... options) {
Preconditions.checkArgument(statement.isQuery(), "Statement is not a query");
- checkOrCreateValidTransaction(statement, callType);
- return executeStatementAsync(
- callType,
- statement,
- () -> {
- checkAborted();
- return DirectExecuteResultSet.ofResultSet(
- internalExecuteQuery(statement, analyzeMode, options));
- },
- SpannerGrpc.getExecuteStreamingSqlMethod());
+ try (Scope ignore = span.makeCurrent()) {
+ checkOrCreateValidTransaction(statement, callType);
+ return executeStatementAsync(
+ callType,
+ statement,
+ () -> {
+ checkAborted();
+ return DirectExecuteResultSet.ofResultSet(
+ internalExecuteQuery(statement, analyzeMode, options));
+ },
+ SpannerGrpc.getExecuteStreamingSqlMethod());
+ }
}
ResultSet internalExecuteQuery(
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 70e789eb580..e4afc501b51 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
@@ -21,12 +21,14 @@
import com.google.api.core.ApiFuture;
import com.google.api.core.ApiFutures;
+import com.google.api.gax.core.GaxProperties;
import com.google.cloud.Timestamp;
import com.google.cloud.spanner.AsyncResultSet;
import com.google.cloud.spanner.BatchClient;
import com.google.cloud.spanner.BatchReadOnlyTransaction;
import com.google.cloud.spanner.CommitResponse;
import com.google.cloud.spanner.DatabaseClient;
+import com.google.cloud.spanner.DatabaseId;
import com.google.cloud.spanner.Dialect;
import com.google.cloud.spanner.ErrorCode;
import com.google.cloud.spanner.Mutation;
@@ -52,11 +54,17 @@
import com.google.cloud.spanner.connection.UnitOfWork.CallType;
import com.google.cloud.spanner.connection.UnitOfWork.UnitOfWorkState;
import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.spanner.v1.DirectedReadOptions;
import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions;
import com.google.spanner.v1.ResultSetStats;
+import io.opentelemetry.api.OpenTelemetry;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.common.AttributesBuilder;
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.api.trace.Tracer;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
@@ -66,6 +74,7 @@
import java.util.List;
import java.util.Set;
import java.util.Stack;
+import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadFactory;
@@ -77,6 +86,15 @@
/** Implementation for {@link Connection}, the generic Spanner connection API (not JDBC). */
class ConnectionImpl implements Connection {
+ private static final String INSTRUMENTATION_SCOPE = "cloud.google.com/java";
+ private static final String DEFAULT_TRACING_PREFIX = "CloudSpanner";
+ private static final String SINGLE_USE_TRANSACTION = "SingleUseTransaction";
+ private static final String READ_ONLY_TRANSACTION = "ReadOnlyTransaction";
+ private static final String READ_WRITE_TRANSACTION = "ReadWriteTransaction";
+ private static final String DML_BATCH = "DmlBatch";
+ private static final String DDL_BATCH = "DdlBatch";
+ private static final String DDL_STATEMENT = "DdlStatement";
+
private static final String CLOSED_ERROR_MSG = "This connection is closed";
private static final String ONLY_ALLOWED_IN_AUTOCOMMIT =
"This method may only be called while in autocommit mode";
@@ -187,6 +205,9 @@ static UnitOfWorkType of(TransactionMode transactionMode) {
private boolean closed = false;
private final Spanner spanner;
+ private final Tracer tracer;
+ private final String tracingPrefix;
+ private final Attributes openTelemetryAttributes;
private final DdlClient ddlClient;
private final DatabaseClient dbClient;
private final BatchClient batchClient;
@@ -261,6 +282,16 @@ static UnitOfWorkType of(TransactionMode transactionMode) {
this.spannerPool = SpannerPool.INSTANCE;
this.options = options;
this.spanner = spannerPool.getSpanner(options, this);
+ this.tracer =
+ spanner
+ .getOptions()
+ .getOpenTelemetry()
+ .getTracer(
+ INSTRUMENTATION_SCOPE,
+ GaxProperties.getLibraryVersion(spanner.getOptions().getClass()));
+ this.tracingPrefix =
+ MoreObjects.firstNonNull(options.getTracingPrefix(), DEFAULT_TRACING_PREFIX);
+ this.openTelemetryAttributes = createOpenTelemetryAttributes(options.getDatabaseId());
if (options.isAutoConfigEmulator()) {
EmulatorUtil.maybeCreateInstanceAndDatabase(
spanner, options.getDatabaseId(), options.getDialect());
@@ -300,6 +331,9 @@ static UnitOfWorkType of(TransactionMode transactionMode) {
this.options = Preconditions.checkNotNull(options);
this.ddlInTransactionMode = options.getDdlInTransactionMode();
this.spanner = spannerPool.getSpanner(options, this);
+ this.tracer = OpenTelemetry.noop().getTracer(INSTRUMENTATION_SCOPE);
+ this.tracingPrefix = DEFAULT_TRACING_PREFIX;
+ this.openTelemetryAttributes = Attributes.empty();
this.ddlClient = Preconditions.checkNotNull(ddlClient);
this.dbClient = Preconditions.checkNotNull(dbClient);
this.batchClient = Preconditions.checkNotNull(batchClient);
@@ -329,6 +363,20 @@ private AbstractStatementParser getStatementParser() {
return this.statementParser;
}
+ Attributes getOpenTelemetryAttributes() {
+ return this.openTelemetryAttributes;
+ }
+
+ @VisibleForTesting
+ static Attributes createOpenTelemetryAttributes(DatabaseId databaseId) {
+ AttributesBuilder attributesBuilder = Attributes.builder();
+ attributesBuilder.put("connection_id", UUID.randomUUID().toString());
+ attributesBuilder.put("database", databaseId.getDatabase());
+ attributesBuilder.put("instance_id", databaseId.getInstanceId().getInstance());
+ attributesBuilder.put("project_id", databaseId.getInstanceId().getProject());
+ return attributesBuilder.build();
+ }
+
@Override
public void close() {
try {
@@ -1687,11 +1735,19 @@ UnitOfWork getCurrentUnitOfWorkOrStartNewUnitOfWork(
/* isInternalMetadataQuery = */ false,
/* forceSingleUse = */ statementType == StatementType.DDL
&& this.ddlInTransactionMode != DdlInTransactionMode.FAIL
- && !this.transactionBeginMarked);
+ && !this.transactionBeginMarked,
+ statementType);
}
return this.currentUnitOfWork;
}
+ private Span createSpanForUnitOfWork(String name) {
+ return tracer
+ .spanBuilder(this.tracingPrefix + "." + name)
+ .setAllAttributes(getOpenTelemetryAttributes())
+ .startSpan();
+ }
+
void maybeAutoCommitCurrentTransaction(StatementType statementType) {
if (this.currentUnitOfWork instanceof ReadWriteTransaction
&& this.currentUnitOfWork.isActive()
@@ -1703,6 +1759,12 @@ void maybeAutoCommitCurrentTransaction(StatementType statementType) {
@VisibleForTesting
UnitOfWork createNewUnitOfWork(boolean isInternalMetadataQuery, boolean forceSingleUse) {
+ return createNewUnitOfWork(isInternalMetadataQuery, forceSingleUse, null);
+ }
+
+ @VisibleForTesting
+ UnitOfWork createNewUnitOfWork(
+ boolean isInternalMetadataQuery, boolean forceSingleUse, StatementType statementType) {
if (isInternalMetadataQuery
|| (isAutocommit() && !isInTransaction() && !isInBatch())
|| forceSingleUse) {
@@ -1718,6 +1780,9 @@ UnitOfWork createNewUnitOfWork(boolean isInternalMetadataQuery, boolean forceSin
.setMaxCommitDelay(maxCommitDelay)
.setStatementTimeout(statementTimeout)
.withStatementExecutor(statementExecutor)
+ .setSpan(
+ createSpanForUnitOfWork(
+ statementType == StatementType.DDL ? DDL_STATEMENT : SINGLE_USE_TRANSACTION))
.build();
} else {
switch (getUnitOfWorkType()) {
@@ -1730,6 +1795,7 @@ UnitOfWork createNewUnitOfWork(boolean isInternalMetadataQuery, boolean forceSin
.withStatementExecutor(statementExecutor)
.setTransactionTag(transactionTag)
.setRpcPriority(rpcPriority)
+ .setSpan(createSpanForUnitOfWork(READ_ONLY_TRANSACTION))
.build();
case READ_WRITE_TRANSACTION:
return ReadWriteTransaction.newBuilder()
@@ -1745,6 +1811,7 @@ UnitOfWork createNewUnitOfWork(boolean isInternalMetadataQuery, boolean forceSin
.withStatementExecutor(statementExecutor)
.setTransactionTag(transactionTag)
.setRpcPriority(rpcPriority)
+ .setSpan(createSpanForUnitOfWork(READ_WRITE_TRANSACTION))
.build();
case DML_BATCH:
// A DML batch can run inside the current transaction. It should therefore only
@@ -1756,6 +1823,7 @@ UnitOfWork createNewUnitOfWork(boolean isInternalMetadataQuery, boolean forceSin
.withStatementExecutor(statementExecutor)
.setStatementTag(statementTag)
.setRpcPriority(rpcPriority)
+ .setSpan(createSpanForUnitOfWork(DML_BATCH))
.build();
case DDL_BATCH:
return DdlBatch.newBuilder()
@@ -1763,6 +1831,7 @@ UnitOfWork createNewUnitOfWork(boolean isInternalMetadataQuery, boolean forceSin
.setDatabaseClient(dbClient)
.setStatementTimeout(statementTimeout)
.withStatementExecutor(statementExecutor)
+ .setSpan(createSpanForUnitOfWork(DDL_BATCH))
.build();
default:
}
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 59c30789afb..24b2f64bd85 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
@@ -100,9 +100,12 @@ private static ConnectionProperty createStringProperty(String name, String descr
}
private static ConnectionProperty createBooleanProperty(
- String name, String description, boolean defaultValue) {
+ String name, String description, Boolean defaultValue) {
return new ConnectionProperty(
- name, description, String.valueOf(defaultValue), BOOLEAN_VALUES);
+ name,
+ description,
+ defaultValue == null ? "" : String.valueOf(defaultValue),
+ BOOLEAN_VALUES);
}
private static ConnectionProperty createIntProperty(
@@ -195,6 +198,7 @@ public String[] getValidValues() {
private static final boolean DEFAULT_AUTO_PARTITION_MODE = false;
private static final int DEFAULT_MAX_PARTITIONS = 0;
private static final int DEFAULT_MAX_PARTITIONED_PARALLELISM = 1;
+ private static final Boolean DEFAULT_ENABLE_EXTENDED_TRACING = null;
private static final String PLAIN_TEXT_PROTOCOL = "http:";
private static final String HOST_PROTOCOL = "https:";
@@ -275,6 +279,8 @@ public String[] getValidValues() {
public static final String MAX_PARTITIONED_PARALLELISM_PROPERTY_NAME =
"maxPartitionedParallelism";
+ public static final String ENABLE_EXTENDED_TRACING_PROPERTY_NAME = "enableExtendedTracing";
+
private static final String GUARDED_CONNECTION_PROPERTY_ERROR_MESSAGE =
"%s can only be used if the system property %s has been set to true. "
+ "Start the application with the JVM command line option -D%s=true";
@@ -436,7 +442,13 @@ private static String generateGuardedConnectionPropertyError(
"The maximum number of partitions that will be executed in parallel "
+ "for partitioned queries on this connection. Set this value to 0 to "
+ "dynamically use the number of processors available in the runtime.",
- DEFAULT_MAX_PARTITIONED_PARALLELISM))));
+ DEFAULT_MAX_PARTITIONED_PARALLELISM),
+ ConnectionProperty.createBooleanProperty(
+ ENABLE_EXTENDED_TRACING_PROPERTY_NAME,
+ "Include the SQL string in the OpenTelemetry traces that are generated "
+ + "by this connection. The SQL string is added as the standard OpenTelemetry "
+ + "attribute 'db.statement'.",
+ DEFAULT_ENABLE_EXTENDED_TRACING))));
private static final Set INTERNAL_PROPERTIES =
Collections.unmodifiableSet(
@@ -508,6 +520,7 @@ public static class Builder {
Collections.emptyList();
private SpannerOptionsConfigurator configurator;
private OpenTelemetry openTelemetry;
+ private String tracingPrefix;
private Builder() {}
@@ -662,6 +675,11 @@ public Builder setOpenTelemetry(OpenTelemetry openTelemetry) {
return this;
}
+ public Builder setTracingPrefix(String tracingPrefix) {
+ this.tracingPrefix = tracingPrefix;
+ return this;
+ }
+
/** @return the {@link ConnectionOptions} */
public ConnectionOptions build() {
Preconditions.checkState(this.uri != null, "Connection URI is required");
@@ -723,6 +741,8 @@ public static Builder newBuilder() {
private final boolean useVirtualThreads;
private final boolean useVirtualGrpcTransportThreads;
private final OpenTelemetry openTelemetry;
+ private final String tracingPrefix;
+ private final Boolean enableExtendedTracing;
private final List statementExecutionInterceptors;
private final SpannerOptionsConfigurator configurator;
@@ -829,6 +849,8 @@ private ConnectionOptions(Builder builder) {
this.useVirtualThreads = parseUseVirtualThreads(this.uri);
this.useVirtualGrpcTransportThreads = parseUseVirtualGrpcTransportThreads(this.uri);
this.openTelemetry = builder.openTelemetry;
+ this.tracingPrefix = builder.tracingPrefix;
+ this.enableExtendedTracing = parseEnableExtendedTracing(this.uri);
this.statementExecutionInterceptors =
Collections.unmodifiableList(builder.statementExecutionInterceptors);
this.configurator = builder.configurator;
@@ -908,6 +930,15 @@ OpenTelemetry getOpenTelemetry() {
return this.openTelemetry;
}
+ /**
+ * @return The prefix that will be added to all traces that are started by the Connection API.
+ * This property is used by for example the JDBC driver to make sure all traces start with
+ * CloudSpannerJdbc.
+ */
+ String getTracingPrefix() {
+ return this.tracingPrefix;
+ }
+
SpannerOptionsConfigurator getConfigurator() {
return configurator;
}
@@ -1214,6 +1245,12 @@ static DdlInTransactionMode parseDdlInTransactionMode(String uri) {
: DEFAULT_DDL_IN_TRANSACTION_MODE;
}
+ @VisibleForTesting
+ static Boolean parseEnableExtendedTracing(String uri) {
+ String value = parseUriProperty(uri, ENABLE_EXTENDED_TRACING_PROPERTY_NAME);
+ return value != null ? Boolean.valueOf(value) : DEFAULT_ENABLE_EXTENDED_TRACING;
+ }
+
@VisibleForTesting
static String parseUriProperty(String uri, String property) {
Pattern pattern = Pattern.compile(String.format("(?is)(?:;|\\?)%s=(.*?)(?:;|$)", property));
@@ -1517,6 +1554,10 @@ int getMaxPartitionedParallelism() {
return this.maxPartitionedParallelism;
}
+ Boolean isEnableExtendedTracing() {
+ return this.enableExtendedTracing;
+ }
+
/** Interceptors that should be executed after each statement */
List getStatementExecutionInterceptors() {
return statementExecutionInterceptors;
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DdlBatch.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DdlBatch.java
index 55b780c5718..ca5da153f6a 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DdlBatch.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DdlBatch.java
@@ -35,8 +35,12 @@
import com.google.cloud.spanner.connection.AbstractStatementParser.StatementType;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
+import com.google.common.util.concurrent.MoreExecutors;
import com.google.spanner.admin.database.v1.DatabaseAdminGrpc;
import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.trace.StatusCode;
+import io.opentelemetry.context.Scope;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -92,6 +96,11 @@ private DdlBatch(Builder builder) {
this.dbClient = builder.dbClient;
}
+ @Override
+ boolean isSingleUse() {
+ return false;
+ }
+
@Override
public Type getType() {
return Type.BATCH;
@@ -204,36 +213,51 @@ public ApiFuture writeAsync(CallType callType, Iterable mutation
public ApiFuture runBatchAsync(CallType callType) {
ConnectionPreconditions.checkState(
state == UnitOfWorkState.STARTED, "The batch is no longer active and cannot be ran");
- if (statements.isEmpty()) {
- this.state = UnitOfWorkState.RAN;
- return ApiFutures.immediateFuture(new long[0]);
- }
- // create a statement that can be passed in to the execute method
- Callable callable =
- () -> {
- try {
- OperationFuture operation =
- ddlClient.executeDdl(statements);
+ try (Scope ignore = span.makeCurrent()) {
+ if (statements.isEmpty()) {
+ this.state = UnitOfWorkState.RAN;
+ return ApiFutures.transform(
+ asyncEndUnitOfWorkSpan(), unused -> new long[0], MoreExecutors.directExecutor());
+ }
+ // Set the DDL statements on the span.
+
+ span.setAllAttributes(Attributes.of(DB_STATEMENT_ARRAY_KEY, statements));
+ // create a statement that can be passed in to the execute method
+ Callable callable =
+ () -> {
try {
- // Wait until the operation has finished.
- getWithStatementTimeout(operation, RUN_BATCH_STATEMENT);
- long[] updateCounts = new long[statements.size()];
- Arrays.fill(updateCounts, 1L);
- state = UnitOfWorkState.RAN;
- return updateCounts;
- } catch (SpannerException e) {
- long[] updateCounts = extractUpdateCounts(operation);
- throw SpannerExceptionFactory.newSpannerBatchUpdateException(
- e.getErrorCode(), e.getMessage(), updateCounts);
+ OperationFuture operation =
+ ddlClient.executeDdl(statements);
+ try {
+ // Wait until the operation has finished.
+ getWithStatementTimeout(operation, RUN_BATCH_STATEMENT);
+ long[] updateCounts = new long[statements.size()];
+ Arrays.fill(updateCounts, 1L);
+ state = UnitOfWorkState.RAN;
+ return updateCounts;
+ } catch (SpannerException e) {
+ long[] updateCounts = extractUpdateCounts(operation);
+ throw SpannerExceptionFactory.newSpannerBatchUpdateException(
+ e.getErrorCode(), e.getMessage(), updateCounts);
+ }
+ } catch (Throwable t) {
+ span.setStatus(StatusCode.ERROR);
+ span.recordException(t);
+ state = UnitOfWorkState.RUN_FAILED;
+ throw t;
}
- } catch (Throwable t) {
- state = UnitOfWorkState.RUN_FAILED;
- throw t;
- }
- };
- this.state = UnitOfWorkState.RUNNING;
- return executeStatementAsync(
- callType, RUN_BATCH_STATEMENT, callable, DatabaseAdminGrpc.getUpdateDatabaseDdlMethod());
+ };
+ this.state = UnitOfWorkState.RUNNING;
+ ApiFuture result =
+ executeStatementAsync(
+ callType,
+ RUN_BATCH_STATEMENT,
+ callable,
+ DatabaseAdminGrpc.getUpdateDatabaseDdlMethod());
+ asyncEndUnitOfWorkSpan();
+
+ return result;
+ }
}
long[] extractUpdateCounts(OperationFuture operation) {
@@ -261,6 +285,7 @@ long[] extractUpdateCounts(UpdateDatabaseDdlMetadata metadata) {
public void abortBatch() {
ConnectionPreconditions.checkState(
state == UnitOfWorkState.STARTED, "The batch is no longer active and cannot be aborted.");
+ asyncEndUnitOfWorkSpan();
this.state = UnitOfWorkState.ABORTED;
}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DmlBatch.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DmlBatch.java
index d537a8ee997..1551a186e7f 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DmlBatch.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/DmlBatch.java
@@ -33,6 +33,7 @@
import com.google.cloud.spanner.connection.AbstractStatementParser.StatementType;
import com.google.common.base.Preconditions;
import com.google.common.util.concurrent.MoreExecutors;
+import io.opentelemetry.context.Scope;
import java.util.ArrayList;
import java.util.List;
@@ -81,6 +82,11 @@ private DmlBatch(Builder builder) {
this.statementTag = builder.statementTag;
}
+ @Override
+ boolean isSingleUse() {
+ return false;
+ }
+
@Override
public Type getType() {
return Type.BATCH;
@@ -189,57 +195,61 @@ public ApiFuture writeAsync(CallType callType, Iterable mutation
public ApiFuture runBatchAsync(CallType callType) {
ConnectionPreconditions.checkState(
state == UnitOfWorkState.STARTED, "The batch is no longer active and cannot be ran");
- if (statements.isEmpty()) {
- this.state = UnitOfWorkState.RAN;
- return ApiFutures.immediateFuture(new long[0]);
- }
- this.state = UnitOfWorkState.RUNNING;
- // Use a SettableApiFuture to return the result, instead of directly returning the future that
- // is returned by the executeBatchUpdateAsync method. This is needed because the state of the
- // batch is set after the update has finished, and this happens in a listener. A listener is
- // executed AFTER a Future is done, which means that a user could read the state of the Batch
- // before it has been changed.
- final SettableApiFuture res = SettableApiFuture.create();
- int numOptions = 0;
- if (statementTag != null) {
- numOptions++;
- }
- if (this.rpcPriority != null) {
- numOptions++;
- }
- UpdateOption[] options = new UpdateOption[numOptions];
- int index = 0;
- if (statementTag != null) {
- options[index++] = Options.tag(statementTag);
- }
- if (this.rpcPriority != null) {
- options[index++] = Options.priority(this.rpcPriority);
+ try (Scope ignore = span.makeCurrent()) {
+ if (statements.isEmpty()) {
+ this.state = UnitOfWorkState.RAN;
+ return ApiFutures.immediateFuture(new long[0]);
+ }
+ this.state = UnitOfWorkState.RUNNING;
+ // Use a SettableApiFuture to return the result, instead of directly returning the future that
+ // is returned by the executeBatchUpdateAsync method. This is needed because the state of the
+ // batch is set after the update has finished, and this happens in a listener. A listener is
+ // executed AFTER a Future is done, which means that a user could read the state of the Batch
+ // before it has been changed.
+ final SettableApiFuture res = SettableApiFuture.create();
+ int numOptions = 0;
+ if (statementTag != null) {
+ numOptions++;
+ }
+ if (this.rpcPriority != null) {
+ numOptions++;
+ }
+ UpdateOption[] options = new UpdateOption[numOptions];
+ int index = 0;
+ if (statementTag != null) {
+ options[index++] = Options.tag(statementTag);
+ }
+ if (this.rpcPriority != null) {
+ options[index++] = Options.priority(this.rpcPriority);
+ }
+ ApiFuture updateCounts =
+ transaction.executeBatchUpdateAsync(callType, statements, options);
+ ApiFutures.addCallback(
+ updateCounts,
+ new ApiFutureCallback() {
+ @Override
+ public void onFailure(Throwable t) {
+ state = UnitOfWorkState.RUN_FAILED;
+ res.setException(t);
+ }
+
+ @Override
+ public void onSuccess(long[] result) {
+ state = UnitOfWorkState.RAN;
+ res.set(result);
+ }
+ },
+ MoreExecutors.directExecutor());
+ asyncEndUnitOfWorkSpan();
+ return res;
}
- ApiFuture updateCounts =
- transaction.executeBatchUpdateAsync(callType, statements, options);
- ApiFutures.addCallback(
- updateCounts,
- new ApiFutureCallback() {
- @Override
- public void onFailure(Throwable t) {
- state = UnitOfWorkState.RUN_FAILED;
- res.setException(t);
- }
-
- @Override
- public void onSuccess(long[] result) {
- state = UnitOfWorkState.RAN;
- res.set(result);
- }
- },
- MoreExecutors.directExecutor());
- return res;
}
@Override
public void abortBatch() {
ConnectionPreconditions.checkState(
state == UnitOfWorkState.STARTED, "The batch is no longer active and cannot be aborted.");
+ asyncEndUnitOfWorkSpan();
this.state = UnitOfWorkState.ABORTED;
}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReadOnlyTransaction.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReadOnlyTransaction.java
index 63e5221362e..ea4a11a4cd1 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReadOnlyTransaction.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ReadOnlyTransaction.java
@@ -17,7 +17,6 @@
package com.google.cloud.spanner.connection;
import com.google.api.core.ApiFuture;
-import com.google.api.core.ApiFutures;
import com.google.cloud.Timestamp;
import com.google.cloud.spanner.BatchClient;
import com.google.cloud.spanner.BatchReadOnlyTransaction;
@@ -38,6 +37,7 @@
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.spanner.v1.SpannerGrpc;
+import io.opentelemetry.context.Scope;
import java.util.concurrent.Callable;
/**
@@ -186,21 +186,23 @@ public ApiFuture partitionQueryAsync(
// statement in the transaction is to partition a query.
// Using a batch-read-only transaction for every read-only transaction is not efficient, as
// these transactions use a session that is created synchronously only for this transaction.
- if (transaction == null) {
- batchReadOnlyTransaction = batchClient.batchReadOnlyTransaction(readOnlyStaleness);
- transaction = batchReadOnlyTransaction;
- } else if (batchReadOnlyTransaction == null) {
- batchReadOnlyTransaction =
- batchClient.batchReadOnlyTransaction(
- TimestampBound.ofReadTimestamp(transaction.getReadTimestamp()));
+ try (Scope ignore = span.makeCurrent()) {
+ if (transaction == null) {
+ batchReadOnlyTransaction = batchClient.batchReadOnlyTransaction(readOnlyStaleness);
+ transaction = batchReadOnlyTransaction;
+ } else if (batchReadOnlyTransaction == null) {
+ batchReadOnlyTransaction =
+ batchClient.batchReadOnlyTransaction(
+ TimestampBound.ofReadTimestamp(transaction.getReadTimestamp()));
+ }
+ Callable callable =
+ () -> partitionQuery(batchReadOnlyTransaction, partitionOptions, query, options);
+ return executeStatementAsync(
+ callType,
+ query,
+ callable,
+ ImmutableList.of(SpannerGrpc.getExecuteSqlMethod(), SpannerGrpc.getCommitMethod()));
}
- Callable callable =
- () -> partitionQuery(batchReadOnlyTransaction, partitionOptions, query, options);
- return executeStatementAsync(
- callType,
- query,
- callable,
- ImmutableList.of(SpannerGrpc.getExecuteSqlMethod(), SpannerGrpc.getCommitMethod()));
}
@Override
@@ -240,25 +242,30 @@ public ApiFuture writeAsync(CallType callType, Iterable mutation
@Override
public ApiFuture commitAsync(CallType callType) {
- closeTransactions();
- this.state = UnitOfWorkState.COMMITTED;
- return ApiFutures.immediateFuture(null);
+ try (Scope ignore = span.makeCurrent()) {
+ ApiFuture result = closeTransactions();
+ this.state = UnitOfWorkState.COMMITTED;
+ return result;
+ }
}
@Override
public ApiFuture rollbackAsync(CallType callType) {
- closeTransactions();
- this.state = UnitOfWorkState.ROLLED_BACK;
- return ApiFutures.immediateFuture(null);
+ try (Scope ignore = span.makeCurrent()) {
+ ApiFuture result = closeTransactions();
+ this.state = UnitOfWorkState.ROLLED_BACK;
+ return result;
+ }
}
- private void closeTransactions() {
+ private ApiFuture closeTransactions() {
if (this.transaction != null) {
this.transaction.close();
}
if (this.batchReadOnlyTransaction != null) {
this.batchReadOnlyTransaction.close();
}
+ return asyncEndUnitOfWorkSpan();
}
@Override
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 86d6feff90e..19c91ac1945 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
@@ -57,6 +57,7 @@
import com.google.common.collect.Iterables;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.spanner.v1.SpannerGrpc;
+import io.opentelemetry.context.Scope;
import java.time.Duration;
import java.util.ArrayList;
import java.util.LinkedList;
@@ -484,76 +485,82 @@ public ApiFuture executeQueryAsync(
(statement.getType() == StatementType.QUERY)
|| (statement.getType() == StatementType.UPDATE && statement.hasReturningClause()),
"Statement must be a query or DML with returning clause");
- checkOrCreateValidTransaction(statement, callType);
-
- ApiFuture res;
- if (retryAbortsInternally && txContextFuture != null) {
- res =
- executeStatementAsync(
- callType,
- statement,
- () -> {
- checkTimedOut();
- return runWithRetry(
- () -> {
- try {
- getStatementExecutor()
- .invokeInterceptors(
- statement,
- StatementExecutionStep.EXECUTE_STATEMENT,
- ReadWriteTransaction.this);
- DirectExecuteResultSet delegate =
- DirectExecuteResultSet.ofResultSet(
- internalExecuteQuery(statement, analyzeMode, options));
- return createAndAddRetryResultSet(
- delegate, statement, analyzeMode, options);
- } catch (AbortedException e) {
- throw e;
- } catch (SpannerException e) {
- createAndAddFailedQuery(e, statement, analyzeMode, options);
- throw e;
- }
- });
- },
- // ignore interceptors here as they are invoked in the Callable.
- InterceptorsUsage.IGNORE_INTERCEPTORS,
- ImmutableList.of(SpannerGrpc.getExecuteStreamingSqlMethod()));
- } else {
- res = super.executeQueryAsync(callType, statement, analyzeMode, options);
- }
- ApiFutures.addCallback(
- res,
- new ApiFutureCallback() {
- @Override
- public void onFailure(Throwable t) {
- if (t instanceof SpannerException) {
- handlePossibleInvalidatingException((SpannerException) t);
+ try (Scope ignore = span.makeCurrent()) {
+ checkOrCreateValidTransaction(statement, callType);
+
+ ApiFuture res;
+ if (retryAbortsInternally && txContextFuture != null) {
+ res =
+ executeStatementAsync(
+ callType,
+ statement,
+ () -> {
+ checkTimedOut();
+ return runWithRetry(
+ () -> {
+ try {
+ getStatementExecutor()
+ .invokeInterceptors(
+ statement,
+ StatementExecutionStep.EXECUTE_STATEMENT,
+ ReadWriteTransaction.this);
+ DirectExecuteResultSet delegate =
+ DirectExecuteResultSet.ofResultSet(
+ internalExecuteQuery(statement, analyzeMode, options));
+ return createAndAddRetryResultSet(
+ delegate, statement, analyzeMode, options);
+ } catch (AbortedException e) {
+ throw e;
+ } catch (SpannerException e) {
+ createAndAddFailedQuery(e, statement, analyzeMode, options);
+ throw e;
+ }
+ });
+ },
+ // ignore interceptors here as they are invoked in the Callable.
+ InterceptorsUsage.IGNORE_INTERCEPTORS,
+ ImmutableList.of(SpannerGrpc.getExecuteStreamingSqlMethod()));
+ } else {
+ res = super.executeQueryAsync(callType, statement, analyzeMode, options);
+ }
+ ApiFutures.addCallback(
+ res,
+ new ApiFutureCallback() {
+ @Override
+ public void onFailure(Throwable t) {
+ if (t instanceof SpannerException) {
+ handlePossibleInvalidatingException((SpannerException) t);
+ }
}
- }
- @Override
- public void onSuccess(ResultSet result) {}
- },
- MoreExecutors.directExecutor());
- return res;
+ @Override
+ public void onSuccess(ResultSet result) {}
+ },
+ MoreExecutors.directExecutor());
+ return res;
+ }
}
@Override
public ApiFuture analyzeUpdateAsync(
CallType callType, ParsedStatement update, AnalyzeMode analyzeMode, UpdateOption... options) {
- return ApiFutures.transform(
- internalExecuteUpdateAsync(callType, update, analyzeMode, options),
- Tuple::y,
- MoreExecutors.directExecutor());
+ try (Scope ignore = span.makeCurrent()) {
+ return ApiFutures.transform(
+ internalExecuteUpdateAsync(callType, update, analyzeMode, options),
+ Tuple::y,
+ MoreExecutors.directExecutor());
+ }
}
@Override
public ApiFuture executeUpdateAsync(
CallType callType, final ParsedStatement update, final UpdateOption... options) {
- return ApiFutures.transform(
- internalExecuteUpdateAsync(callType, update, AnalyzeMode.NONE, options),
- Tuple::x,
- MoreExecutors.directExecutor());
+ try (Scope ignore = span.makeCurrent()) {
+ return ApiFutures.transform(
+ internalExecuteUpdateAsync(callType, update, AnalyzeMode.NONE, options),
+ Tuple::x,
+ MoreExecutors.directExecutor());
+ }
}
/**
@@ -661,86 +668,91 @@ public void onSuccess(Tuple result) {}
public ApiFuture executeBatchUpdateAsync(
CallType callType, Iterable updates, final UpdateOption... options) {
Preconditions.checkNotNull(updates);
- final List updateStatements = new LinkedList<>();
- for (ParsedStatement update : updates) {
- Preconditions.checkArgument(
- update.isUpdate(),
- "Statement is not an update statement: " + update.getSqlWithoutComments());
- updateStatements.add(update.getStatement());
- }
- checkOrCreateValidTransaction(Iterables.getFirst(updates, null), callType);
-
- ApiFuture res;
- if (retryAbortsInternally) {
- res =
- executeStatementAsync(
- callType,
- RUN_BATCH_STATEMENT,
- () -> {
- checkTimedOut();
- return runWithRetry(
- () -> {
- try {
- getStatementExecutor()
- .invokeInterceptors(
- RUN_BATCH_STATEMENT,
- StatementExecutionStep.EXECUTE_STATEMENT,
- ReadWriteTransaction.this);
- long[] updateCounts =
- get(txContextFuture).batchUpdate(updateStatements, options);
- createAndAddRetriableBatchUpdate(updateStatements, updateCounts, options);
- return updateCounts;
- } catch (AbortedException e) {
- throw e;
- } catch (SpannerException e) {
- createAndAddFailedBatchUpdate(e, updateStatements);
- throw e;
- }
- });
- },
- // ignore interceptors here as they are invoked in the Callable.
- InterceptorsUsage.IGNORE_INTERCEPTORS,
- ImmutableList.of(SpannerGrpc.getExecuteBatchDmlMethod()));
- } else {
- res =
- executeStatementAsync(
- callType,
- RUN_BATCH_STATEMENT,
- () -> {
- checkTimedOut();
- checkAborted();
- return get(txContextFuture).batchUpdate(updateStatements);
- },
- SpannerGrpc.getExecuteBatchDmlMethod());
- }
- ApiFutures.addCallback(
- res,
- new ApiFutureCallback() {
- @Override
- public void onFailure(Throwable t) {
- if (t instanceof SpannerException) {
- handlePossibleInvalidatingException((SpannerException) t);
+ try (Scope ignore = span.makeCurrent()) {
+ final List updateStatements = new LinkedList<>();
+ for (ParsedStatement update : updates) {
+ Preconditions.checkArgument(
+ update.isUpdate(),
+ "Statement is not an update statement: " + update.getSqlWithoutComments());
+ updateStatements.add(update.getStatement());
+ }
+ checkOrCreateValidTransaction(Iterables.getFirst(updates, null), callType);
+
+ ApiFuture res;
+ if (retryAbortsInternally) {
+ res =
+ executeStatementAsync(
+ callType,
+ RUN_BATCH_STATEMENT,
+ () -> {
+ checkTimedOut();
+ return runWithRetry(
+ () -> {
+ try {
+ getStatementExecutor()
+ .invokeInterceptors(
+ RUN_BATCH_STATEMENT,
+ StatementExecutionStep.EXECUTE_STATEMENT,
+ ReadWriteTransaction.this);
+ long[] updateCounts =
+ get(txContextFuture).batchUpdate(updateStatements, options);
+ createAndAddRetriableBatchUpdate(updateStatements, updateCounts, options);
+ return updateCounts;
+ } catch (AbortedException e) {
+ throw e;
+ } catch (SpannerException e) {
+ createAndAddFailedBatchUpdate(e, updateStatements);
+ throw e;
+ }
+ });
+ },
+ // ignore interceptors here as they are invoked in the Callable.
+ InterceptorsUsage.IGNORE_INTERCEPTORS,
+ ImmutableList.of(SpannerGrpc.getExecuteBatchDmlMethod()));
+ } else {
+ res =
+ executeStatementAsync(
+ callType,
+ RUN_BATCH_STATEMENT,
+ () -> {
+ checkTimedOut();
+ checkAborted();
+ return get(txContextFuture).batchUpdate(updateStatements);
+ },
+ SpannerGrpc.getExecuteBatchDmlMethod());
+ }
+ ApiFutures.addCallback(
+ res,
+ new ApiFutureCallback() {
+ @Override
+ public void onFailure(Throwable t) {
+ if (t instanceof SpannerException) {
+ handlePossibleInvalidatingException((SpannerException) t);
+ }
}
- }
- @Override
- public void onSuccess(long[] result) {}
- },
- MoreExecutors.directExecutor());
- return res;
+ @Override
+ public void onSuccess(long[] result) {}
+ },
+ MoreExecutors.directExecutor());
+ return res;
+ }
}
@Override
public ApiFuture writeAsync(CallType callType, Iterable mutations) {
- Preconditions.checkNotNull(mutations);
- // We actually don't need an underlying transaction yet, as mutations are buffered until commit.
- // But we do need to verify that this transaction is valid, and to mark the start of the
- // transaction.
- checkValidStateAndMarkStarted();
- for (Mutation mutation : mutations) {
- this.mutations.add(checkNotNull(mutation));
+ try (Scope ignore = span.makeCurrent()) {
+ Preconditions.checkNotNull(mutations);
+ // We actually don't need an underlying transaction yet, as mutations are buffered until
+ // commit.
+ // But we do need to verify that this transaction is valid, and to mark the start of the
+ // transaction.
+ checkValidStateAndMarkStarted();
+ for (Mutation mutation : mutations) {
+ this.mutations.add(checkNotNull(mutation));
+ }
+ return ApiFutures.immediateFuture(null);
}
- return ApiFutures.immediateFuture(null);
}
private final Callable commitCallable =
@@ -758,73 +770,77 @@ public Void call() {
@Override
public ApiFuture commitAsync(CallType callType) {
- checkOrCreateValidTransaction(COMMIT_STATEMENT, callType);
- state = UnitOfWorkState.COMMITTING;
- commitResponseFuture = SettableApiFuture.create();
- ApiFuture res;
- // Check if this transaction actually needs to commit anything.
- if (txContextFuture == null) {
- // No actual transaction was started by this read/write transaction, which also means that we
- // don't have to commit anything.
- commitResponseFuture.set(
- new CommitResponse(
- Timestamp.fromProto(com.google.protobuf.Timestamp.getDefaultInstance())));
- state = UnitOfWorkState.COMMITTED;
- res = SettableApiFuture.create();
- ((SettableApiFuture) res).set(null);
- } else if (retryAbortsInternally) {
- res =
- executeStatementAsync(
- callType,
- COMMIT_STATEMENT,
- () -> {
- checkTimedOut();
- try {
- return runWithRetry(
- () -> {
- getStatementExecutor()
- .invokeInterceptors(
- COMMIT_STATEMENT,
- StatementExecutionStep.EXECUTE_STATEMENT,
- ReadWriteTransaction.this);
- return commitCallable.call();
- });
- } catch (Throwable t) {
- commitResponseFuture.setException(t);
- state = UnitOfWorkState.COMMIT_FAILED;
+ try (Scope ignore = span.makeCurrent()) {
+ checkOrCreateValidTransaction(COMMIT_STATEMENT, callType);
+ state = UnitOfWorkState.COMMITTING;
+ commitResponseFuture = SettableApiFuture.create();
+ ApiFuture res;
+ // Check if this transaction actually needs to commit anything.
+ if (txContextFuture == null) {
+ // No actual transaction was started by this read/write transaction, which also means that
+ // we
+ // don't have to commit anything.
+ commitResponseFuture.set(
+ new CommitResponse(
+ Timestamp.fromProto(com.google.protobuf.Timestamp.getDefaultInstance())));
+ state = UnitOfWorkState.COMMITTED;
+ res = SettableApiFuture.create();
+ ((SettableApiFuture) res).set(null);
+ } else if (retryAbortsInternally) {
+ res =
+ executeStatementAsync(
+ callType,
+ COMMIT_STATEMENT,
+ () -> {
+ checkTimedOut();
try {
- txManager.close();
- } catch (Throwable t2) {
- // Ignore.
+ return runWithRetry(
+ () -> {
+ getStatementExecutor()
+ .invokeInterceptors(
+ COMMIT_STATEMENT,
+ StatementExecutionStep.EXECUTE_STATEMENT,
+ ReadWriteTransaction.this);
+ return commitCallable.call();
+ });
+ } catch (Throwable t) {
+ commitResponseFuture.setException(t);
+ state = UnitOfWorkState.COMMIT_FAILED;
+ try {
+ txManager.close();
+ } catch (Throwable t2) {
+ // Ignore.
+ }
+ throw t;
}
- throw t;
- }
- },
- InterceptorsUsage.IGNORE_INTERCEPTORS,
- ImmutableList.of(SpannerGrpc.getCommitMethod()));
- } else {
- res =
- executeStatementAsync(
- callType,
- COMMIT_STATEMENT,
- () -> {
- checkTimedOut();
- try {
- return commitCallable.call();
- } catch (Throwable t) {
- commitResponseFuture.setException(t);
- state = UnitOfWorkState.COMMIT_FAILED;
+ },
+ InterceptorsUsage.IGNORE_INTERCEPTORS,
+ ImmutableList.of(SpannerGrpc.getCommitMethod()));
+ } else {
+ res =
+ executeStatementAsync(
+ callType,
+ COMMIT_STATEMENT,
+ () -> {
+ checkTimedOut();
try {
- txManager.close();
- } catch (Throwable t2) {
- // Ignore.
+ return commitCallable.call();
+ } catch (Throwable t) {
+ commitResponseFuture.setException(t);
+ state = UnitOfWorkState.COMMIT_FAILED;
+ try {
+ txManager.close();
+ } catch (Throwable t2) {
+ // Ignore.
+ }
+ throw t;
}
- throw t;
- }
- },
- SpannerGrpc.getCommitMethod());
+ },
+ SpannerGrpc.getCommitMethod());
+ }
+ asyncEndUnitOfWorkSpan();
+ return res;
}
- return res;
}
/**
@@ -959,12 +975,15 @@ private void addRetryStatement(RetriableStatement statement) {
private void handleAborted(AbortedException aborted) {
if (transactionRetryAttempts >= maxInternalRetries) {
// If the same statement in transaction keeps aborting, then we need to abort here.
+ span.addEvent("Internal retry attempts exceeded");
throwAbortWithRetryAttemptsExceeded();
} else if (retryAbortsInternally) {
logger.fine(toString() + ": Starting internal transaction retry");
while (true) {
// First back off and then restart the transaction.
long delay = aborted.getRetryDelayInMillis();
+ span.addEvent(
+ "Transaction aborted. Backing off for " + delay + " milliseconds and retrying.");
try {
if (delay > 0L) {
//noinspection BusyWait
@@ -1108,7 +1127,9 @@ public Void call() {
@Override
public ApiFuture rollbackAsync(CallType callType) {
- return rollbackAsync(callType, true);
+ try (Scope ignore = span.makeCurrent()) {
+ return rollbackAsync(callType, true);
+ }
}
private ApiFuture rollbackAsync(CallType callType, boolean updateStatus) {
@@ -1117,12 +1138,16 @@ private ApiFuture rollbackAsync(CallType callType, boolean updateStatus) {
"This transaction has status " + state.name());
if (updateStatus) {
state = UnitOfWorkState.ROLLED_BACK;
+ asyncEndUnitOfWorkSpan();
}
if (txContextFuture != null && state != UnitOfWorkState.ABORTED) {
- return executeStatementAsync(
- callType, ROLLBACK_STATEMENT, rollbackCallable, SpannerGrpc.getRollbackMethod());
+ ApiFuture result =
+ executeStatementAsync(
+ callType, ROLLBACK_STATEMENT, rollbackCallable, SpannerGrpc.getRollbackMethod());
+ asyncEndUnitOfWorkSpan();
+ return result;
} else {
- return ApiFutures.immediateFuture(null);
+ return asyncEndUnitOfWorkSpan();
}
}
@@ -1168,18 +1193,20 @@ private Savepoint createAutoSavepoint() {
@Override
void rollbackToSavepoint(Savepoint savepoint) {
- get(rollbackAsync(CallType.SYNC, false));
- // Mark the state of the transaction as rolled back to a savepoint. This will ensure that the
- // transaction will retry the next time a statement is actually executed.
- this.rolledBackToSavepointException =
- (AbortedException)
- SpannerExceptionFactory.newSpannerException(
- ErrorCode.ABORTED,
- "Transaction has been rolled back to a savepoint",
- 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();
+ try (Scope ignore = span.makeCurrent()) {
+ get(rollbackAsync(CallType.SYNC, false));
+ // Mark the state of the transaction as rolled back to a savepoint. This will ensure that the
+ // transaction will retry the next time a statement is actually executed.
+ this.rolledBackToSavepointException =
+ (AbortedException)
+ SpannerExceptionFactory.newSpannerException(
+ ErrorCode.ABORTED,
+ "Transaction has been rolled back to a savepoint",
+ 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/SingleUseTransaction.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SingleUseTransaction.java
index 164c3ae7ada..7dfa46a5977 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SingleUseTransaction.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SingleUseTransaction.java
@@ -53,6 +53,7 @@
import com.google.common.util.concurrent.MoreExecutors;
import com.google.spanner.admin.database.v1.DatabaseAdminGrpc;
import com.google.spanner.v1.SpannerGrpc;
+import io.opentelemetry.context.Scope;
import java.time.Duration;
import java.util.concurrent.Callable;
@@ -177,6 +178,11 @@ private SingleUseTransaction(Builder builder) {
this.internalMetdataQuery = builder.internalMetadataQuery;
}
+ @Override
+ boolean isSingleUse() {
+ return true;
+ }
+
@Override
public Type getType() {
return Type.TRANSACTION;
@@ -220,48 +226,50 @@ public ApiFuture executeQueryAsync(
|| (statement.isUpdate()
&& (analyzeMode != AnalyzeMode.NONE || statement.hasReturningClause())),
"The statement must be a query, or the statement must be DML and AnalyzeMode must be PLAN or PROFILE");
- checkAndMarkUsed();
-
- if (statement.isUpdate()) {
- if (analyzeMode != AnalyzeMode.NONE) {
- return analyzeTransactionalUpdateAsync(callType, statement, analyzeMode);
+ try (Scope ignore = span.makeCurrent()) {
+ checkAndMarkUsed();
+
+ if (statement.isUpdate()) {
+ if (analyzeMode != AnalyzeMode.NONE) {
+ return analyzeTransactionalUpdateAsync(callType, statement, analyzeMode);
+ }
+ // DML with returning clause.
+ return executeDmlReturningAsync(callType, statement, options);
}
- // DML with returning clause.
- return executeDmlReturningAsync(callType, statement, options);
- }
- // Do not use a read-only staleness for internal metadata queries.
- final ReadOnlyTransaction currentTransaction =
- internalMetdataQuery
- ? dbClient.singleUseReadOnlyTransaction()
- : dbClient.singleUseReadOnlyTransaction(readOnlyStaleness);
- Callable callable =
- () -> {
- try {
- ResultSet rs;
- if (analyzeMode == AnalyzeMode.NONE) {
- rs = currentTransaction.executeQuery(statement.getStatement(), options);
- } else {
- rs =
- currentTransaction.analyzeQuery(
- statement.getStatement(), analyzeMode.getQueryAnalyzeMode());
+ // Do not use a read-only staleness for internal metadata queries.
+ final ReadOnlyTransaction currentTransaction =
+ internalMetdataQuery
+ ? dbClient.singleUseReadOnlyTransaction()
+ : dbClient.singleUseReadOnlyTransaction(readOnlyStaleness);
+ Callable callable =
+ () -> {
+ try {
+ ResultSet rs;
+ if (analyzeMode == AnalyzeMode.NONE) {
+ rs = currentTransaction.executeQuery(statement.getStatement(), options);
+ } else {
+ rs =
+ currentTransaction.analyzeQuery(
+ statement.getStatement(), analyzeMode.getQueryAnalyzeMode());
+ }
+ // Return a DirectExecuteResultSet, which will directly do a next() call in order to
+ // ensure that the query is actually sent to Spanner.
+ ResultSet directRs = DirectExecuteResultSet.ofResultSet(rs);
+ state = UnitOfWorkState.COMMITTED;
+ readTimestamp.set(currentTransaction.getReadTimestamp());
+ return directRs;
+ } catch (Throwable t) {
+ state = UnitOfWorkState.COMMIT_FAILED;
+ readTimestamp.set(null);
+ currentTransaction.close();
+ throw t;
}
- // Return a DirectExecuteResultSet, which will directly do a next() call in order to
- // ensure that the query is actually sent to Spanner.
- ResultSet directRs = DirectExecuteResultSet.ofResultSet(rs);
- state = UnitOfWorkState.COMMITTED;
- readTimestamp.set(currentTransaction.getReadTimestamp());
- return directRs;
- } catch (Throwable t) {
- state = UnitOfWorkState.COMMIT_FAILED;
- readTimestamp.set(null);
- currentTransaction.close();
- throw t;
- }
- };
- readTimestamp = SettableApiFuture.create();
- return executeStatementAsync(
- callType, statement, callable, SpannerGrpc.getExecuteStreamingSqlMethod());
+ };
+ readTimestamp = SettableApiFuture.create();
+ return executeStatementAsync(
+ callType, statement, callable, SpannerGrpc.getExecuteStreamingSqlMethod());
+ }
}
private ApiFuture executeDmlReturningAsync(
@@ -295,26 +303,28 @@ public ApiFuture partitionQueryAsync(
ParsedStatement query,
PartitionOptions partitionOptions,
QueryOption... options) {
- Callable callable =
- () -> {
- try (BatchReadOnlyTransaction transaction =
- batchClient.batchReadOnlyTransaction(readOnlyStaleness)) {
- ResultSet resultSet = partitionQuery(transaction, partitionOptions, query, options);
- readTimestamp.set(transaction.getReadTimestamp());
- state = UnitOfWorkState.COMMITTED;
- return resultSet;
- } catch (Throwable throwable) {
- state = UnitOfWorkState.COMMIT_FAILED;
- readTimestamp.set(null);
- throw throwable;
- }
- };
- readTimestamp = SettableApiFuture.create();
- return executeStatementAsync(
- callType,
- query,
- callable,
- ImmutableList.of(SpannerGrpc.getExecuteSqlMethod(), SpannerGrpc.getCommitMethod()));
+ try (Scope ignore = span.makeCurrent()) {
+ Callable callable =
+ () -> {
+ try (BatchReadOnlyTransaction transaction =
+ batchClient.batchReadOnlyTransaction(readOnlyStaleness)) {
+ ResultSet resultSet = partitionQuery(transaction, partitionOptions, query, options);
+ readTimestamp.set(transaction.getReadTimestamp());
+ state = UnitOfWorkState.COMMITTED;
+ return resultSet;
+ } catch (Throwable throwable) {
+ state = UnitOfWorkState.COMMIT_FAILED;
+ readTimestamp.set(null);
+ throw throwable;
+ }
+ };
+ readTimestamp = SettableApiFuture.create();
+ return executeStatementAsync(
+ callType,
+ query,
+ callable,
+ ImmutableList.of(SpannerGrpc.getExecuteSqlMethod(), SpannerGrpc.getCommitMethod()));
+ }
}
@Override
@@ -373,29 +383,32 @@ public ApiFuture executeDdlAsync(CallType callType, final ParsedStatement
ddl.getType() == StatementType.DDL, "Statement is not a ddl statement");
ConnectionPreconditions.checkState(
!isReadOnly(), "DDL statements are not allowed in read-only mode");
- checkAndMarkUsed();
-
- Callable callable =
- () -> {
- try {
- OperationFuture, ?> operation;
- if (DdlClient.isCreateDatabaseStatement(ddl.getSqlWithoutComments())) {
- operation =
- ddlClient.executeCreateDatabase(
- ddl.getSqlWithoutComments(), dbClient.getDialect());
- } else {
- operation = ddlClient.executeDdl(ddl.getSqlWithoutComments());
+ try (Scope ignore = span.makeCurrent()) {
+ checkAndMarkUsed();
+ span.setAttribute(DB_STATEMENT_KEY, ddl.getStatement().getSql());
+
+ Callable callable =
+ () -> {
+ try {
+ OperationFuture, ?> operation;
+ if (DdlClient.isCreateDatabaseStatement(ddl.getSqlWithoutComments())) {
+ operation =
+ ddlClient.executeCreateDatabase(
+ ddl.getSqlWithoutComments(), dbClient.getDialect());
+ } else {
+ operation = ddlClient.executeDdl(ddl.getSqlWithoutComments());
+ }
+ getWithStatementTimeout(operation, ddl);
+ state = UnitOfWorkState.COMMITTED;
+ return null;
+ } catch (Throwable t) {
+ state = UnitOfWorkState.COMMIT_FAILED;
+ throw t;
}
- getWithStatementTimeout(operation, ddl);
- state = UnitOfWorkState.COMMITTED;
- return null;
- } catch (Throwable t) {
- state = UnitOfWorkState.COMMIT_FAILED;
- throw t;
- }
- };
- return executeStatementAsync(
- callType, ddl, callable, DatabaseAdminGrpc.getUpdateDatabaseDdlMethod());
+ };
+ return executeStatementAsync(
+ callType, ddl, callable, DatabaseAdminGrpc.getUpdateDatabaseDdlMethod());
+ }
}
@Override
@@ -405,25 +418,27 @@ public ApiFuture executeUpdateAsync(
Preconditions.checkArgument(update.isUpdate(), "Statement is not an update statement");
ConnectionPreconditions.checkState(
!isReadOnly(), "Update statements are not allowed in read-only mode");
- checkAndMarkUsed();
-
- ApiFuture res;
- switch (autocommitDmlMode) {
- case TRANSACTIONAL:
- res =
- ApiFutures.transform(
- executeTransactionalUpdateAsync(callType, update, AnalyzeMode.NONE, options),
- Tuple::x,
- MoreExecutors.directExecutor());
- break;
- case PARTITIONED_NON_ATOMIC:
- res = executePartitionedUpdateAsync(callType, update, options);
- break;
- default:
- throw SpannerExceptionFactory.newSpannerException(
- ErrorCode.FAILED_PRECONDITION, "Unknown dml mode: " + autocommitDmlMode);
+ try (Scope ignore = span.makeCurrent()) {
+ checkAndMarkUsed();
+
+ ApiFuture res;
+ switch (autocommitDmlMode) {
+ case TRANSACTIONAL:
+ res =
+ ApiFutures.transform(
+ executeTransactionalUpdateAsync(callType, update, AnalyzeMode.NONE, options),
+ Tuple::x,
+ MoreExecutors.directExecutor());
+ break;
+ case PARTITIONED_NON_ATOMIC:
+ res = executePartitionedUpdateAsync(callType, update, options);
+ break;
+ default:
+ throw SpannerExceptionFactory.newSpannerException(
+ ErrorCode.FAILED_PRECONDITION, "Unknown dml mode: " + autocommitDmlMode);
+ }
+ return res;
}
- return res;
}
@Override
@@ -436,12 +451,14 @@ public ApiFuture analyzeUpdateAsync(
ConnectionPreconditions.checkState(
autocommitDmlMode != AutocommitDmlMode.PARTITIONED_NON_ATOMIC,
"Analyzing update statements is not supported for Partitioned DML");
- checkAndMarkUsed();
+ try (Scope ignore = span.makeCurrent()) {
+ checkAndMarkUsed();
- return ApiFutures.transform(
- executeTransactionalUpdateAsync(callType, update, analyzeMode, options),
- Tuple::y,
- MoreExecutors.directExecutor());
+ return ApiFutures.transform(
+ executeTransactionalUpdateAsync(callType, update, analyzeMode, options),
+ Tuple::y,
+ MoreExecutors.directExecutor());
+ }
}
@Override
@@ -455,17 +472,20 @@ public ApiFuture executeBatchUpdateAsync(
}
ConnectionPreconditions.checkState(
!isReadOnly(), "Batch update statements are not allowed in read-only mode");
- checkAndMarkUsed();
-
- switch (autocommitDmlMode) {
- case TRANSACTIONAL:
- return executeTransactionalBatchUpdateAsync(callType, updates, options);
- case PARTITIONED_NON_ATOMIC:
- throw SpannerExceptionFactory.newSpannerException(
- ErrorCode.FAILED_PRECONDITION, "Batch updates are not allowed in " + autocommitDmlMode);
- default:
- throw SpannerExceptionFactory.newSpannerException(
- ErrorCode.FAILED_PRECONDITION, "Unknown dml mode: " + autocommitDmlMode);
+ try (Scope ignore = span.makeCurrent()) {
+ checkAndMarkUsed();
+
+ switch (autocommitDmlMode) {
+ case TRANSACTIONAL:
+ return executeTransactionalBatchUpdateAsync(callType, updates, options);
+ case PARTITIONED_NON_ATOMIC:
+ throw SpannerExceptionFactory.newSpannerException(
+ ErrorCode.FAILED_PRECONDITION,
+ "Batch updates are not allowed in " + autocommitDmlMode);
+ default:
+ throw SpannerExceptionFactory.newSpannerException(
+ ErrorCode.FAILED_PRECONDITION, "Unknown dml mode: " + autocommitDmlMode);
+ }
}
}
@@ -610,27 +630,29 @@ public ApiFuture writeAsync(CallType callType, final Iterable mu
Preconditions.checkNotNull(mutations);
ConnectionPreconditions.checkState(
!isReadOnly(), "Update statements are not allowed in read-only mode");
- checkAndMarkUsed();
-
- Callable callable =
- () -> {
- try {
- writeTransaction = createWriteTransaction();
- Void res =
- writeTransaction.run(
- transaction -> {
- transaction.buffer(mutations);
- return null;
- });
- state = UnitOfWorkState.COMMITTED;
- return res;
- } catch (Throwable t) {
- state = UnitOfWorkState.COMMIT_FAILED;
- throw t;
- }
- };
- return executeStatementAsync(
- callType, COMMIT_STATEMENT, callable, SpannerGrpc.getCommitMethod());
+ try (Scope ignore = span.makeCurrent()) {
+ checkAndMarkUsed();
+
+ Callable callable =
+ () -> {
+ try {
+ writeTransaction = createWriteTransaction();
+ Void res =
+ writeTransaction.run(
+ transaction -> {
+ transaction.buffer(mutations);
+ return null;
+ });
+ state = UnitOfWorkState.COMMITTED;
+ return res;
+ } catch (Throwable t) {
+ state = UnitOfWorkState.COMMIT_FAILED;
+ throw t;
+ }
+ };
+ return executeStatementAsync(
+ callType, COMMIT_STATEMENT, callable, SpannerGrpc.getCommitMethod());
+ }
}
@Override
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerPool.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerPool.java
index b4ac7cf3a35..6f8d4ef468d 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerPool.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/SpannerPool.java
@@ -158,6 +158,7 @@ static class SpannerPoolKey {
private final boolean routeToLeader;
private final boolean useVirtualGrpcTransportThreads;
private final OpenTelemetry openTelemetry;
+ private final Boolean enableExtendedTracing;
@VisibleForTesting
static SpannerPoolKey of(ConnectionOptions options) {
@@ -186,6 +187,7 @@ private SpannerPoolKey(ConnectionOptions options) throws IOException {
this.routeToLeader = options.isRouteToLeader();
this.useVirtualGrpcTransportThreads = options.isUseVirtualGrpcTransportThreads();
this.openTelemetry = options.getOpenTelemetry();
+ this.enableExtendedTracing = options.isEnableExtendedTracing();
}
@Override
@@ -205,7 +207,8 @@ public boolean equals(Object o) {
&& Objects.equals(this.routeToLeader, other.routeToLeader)
&& Objects.equals(
this.useVirtualGrpcTransportThreads, other.useVirtualGrpcTransportThreads)
- && Objects.equals(this.openTelemetry, other.openTelemetry);
+ && Objects.equals(this.openTelemetry, other.openTelemetry)
+ && Objects.equals(this.enableExtendedTracing, other.enableExtendedTracing);
}
@Override
@@ -221,7 +224,8 @@ public int hashCode() {
this.userAgent,
this.routeToLeader,
this.useVirtualGrpcTransportThreads,
- this.openTelemetry);
+ this.openTelemetry,
+ this.enableExtendedTracing);
}
}
@@ -357,6 +361,9 @@ Spanner createSpanner(SpannerPoolKey key, ConnectionOptions options) {
if (key.openTelemetry != null) {
builder.setOpenTelemetry(key.openTelemetry);
}
+ if (key.enableExtendedTracing != null) {
+ builder.setEnableExtendedTracing(key.enableExtendedTracing);
+ }
if (key.numChannels != null) {
builder.setNumChannels(key.numChannels);
}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/HeaderInterceptor.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/HeaderInterceptor.java
index 7de63dc33ba..b32cca08696 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/HeaderInterceptor.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/HeaderInterceptor.java
@@ -22,7 +22,11 @@
import static com.google.cloud.spanner.spi.v1.SpannerRpcViews.SPANNER_GFE_HEADER_MISSING_COUNT;
import static com.google.cloud.spanner.spi.v1.SpannerRpcViews.SPANNER_GFE_LATENCY;
+import com.google.cloud.spanner.SpannerExceptionFactory;
import com.google.cloud.spanner.SpannerRpcMetrics;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.spanner.admin.database.v1.DatabaseName;
import io.grpc.CallOptions;
import io.grpc.Channel;
import io.grpc.ClientCall;
@@ -40,6 +44,7 @@
import io.opencensus.tags.Tags;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.AttributesBuilder;
+import java.util.concurrent.ExecutionException;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
@@ -50,15 +55,22 @@
* Missing count metrics.
*/
class HeaderInterceptor implements ClientInterceptor {
-
+ private static final DatabaseName UNDEFINED_DATABASE_NAME =
+ DatabaseName.of("undefined-project", "undefined-instance", "undefined-database");
private static final Metadata.Key SERVER_TIMING_HEADER_KEY =
Metadata.Key.of("server-timing", Metadata.ASCII_STRING_MARSHALLER);
- private static final Pattern SERVER_TIMING_HEADER_PATTERN = Pattern.compile(".*dur=(?\\d+)");
+ private static final String SERVER_TIMING_HEADER_PREFIX = "gfet4t7; dur=";
private static final Metadata.Key GOOGLE_CLOUD_RESOURCE_PREFIX_KEY =
Metadata.Key.of("google-cloud-resource-prefix", Metadata.ASCII_STRING_MARSHALLER);
private static final Pattern GOOGLE_CLOUD_RESOURCE_PREFIX_PATTERN =
Pattern.compile(
".*projects/(?\\p{ASCII}[^/]*)(/instances/(?\\p{ASCII}[^/]*))?(/databases/(?\\p{ASCII}[^/]*))?");
+ private final Cache databaseNameCache =
+ CacheBuilder.newBuilder().maximumSize(100).build();
+ private final Cache tagsCache =
+ CacheBuilder.newBuilder().maximumSize(1000).build();
+ private final Cache attributesCache =
+ CacheBuilder.newBuilder().maximumSize(1000).build();
// Get the global singleton Tagger object.
private static final Tagger TAGGER = Tags.getTagger();
@@ -72,57 +84,49 @@ class HeaderInterceptor implements ClientInterceptor {
this.spannerRpcMetrics = spannerRpcMetrics;
}
- private class SpannerProperties {
- String projectId;
- String instanceId;
- String databaseId;
-
- SpannerProperties(String projectId, String instanceId, String databaseId) {
- this.databaseId = databaseId;
- this.instanceId = instanceId;
- this.projectId = projectId;
- }
- }
-
@Override
public ClientCall interceptCall(
MethodDescriptor method, CallOptions callOptions, Channel next) {
return new SimpleForwardingClientCall(next.newCall(method, callOptions)) {
@Override
public void start(Listener responseListener, Metadata headers) {
- SpannerProperties spannerProperties = createProjectPropertes(headers);
- TagContext tagContext = getTagContext(method.getFullMethodName(), spannerProperties);
- Attributes attributes = getMetricAttributes(method.getFullMethodName(), spannerProperties);
- super.start(
- new SimpleForwardingClientCallListener(responseListener) {
- @Override
- public void onHeaders(Metadata metadata) {
- processHeader(metadata, tagContext, attributes);
- super.onHeaders(metadata);
- }
- },
- headers);
+ try {
+ DatabaseName databaseName = extractDatabaseName(headers);
+ String key = databaseName + method.getFullMethodName();
+ TagContext tagContext = getTagContext(key, method.getFullMethodName(), databaseName);
+ Attributes attributes =
+ getMetricAttributes(key, method.getFullMethodName(), databaseName);
+ super.start(
+ new SimpleForwardingClientCallListener(responseListener) {
+ @Override
+ public void onHeaders(Metadata metadata) {
+ processHeader(metadata, tagContext, attributes);
+ super.onHeaders(metadata);
+ }
+ },
+ headers);
+ } catch (ExecutionException executionException) {
+ // This should never happen,
+ throw SpannerExceptionFactory.asSpannerException(executionException.getCause());
+ }
}
};
}
private void processHeader(Metadata metadata, TagContext tagContext, Attributes attributes) {
MeasureMap measureMap = STATS_RECORDER.newMeasureMap();
- if (metadata.get(SERVER_TIMING_HEADER_KEY) != null) {
- String serverTiming = metadata.get(SERVER_TIMING_HEADER_KEY);
- Matcher matcher = SERVER_TIMING_HEADER_PATTERN.matcher(serverTiming);
- if (matcher.find()) {
- try {
- long latency = Long.parseLong(matcher.group("dur"));
- measureMap.put(SPANNER_GFE_LATENCY, latency);
- measureMap.put(SPANNER_GFE_HEADER_MISSING_COUNT, 0L);
- measureMap.record(tagContext);
-
- spannerRpcMetrics.recordGfeLatency(latency, attributes);
- spannerRpcMetrics.recordGfeHeaderMissingCount(0L, attributes);
- } catch (NumberFormatException e) {
- LOGGER.log(LEVEL, "Invalid server-timing object in header", matcher.group("dur"));
- }
+ String serverTiming = metadata.get(SERVER_TIMING_HEADER_KEY);
+ if (serverTiming != null && serverTiming.startsWith(SERVER_TIMING_HEADER_PREFIX)) {
+ try {
+ long latency = Long.parseLong(serverTiming.substring(SERVER_TIMING_HEADER_PREFIX.length()));
+ measureMap.put(SPANNER_GFE_LATENCY, latency);
+ measureMap.put(SPANNER_GFE_HEADER_MISSING_COUNT, 0L);
+ measureMap.record(tagContext);
+
+ spannerRpcMetrics.recordGfeLatency(latency, attributes);
+ spannerRpcMetrics.recordGfeHeaderMissingCount(0L, attributes);
+ } catch (NumberFormatException e) {
+ LOGGER.log(LEVEL, "Invalid server-timing object in header: {}", serverTiming);
}
} else {
spannerRpcMetrics.recordGfeHeaderMissingCount(1L, attributes);
@@ -130,45 +134,60 @@ private void processHeader(Metadata metadata, TagContext tagContext, Attributes
}
}
- private SpannerProperties createProjectPropertes(Metadata headers) {
- String projectId = "undefined-project";
- String instanceId = "undefined-database";
- String databaseId = "undefined-database";
- if (headers.get(GOOGLE_CLOUD_RESOURCE_PREFIX_KEY) != null) {
- String googleResourcePrefix = headers.get(GOOGLE_CLOUD_RESOURCE_PREFIX_KEY);
- Matcher matcher = GOOGLE_CLOUD_RESOURCE_PREFIX_PATTERN.matcher(googleResourcePrefix);
- if (matcher.find()) {
- projectId = matcher.group("project");
- if (matcher.group("instance") != null) {
- instanceId = matcher.group("instance");
- }
- if (matcher.group("database") != null) {
- databaseId = matcher.group("database");
- }
- } else {
- LOGGER.log(LEVEL, "Error parsing google cloud resource header: " + googleResourcePrefix);
- }
+ private DatabaseName extractDatabaseName(Metadata headers) throws ExecutionException {
+ String googleResourcePrefix = headers.get(GOOGLE_CLOUD_RESOURCE_PREFIX_KEY);
+ if (googleResourcePrefix != null) {
+ return databaseNameCache.get(
+ googleResourcePrefix,
+ () -> {
+ String projectId = "undefined-project";
+ String instanceId = "undefined-database";
+ String databaseId = "undefined-database";
+ Matcher matcher = GOOGLE_CLOUD_RESOURCE_PREFIX_PATTERN.matcher(googleResourcePrefix);
+ if (matcher.find()) {
+ projectId = matcher.group("project");
+ if (matcher.group("instance") != null) {
+ instanceId = matcher.group("instance");
+ }
+ if (matcher.group("database") != null) {
+ databaseId = matcher.group("database");
+ }
+ } else {
+ LOGGER.log(
+ LEVEL, "Error parsing google cloud resource header: " + googleResourcePrefix);
+ }
+ return DatabaseName.of(projectId, instanceId, databaseId);
+ });
}
- return new SpannerProperties(projectId, instanceId, databaseId);
+ return UNDEFINED_DATABASE_NAME;
}
- private TagContext getTagContext(String method, SpannerProperties spannerProperties) {
- return TAGGER
- .currentBuilder()
- .putLocal(PROJECT_ID, TagValue.create(spannerProperties.projectId))
- .putLocal(INSTANCE_ID, TagValue.create(spannerProperties.instanceId))
- .putLocal(DATABASE_ID, TagValue.create(spannerProperties.databaseId))
- .putLocal(METHOD, TagValue.create(method))
- .build();
+ private TagContext getTagContext(String key, String method, DatabaseName databaseName)
+ throws ExecutionException {
+ return tagsCache.get(
+ key,
+ () ->
+ TAGGER
+ .currentBuilder()
+ .putLocal(PROJECT_ID, TagValue.create(databaseName.getProject()))
+ .putLocal(INSTANCE_ID, TagValue.create(databaseName.getInstance()))
+ .putLocal(DATABASE_ID, TagValue.create(databaseName.getDatabase()))
+ .putLocal(METHOD, TagValue.create(method))
+ .build());
}
- private Attributes getMetricAttributes(String method, SpannerProperties spannerProperties) {
- AttributesBuilder attributesBuilder = Attributes.builder();
- attributesBuilder.put("database", spannerProperties.databaseId);
- attributesBuilder.put("instance_id", spannerProperties.instanceId);
- attributesBuilder.put("project_id", spannerProperties.projectId);
- attributesBuilder.put("method", method);
-
- return attributesBuilder.build();
+ private Attributes getMetricAttributes(String key, String method, DatabaseName databaseName)
+ throws ExecutionException {
+ return attributesCache.get(
+ key,
+ () -> {
+ AttributesBuilder attributesBuilder = Attributes.builder();
+ attributesBuilder.put("database", databaseName.getDatabase());
+ attributesBuilder.put("instance_id", databaseName.getInstance());
+ attributesBuilder.put("project_id", databaseName.getProject());
+ attributesBuilder.put("method", method);
+
+ return attributesBuilder.build();
+ });
}
}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerMetadataProvider.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerMetadataProvider.java
index 77406a5399b..0b8d76d52df 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerMetadataProvider.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerMetadataProvider.java
@@ -15,17 +15,25 @@
*/
package com.google.cloud.spanner.spi.v1;
+import com.google.cloud.spanner.SpannerExceptionFactory;
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Strings;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
import com.google.common.collect.ImmutableMap;
import io.grpc.Metadata;
import io.grpc.Metadata.Key;
import java.util.Collections;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.ExecutionException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** For internal use only. */
class SpannerMetadataProvider {
+ private final Cache>> extraHeadersCache =
+ CacheBuilder.newBuilder().maximumSize(100).build();
private final Map, String> headers;
private final String resourceHeaderKey;
private static final String ROUTE_TO_LEADER_HEADER_KEY = "x-goog-spanner-route-to-leader";
@@ -61,12 +69,20 @@ Metadata newMetadata(String resourceTokenTemplate, String defaultResourceToken)
Map> newExtraHeaders(
String resourceTokenTemplate, String defaultResourceToken) {
- return ImmutableMap.>builder()
- .put(
- resourceHeaderKey,
- Collections.singletonList(
- getResourceHeaderValue(resourceTokenTemplate, defaultResourceToken)))
- .build();
+ try {
+ return extraHeadersCache.get(
+ MoreObjects.firstNonNull(resourceTokenTemplate, ""),
+ () ->
+ ImmutableMap.>builder()
+ .put(
+ resourceHeaderKey,
+ Collections.singletonList(
+ getResourceHeaderValue(resourceTokenTemplate, defaultResourceToken)))
+ .build());
+ } catch (ExecutionException executionException) {
+ // This should never happen.
+ throw SpannerExceptionFactory.asSpannerException(executionException.getCause());
+ }
}
Map> newRouteToLeaderHeader() {
@@ -86,7 +102,7 @@ private Map, String> constructHeadersAsMetadata(
private String getResourceHeaderValue(String resourceTokenTemplate, String defaultResourceToken) {
String resourceToken = defaultResourceToken;
- if (resourceTokenTemplate != null) {
+ if (!Strings.isNullOrEmpty(resourceTokenTemplate)) {
for (Pattern pattern : RESOURCE_TOKEN_PATTERNS) {
Matcher m = pattern.matcher(resourceTokenTemplate);
if (m.matches()) {
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 41da46a56a8..91189c70d9c 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
@@ -3883,7 +3883,7 @@ public void testReadWriteTransaction_usesOptions() {
TransactionOption option = mock(TransactionOption.class);
TraceWrapper traceWrapper =
- new TraceWrapper(Tracing.getTracer(), OpenTelemetry.noop().getTracer(""));
+ new TraceWrapper(Tracing.getTracer(), OpenTelemetry.noop().getTracer(""), false);
DatabaseClientImpl client = new DatabaseClientImpl(pool, traceWrapper);
client.readWriteTransaction(option);
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ITSessionPoolIntegrationTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ITSessionPoolIntegrationTest.java
index be9f6841f20..df29aac9170 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ITSessionPoolIntegrationTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ITSessionPoolIntegrationTest.java
@@ -96,7 +96,7 @@ public ScheduledExecutorService get() {
}
},
((SpannerImpl) env.getTestHelper().getClient()).getSessionClient(db.getId()),
- new TraceWrapper(Tracing.getTracer(), OpenTelemetry.noop().getTracer("")),
+ new TraceWrapper(Tracing.getTracer(), OpenTelemetry.noop().getTracer(""), false),
OpenTelemetry.noop());
}
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/IntegrationTestEnv.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/IntegrationTestEnv.java
index 4d4f639d3d9..6dd9c29e23f 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/IntegrationTestEnv.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/IntegrationTestEnv.java
@@ -17,6 +17,7 @@
package com.google.cloud.spanner;
import static com.google.common.base.Preconditions.checkState;
+import static org.junit.Assume.assumeFalse;
import com.google.api.client.util.ExponentialBackOff;
import com.google.api.gax.longrunning.OperationFuture;
@@ -26,6 +27,7 @@
import com.google.cloud.spanner.testing.RemoteSpannerHelper;
import com.google.common.collect.Iterators;
import com.google.spanner.admin.instance.v1.CreateInstanceMetadata;
+import java.util.Objects;
import java.util.Random;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
@@ -89,9 +91,17 @@ protected void initializeConfig()
config = configClass.newInstance();
}
+ boolean isCloudDevel() {
+ return Objects.equals(
+ System.getProperty("spanner.gce.config.server_url"),
+ "https://staging-wrenchworks.sandbox.googleapis.com");
+ }
+
@Override
protected void before() throws Throwable {
this.initializeConfig();
+ assumeFalse(alwaysCreateNewInstance && isCloudDevel());
+
this.config.setUp();
SpannerOptions options = config.spannerOptions();
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 f596183507e..ca7f8386894 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
@@ -292,7 +292,7 @@ private SessionPool createPool() {
client.getSessionClient(db),
clock,
Position.FIRST,
- new TraceWrapper(Tracing.getTracer(), OpenTelemetry.noop().getTracer("")),
+ new TraceWrapper(Tracing.getTracer(), OpenTelemetry.noop().getTracer(""), false),
OpenTelemetry.noop());
pool.multiplexedSessionRemovedListener =
input -> {
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 ed9926dea88..fcad1ff22c6 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
@@ -54,7 +54,7 @@ public class MultiplexedSessionPoolTest extends BaseSessionPoolTest {
@Mock SpannerOptions spannerOptions;
private final DatabaseId db = DatabaseId.of("projects/p/instances/i/databases/unused");
private final TraceWrapper tracer =
- new TraceWrapper(Tracing.getTracer(), OpenTelemetry.noop().getTracer(""));
+ new TraceWrapper(Tracing.getTracer(), OpenTelemetry.noop().getTracer(""), false);
SessionPoolOptions options;
SessionPool pool;
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 a351231f0e7..175fda049ee 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
@@ -424,11 +424,13 @@ public void transactionRunner() {
? ImmutableList.of(
"CloudSpannerOperation.CreateMultiplexedSession",
"CloudSpannerOperation.BatchCreateSessionsRequest",
+ "CloudSpannerOperation.ExecuteUpdate",
"CloudSpannerOperation.Commit",
"CloudSpannerOperation.BatchCreateSessions",
"CloudSpanner.ReadWriteTransaction")
: ImmutableList.of(
"CloudSpannerOperation.BatchCreateSessionsRequest",
+ "CloudSpannerOperation.ExecuteUpdate",
"CloudSpannerOperation.Commit",
"CloudSpannerOperation.BatchCreateSessions",
"CloudSpanner.ReadWriteTransaction");
@@ -460,6 +462,7 @@ public void transactionRunner() {
expectedBatchCreateSessionsEventsCount);
break;
case "CloudSpannerOperation.Commit":
+ case "CloudSpannerOperation.ExecuteUpdate":
assertEquals(0, spanItem.getEvents().size());
break;
case "CloudSpanner.ReadWriteTransaction":
@@ -484,10 +487,12 @@ public void transactionRunnerWithError() {
"CloudSpannerOperation.CreateMultiplexedSession",
"CloudSpannerOperation.BatchCreateSessionsRequest",
"CloudSpannerOperation.BatchCreateSessions",
+ "CloudSpannerOperation.ExecuteUpdate",
"CloudSpanner.ReadWriteTransaction")
: ImmutableList.of(
"CloudSpannerOperation.BatchCreateSessionsRequest",
"CloudSpannerOperation.BatchCreateSessions",
+ "CloudSpannerOperation.ExecuteUpdate",
"CloudSpanner.ReadWriteTransaction");
TransactionRunner runner = client.readWriteTransaction();
SpannerException e =
@@ -527,6 +532,9 @@ public void transactionRunnerWithError() {
expectedReadWriteTransactionErrorEvents,
expectedReadWriteTransactionErrorEventsCount);
break;
+ case "CloudSpannerOperation.ExecuteUpdate":
+ assertEquals(0, spanItem.getEvents().size());
+ break;
default:
assert false;
}
@@ -543,12 +551,14 @@ public void transactionRunnerWithFailedAndBeginTransaction() {
"CloudSpannerOperation.CreateMultiplexedSession",
"CloudSpannerOperation.BeginTransaction",
"CloudSpannerOperation.BatchCreateSessionsRequest",
+ "CloudSpannerOperation.ExecuteUpdate",
"CloudSpannerOperation.Commit",
"CloudSpannerOperation.BatchCreateSessions",
"CloudSpanner.ReadWriteTransaction")
: ImmutableList.of(
"CloudSpannerOperation.BeginTransaction",
"CloudSpannerOperation.BatchCreateSessionsRequest",
+ "CloudSpannerOperation.ExecuteUpdate",
"CloudSpannerOperation.Commit",
"CloudSpannerOperation.BatchCreateSessions",
"CloudSpanner.ReadWriteTransaction");
@@ -606,6 +616,7 @@ public void transactionRunnerWithFailedAndBeginTransaction() {
break;
case "CloudSpannerOperation.Commit":
case "CloudSpannerOperation.BeginTransaction":
+ case "CloudSpannerOperation.ExecuteUpdate":
assertEquals(0, spanItem.getEvents().size());
break;
case "CloudSpanner.ReadWriteTransaction":
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 899bec4c622..88080a39ef5 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
@@ -157,7 +157,7 @@ private void initWithLimit(int maxBufferSize) {
maxBufferSize,
"",
new OpenTelemetrySpan(mock(io.opentelemetry.api.trace.Span.class)),
- new TraceWrapper(Tracing.getTracer(), OpenTelemetry.noop().getTracer("")),
+ new TraceWrapper(Tracing.getTracer(), OpenTelemetry.noop().getTracer(""), false),
SpannerStubSettings.newBuilder().executeStreamingSqlSettings().getRetrySettings(),
SpannerStubSettings.newBuilder().executeStreamingSqlSettings().getRetryableCodes()) {
@Override
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 c0ae8de97c9..bcba430c521 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
@@ -92,7 +92,7 @@ public static Collection
@@ -33,7 +33,7 @@
com.google.cloudgoogle-cloud-spanner
- 6.65.1
+ 6.66.0
@@ -107,6 +107,11 @@
+
+
+ ../snippets/src/main/resources
+
+ org.codehaus.mojo
@@ -143,8 +148,8 @@
3.2.5
- java-client-integration-test
- java-client-mr-integration-test
+ java-client-integration-tests
+ java-client-mr-integration-testsnam11us-east1java-client-integration-test-cmek-ring
diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml
index 6ffc3d57db9..8877cf82b20 100644
--- a/samples/snapshot/pom.xml
+++ b/samples/snapshot/pom.xml
@@ -24,7 +24,7 @@
UTF-80.31.12.41.0
- 3.43.0
+ 3.44.0
@@ -32,7 +32,7 @@
com.google.cloudgoogle-cloud-spanner
- 6.66.0
+ 6.67.0
@@ -106,6 +106,11 @@
+
+
+ ../snippets/src/main/resources
+
+ org.codehaus.mojo
@@ -142,8 +147,8 @@
3.2.5
- java-client-integration-test
- java-client-mr-integration-test
+ java-client-integration-tests
+ java-client-mr-integration-testsnam11us-east1java-client-integration-test-cmek-ring
diff --git a/samples/snippets/pom.xml b/samples/snippets/pom.xml
index c52316baa78..fc8c0052157 100644
--- a/samples/snippets/pom.xml
+++ b/samples/snippets/pom.xml
@@ -178,8 +178,8 @@
3.2.5
- java-client-integration-test
- java-client-mr-integration-test
+ java-client-integration-tests
+ java-client-mr-integration-testsnam11us-east1java-client-integration-test-cmek-ring
@@ -189,6 +189,15 @@
+
+ org.apache.maven.plugins
+ maven-checkstyle-plugin
+ 3.3.1
+
+
+ **/SingerProto.java
+
+
diff --git a/samples/snippets/src/main/java/com/example/spanner/AddProtoColumnSample.java b/samples/snippets/src/main/java/com/example/spanner/AddProtoColumnSample.java
new file mode 100644
index 00000000000..36be70034f7
--- /dev/null
+++ b/samples/snippets/src/main/java/com/example/spanner/AddProtoColumnSample.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2024 Google Inc.
+ *
+ * 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.example.spanner;
+
+// [START spanner_add_proto_type_columns]
+
+import com.google.cloud.spanner.Spanner;
+import com.google.cloud.spanner.SpannerOptions;
+import com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient;
+import com.google.common.collect.ImmutableList;
+import com.google.protobuf.ByteString;
+import com.google.spanner.admin.database.v1.DatabaseName;
+import com.google.spanner.admin.database.v1.UpdateDatabaseDdlRequest;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.concurrent.ExecutionException;
+
+class AddProtoColumnSample {
+
+ static void addProtoColumn() throws InterruptedException, ExecutionException, IOException {
+ // TODO(developer): Replace these variables before running the sample.
+ String projectId = "my-project";
+ String instanceId = "my-instance";
+ String databaseId = "my-database";
+
+ addProtoColumn(projectId, instanceId, databaseId);
+ }
+
+ static void addProtoColumn(String projectId, String instanceId, String databaseId)
+ throws InterruptedException, ExecutionException, IOException {
+ InputStream in =
+ AddProtoColumnSample.class
+ .getClassLoader()
+ .getResourceAsStream("com/example/spanner/descriptors.pb");
+ try (Spanner spanner =
+ SpannerOptions.newBuilder().setProjectId(projectId).build().getService();
+ DatabaseAdminClient databaseAdminClient = spanner.createDatabaseAdminClient()) {
+ UpdateDatabaseDdlRequest request =
+ UpdateDatabaseDdlRequest.newBuilder()
+ .setDatabase(DatabaseName.of(projectId, instanceId, databaseId).toString())
+ .addAllStatements(
+ ImmutableList.of(
+ "CREATE PROTO BUNDLE ("
+ + "examples.spanner.music.SingerInfo,"
+ + "examples.spanner.music.Genre,"
+ + ")",
+ "ALTER TABLE Singers ADD COLUMN SingerInfo examples.spanner.music.SingerInfo",
+ "ALTER TABLE Singers ADD COLUMN "
+ + "SingerInfoArray ARRAY",
+ "ALTER TABLE Singers ADD COLUMN SingerGenre examples.spanner.music.Genre",
+ "ALTER TABLE Singers ADD COLUMN "
+ + "SingerGenreArray ARRAY"))
+ .setProtoDescriptors(ByteString.readFrom(in))
+ .build();
+ // Wait for the operation to finish.
+ // This will throw an ExecutionException if the operation fails.
+ databaseAdminClient.updateDatabaseDdlAsync(request).get();
+ System.out.printf("Added Proto columns %n");
+ }
+ }
+}
+// [END spanner_add_proto_type_columns]
diff --git a/samples/snippets/src/main/java/com/example/spanner/QueryWithProtoParameterSample.java b/samples/snippets/src/main/java/com/example/spanner/QueryWithProtoParameterSample.java
new file mode 100644
index 00000000000..b700fa341fa
--- /dev/null
+++ b/samples/snippets/src/main/java/com/example/spanner/QueryWithProtoParameterSample.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2024 Google Inc.
+ *
+ * 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.example.spanner;
+
+// [START spanner_query_with_proto_types_parameter]
+import com.example.spanner.SingerProto.Genre;
+import com.example.spanner.SingerProto.SingerInfo;
+import com.google.cloud.spanner.DatabaseClient;
+import com.google.cloud.spanner.DatabaseId;
+import com.google.cloud.spanner.ResultSet;
+import com.google.cloud.spanner.Spanner;
+import com.google.cloud.spanner.SpannerOptions;
+import com.google.cloud.spanner.Statement;
+
+class QueryWithProtoParameterSample {
+
+ static void queryWithProtoParameter() {
+ // TODO(developer): Replace these variables before running the sample.
+ String projectId = "my-project";
+ String instanceId = "my-instance";
+ String databaseId = "my-database";
+
+ try (Spanner spanner =
+ SpannerOptions.newBuilder().setProjectId(projectId).build().getService()) {
+ DatabaseClient client =
+ spanner.getDatabaseClient(DatabaseId.of(projectId, instanceId, databaseId));
+ queryWithProtoParameter(client);
+ }
+ }
+
+ static void queryWithProtoParameter(DatabaseClient client) {
+ Statement statement =
+ Statement.newBuilder(
+ "SELECT SingerId, SingerInfo, SingerInfo.nationality, SingerInfoArray, "
+ + "SingerGenre, SingerGenreArray FROM Singers "
+ + "WHERE SingerInfo.nationality=@country and SingerGenre=@singerGenre")
+ .bind("country")
+ .to("Country2")
+ .bind("singerGenre")
+ .to(Genre.FOLK)
+ .build();
+ try (ResultSet resultSet = client.singleUse().executeQuery(statement)) {
+ while (resultSet.next()) {
+ System.out.printf(
+ "%d %s %s %s %s %s%n",
+ resultSet.getLong("SingerId"),
+ resultSet.getProtoMessage("SingerInfo", SingerInfo.getDefaultInstance()),
+ resultSet.getString("nationality"),
+ resultSet.getProtoMessageList("SingerInfoArray", SingerInfo.getDefaultInstance()),
+ resultSet.getProtoEnum("SingerGenre", Genre::forNumber),
+ resultSet.getProtoEnumList("SingerGenreArray", Genre::forNumber));
+ }
+ }
+ }
+}
+// [END spanner_query_with_proto_types_parameter]
diff --git a/samples/snippets/src/main/java/com/example/spanner/SingerProto.java b/samples/snippets/src/main/java/com/example/spanner/SingerProto.java
new file mode 100644
index 00000000000..b962e4bc6b7
--- /dev/null
+++ b/samples/snippets/src/main/java/com/example/spanner/SingerProto.java
@@ -0,0 +1,1191 @@
+/*
+ * 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.
+ */
+
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+// source: samples/snippets/src/main/resources/com/example/spanner/singer.proto
+
+// Protobuf Java Version: 3.25.1
+package com.example.spanner;
+
+public final class SingerProto {
+ private SingerProto() {}
+
+ public static void registerAllExtensions(com.google.protobuf.ExtensionRegistryLite registry) {}
+
+ public static void registerAllExtensions(com.google.protobuf.ExtensionRegistry registry) {
+ registerAllExtensions((com.google.protobuf.ExtensionRegistryLite) registry);
+ }
+ /** Protobuf enum {@code examples.spanner.music.Genre} */
+ public enum Genre implements com.google.protobuf.ProtocolMessageEnum {
+ /** POP = 0; */
+ POP(0),
+ /** JAZZ = 1; */
+ JAZZ(1),
+ /** FOLK = 2; */
+ FOLK(2),
+ /** ROCK = 3; */
+ ROCK(3),
+ UNRECOGNIZED(-1),
+ ;
+
+ /** POP = 0; */
+ public static final int POP_VALUE = 0;
+ /** JAZZ = 1; */
+ public static final int JAZZ_VALUE = 1;
+ /** FOLK = 2; */
+ public static final int FOLK_VALUE = 2;
+ /** ROCK = 3; */
+ public static final int ROCK_VALUE = 3;
+
+ public final int getNumber() {
+ if (this == UNRECOGNIZED) {
+ throw new IllegalArgumentException("Can't get the number of an unknown enum value.");
+ }
+ return value;
+ }
+
+ /**
+ * @param value The numeric wire value of the corresponding enum entry.
+ * @return The enum associated with the given numeric wire value.
+ * @deprecated Use {@link #forNumber(int)} instead.
+ */
+ @Deprecated
+ public static Genre valueOf(int value) {
+ return forNumber(value);
+ }
+
+ /**
+ * @param value The numeric wire value of the corresponding enum entry.
+ * @return The enum associated with the given numeric wire value.
+ */
+ public static Genre forNumber(int value) {
+ switch (value) {
+ case 0:
+ return POP;
+ case 1:
+ return JAZZ;
+ case 2:
+ return FOLK;
+ case 3:
+ return ROCK;
+ default:
+ return null;
+ }
+ }
+
+ public static com.google.protobuf.Internal.EnumLiteMap internalGetValueMap() {
+ return internalValueMap;
+ }
+
+ private static final com.google.protobuf.Internal.EnumLiteMap internalValueMap =
+ new com.google.protobuf.Internal.EnumLiteMap() {
+ public Genre findValueByNumber(int number) {
+ return Genre.forNumber(number);
+ }
+ };
+
+ public final com.google.protobuf.Descriptors.EnumValueDescriptor getValueDescriptor() {
+ if (this == UNRECOGNIZED) {
+ throw new IllegalStateException("Can't get the descriptor of an unrecognized enum value.");
+ }
+ return getDescriptor().getValues().get(ordinal());
+ }
+
+ public final com.google.protobuf.Descriptors.EnumDescriptor getDescriptorForType() {
+ return getDescriptor();
+ }
+
+ public static final com.google.protobuf.Descriptors.EnumDescriptor getDescriptor() {
+ return SingerProto.getDescriptor().getEnumTypes().get(0);
+ }
+
+ private static final Genre[] VALUES = values();
+
+ public static Genre valueOf(com.google.protobuf.Descriptors.EnumValueDescriptor desc) {
+ if (desc.getType() != getDescriptor()) {
+ throw new IllegalArgumentException("EnumValueDescriptor is not for this type.");
+ }
+ if (desc.getIndex() == -1) {
+ return UNRECOGNIZED;
+ }
+ return VALUES[desc.getIndex()];
+ }
+
+ private final int value;
+
+ private Genre(int value) {
+ this.value = value;
+ }
+
+ // @@protoc_insertion_point(enum_scope:examples.spanner.music.Genre)
+ }
+
+ public interface SingerInfoOrBuilder
+ extends
+ // @@protoc_insertion_point(interface_extends:examples.spanner.music.SingerInfo)
+ com.google.protobuf.MessageOrBuilder {
+
+ /**
+ * optional int64 singer_id = 1;
+ *
+ * @return Whether the singerId field is set.
+ */
+ boolean hasSingerId();
+ /**
+ * optional int64 singer_id = 1;
+ *
+ * @return The singerId.
+ */
+ long getSingerId();
+
+ /**
+ * optional string birth_date = 2;
+ *
+ * @return Whether the birthDate field is set.
+ */
+ boolean hasBirthDate();
+ /**
+ * optional string birth_date = 2;
+ *
+ * @return The birthDate.
+ */
+ String getBirthDate();
+ /**
+ * optional string birth_date = 2;
+ *
+ * @return The bytes for birthDate.
+ */
+ com.google.protobuf.ByteString getBirthDateBytes();
+
+ /**
+ * optional string nationality = 3;
+ *
+ * @return Whether the nationality field is set.
+ */
+ boolean hasNationality();
+ /**
+ * optional string nationality = 3;
+ *
+ * @return The nationality.
+ */
+ String getNationality();
+ /**
+ * optional string nationality = 3;
+ *
+ * @return The bytes for nationality.
+ */
+ com.google.protobuf.ByteString getNationalityBytes();
+
+ /**
+ * optional .examples.spanner.music.Genre genre = 4;
+ *
+ * @return Whether the genre field is set.
+ */
+ boolean hasGenre();
+ /**
+ * optional .examples.spanner.music.Genre genre = 4;
+ *
+ * @return The enum numeric value on the wire for genre.
+ */
+ int getGenreValue();
+ /**
+ * optional .examples.spanner.music.Genre genre = 4;
+ *
+ * @return The genre.
+ */
+ Genre getGenre();
+ }
+ /** Protobuf type {@code examples.spanner.music.SingerInfo} */
+ public static final class SingerInfo extends com.google.protobuf.GeneratedMessageV3
+ implements
+ // @@protoc_insertion_point(message_implements:examples.spanner.music.SingerInfo)
+ SingerInfoOrBuilder {
+ private static final long serialVersionUID = 0L;
+ // Use SingerInfo.newBuilder() to construct.
+ private SingerInfo(com.google.protobuf.GeneratedMessageV3.Builder> builder) {
+ super(builder);
+ }
+
+ private SingerInfo() {
+ birthDate_ = "";
+ nationality_ = "";
+ genre_ = 0;
+ }
+
+ @Override
+ @SuppressWarnings({"unused"})
+ protected Object newInstance(UnusedPrivateParameter unused) {
+ return new SingerInfo();
+ }
+
+ public static final com.google.protobuf.Descriptors.Descriptor getDescriptor() {
+ return SingerProto.internal_static_examples_spanner_music_SingerInfo_descriptor;
+ }
+
+ @Override
+ protected FieldAccessorTable internalGetFieldAccessorTable() {
+ return SingerProto.internal_static_examples_spanner_music_SingerInfo_fieldAccessorTable
+ .ensureFieldAccessorsInitialized(SingerInfo.class, Builder.class);
+ }
+
+ private int bitField0_;
+ public static final int SINGER_ID_FIELD_NUMBER = 1;
+ private long singerId_ = 0L;
+ /**
+ * optional int64 singer_id = 1;
+ *
+ * @return Whether the singerId field is set.
+ */
+ @Override
+ public boolean hasSingerId() {
+ return ((bitField0_ & 0x00000001) != 0);
+ }
+ /**
+ * optional int64 singer_id = 1;
+ *
+ * @return The singerId.
+ */
+ @Override
+ public long getSingerId() {
+ return singerId_;
+ }
+
+ public static final int BIRTH_DATE_FIELD_NUMBER = 2;
+
+ @SuppressWarnings("serial")
+ private volatile Object birthDate_ = "";
+ /**
+ * optional string birth_date = 2;
+ *
+ * @return Whether the birthDate field is set.
+ */
+ @Override
+ public boolean hasBirthDate() {
+ return ((bitField0_ & 0x00000002) != 0);
+ }
+ /**
+ * optional string birth_date = 2;
+ *
+ * @return The birthDate.
+ */
+ @Override
+ public String getBirthDate() {
+ Object ref = birthDate_;
+ if (ref instanceof String) {
+ return (String) ref;
+ } else {
+ com.google.protobuf.ByteString bs = (com.google.protobuf.ByteString) ref;
+ String s = bs.toStringUtf8();
+ birthDate_ = s;
+ return s;
+ }
+ }
+ /**
+ * optional string birth_date = 2;
+ *
+ * @return The bytes for birthDate.
+ */
+ @Override
+ public com.google.protobuf.ByteString getBirthDateBytes() {
+ Object ref = birthDate_;
+ if (ref instanceof String) {
+ com.google.protobuf.ByteString b =
+ com.google.protobuf.ByteString.copyFromUtf8((String) ref);
+ birthDate_ = b;
+ return b;
+ } else {
+ return (com.google.protobuf.ByteString) ref;
+ }
+ }
+
+ public static final int NATIONALITY_FIELD_NUMBER = 3;
+
+ @SuppressWarnings("serial")
+ private volatile Object nationality_ = "";
+ /**
+ * optional string nationality = 3;
+ *
+ * @return Whether the nationality field is set.
+ */
+ @Override
+ public boolean hasNationality() {
+ return ((bitField0_ & 0x00000004) != 0);
+ }
+ /**
+ * optional string nationality = 3;
+ *
+ * @return The nationality.
+ */
+ @Override
+ public String getNationality() {
+ Object ref = nationality_;
+ if (ref instanceof String) {
+ return (String) ref;
+ } else {
+ com.google.protobuf.ByteString bs = (com.google.protobuf.ByteString) ref;
+ String s = bs.toStringUtf8();
+ nationality_ = s;
+ return s;
+ }
+ }
+ /**
+ * optional string nationality = 3;
+ *
+ * @return The bytes for nationality.
+ */
+ @Override
+ public com.google.protobuf.ByteString getNationalityBytes() {
+ Object ref = nationality_;
+ if (ref instanceof String) {
+ com.google.protobuf.ByteString b =
+ com.google.protobuf.ByteString.copyFromUtf8((String) ref);
+ nationality_ = b;
+ return b;
+ } else {
+ return (com.google.protobuf.ByteString) ref;
+ }
+ }
+
+ public static final int GENRE_FIELD_NUMBER = 4;
+ private int genre_ = 0;
+ /**
+ * optional .examples.spanner.music.Genre genre = 4;
+ *
+ * @return Whether the genre field is set.
+ */
+ @Override
+ public boolean hasGenre() {
+ return ((bitField0_ & 0x00000008) != 0);
+ }
+ /**
+ * optional .examples.spanner.music.Genre genre = 4;
+ *
+ * @return The enum numeric value on the wire for genre.
+ */
+ @Override
+ public int getGenreValue() {
+ return genre_;
+ }
+ /**
+ * optional .examples.spanner.music.Genre genre = 4;
+ *
+ * @return The genre.
+ */
+ @Override
+ public Genre getGenre() {
+ Genre result = Genre.forNumber(genre_);
+ return result == null ? Genre.UNRECOGNIZED : result;
+ }
+
+ private byte memoizedIsInitialized = -1;
+
+ @Override
+ public final boolean isInitialized() {
+ byte isInitialized = memoizedIsInitialized;
+ if (isInitialized == 1) return true;
+ if (isInitialized == 0) return false;
+
+ memoizedIsInitialized = 1;
+ return true;
+ }
+
+ @Override
+ public void writeTo(com.google.protobuf.CodedOutputStream output) throws java.io.IOException {
+ if (((bitField0_ & 0x00000001) != 0)) {
+ output.writeInt64(1, singerId_);
+ }
+ if (((bitField0_ & 0x00000002) != 0)) {
+ com.google.protobuf.GeneratedMessageV3.writeString(output, 2, birthDate_);
+ }
+ if (((bitField0_ & 0x00000004) != 0)) {
+ com.google.protobuf.GeneratedMessageV3.writeString(output, 3, nationality_);
+ }
+ if (((bitField0_ & 0x00000008) != 0)) {
+ output.writeEnum(4, genre_);
+ }
+ getUnknownFields().writeTo(output);
+ }
+
+ @Override
+ public int getSerializedSize() {
+ int size = memoizedSize;
+ if (size != -1) return size;
+
+ size = 0;
+ if (((bitField0_ & 0x00000001) != 0)) {
+ size += com.google.protobuf.CodedOutputStream.computeInt64Size(1, singerId_);
+ }
+ if (((bitField0_ & 0x00000002) != 0)) {
+ size += com.google.protobuf.GeneratedMessageV3.computeStringSize(2, birthDate_);
+ }
+ if (((bitField0_ & 0x00000004) != 0)) {
+ size += com.google.protobuf.GeneratedMessageV3.computeStringSize(3, nationality_);
+ }
+ if (((bitField0_ & 0x00000008) != 0)) {
+ size += com.google.protobuf.CodedOutputStream.computeEnumSize(4, genre_);
+ }
+ size += getUnknownFields().getSerializedSize();
+ memoizedSize = size;
+ return size;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (!(obj instanceof SingerInfo)) {
+ return super.equals(obj);
+ }
+ SingerInfo other = (SingerInfo) obj;
+
+ if (hasSingerId() != other.hasSingerId()) return false;
+ if (hasSingerId()) {
+ if (getSingerId() != other.getSingerId()) return false;
+ }
+ if (hasBirthDate() != other.hasBirthDate()) return false;
+ if (hasBirthDate()) {
+ if (!getBirthDate().equals(other.getBirthDate())) return false;
+ }
+ if (hasNationality() != other.hasNationality()) return false;
+ if (hasNationality()) {
+ if (!getNationality().equals(other.getNationality())) return false;
+ }
+ if (hasGenre() != other.hasGenre()) return false;
+ if (hasGenre()) {
+ if (genre_ != other.genre_) return false;
+ }
+ if (!getUnknownFields().equals(other.getUnknownFields())) return false;
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ if (memoizedHashCode != 0) {
+ return memoizedHashCode;
+ }
+ int hash = 41;
+ hash = (19 * hash) + getDescriptor().hashCode();
+ if (hasSingerId()) {
+ hash = (37 * hash) + SINGER_ID_FIELD_NUMBER;
+ hash = (53 * hash) + com.google.protobuf.Internal.hashLong(getSingerId());
+ }
+ if (hasBirthDate()) {
+ hash = (37 * hash) + BIRTH_DATE_FIELD_NUMBER;
+ hash = (53 * hash) + getBirthDate().hashCode();
+ }
+ if (hasNationality()) {
+ hash = (37 * hash) + NATIONALITY_FIELD_NUMBER;
+ hash = (53 * hash) + getNationality().hashCode();
+ }
+ if (hasGenre()) {
+ hash = (37 * hash) + GENRE_FIELD_NUMBER;
+ hash = (53 * hash) + genre_;
+ }
+ hash = (29 * hash) + getUnknownFields().hashCode();
+ memoizedHashCode = hash;
+ return hash;
+ }
+
+ public static SingerInfo parseFrom(java.nio.ByteBuffer data)
+ throws com.google.protobuf.InvalidProtocolBufferException {
+ return PARSER.parseFrom(data);
+ }
+
+ public static SingerInfo parseFrom(
+ java.nio.ByteBuffer data, com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+ throws com.google.protobuf.InvalidProtocolBufferException {
+ return PARSER.parseFrom(data, extensionRegistry);
+ }
+
+ public static SingerInfo parseFrom(com.google.protobuf.ByteString data)
+ throws com.google.protobuf.InvalidProtocolBufferException {
+ return PARSER.parseFrom(data);
+ }
+
+ public static SingerInfo parseFrom(
+ com.google.protobuf.ByteString data,
+ com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+ throws com.google.protobuf.InvalidProtocolBufferException {
+ return PARSER.parseFrom(data, extensionRegistry);
+ }
+
+ public static SingerInfo parseFrom(byte[] data)
+ throws com.google.protobuf.InvalidProtocolBufferException {
+ return PARSER.parseFrom(data);
+ }
+
+ public static SingerInfo parseFrom(
+ byte[] data, com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+ throws com.google.protobuf.InvalidProtocolBufferException {
+ return PARSER.parseFrom(data, extensionRegistry);
+ }
+
+ public static SingerInfo parseFrom(java.io.InputStream input) throws java.io.IOException {
+ return com.google.protobuf.GeneratedMessageV3.parseWithIOException(PARSER, input);
+ }
+
+ public static SingerInfo parseFrom(
+ java.io.InputStream input, com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+ throws java.io.IOException {
+ return com.google.protobuf.GeneratedMessageV3.parseWithIOException(
+ PARSER, input, extensionRegistry);
+ }
+
+ public static SingerInfo parseDelimitedFrom(java.io.InputStream input)
+ throws java.io.IOException {
+ return com.google.protobuf.GeneratedMessageV3.parseDelimitedWithIOException(PARSER, input);
+ }
+
+ public static SingerInfo parseDelimitedFrom(
+ java.io.InputStream input, com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+ throws java.io.IOException {
+ return com.google.protobuf.GeneratedMessageV3.parseDelimitedWithIOException(
+ PARSER, input, extensionRegistry);
+ }
+
+ public static SingerInfo parseFrom(com.google.protobuf.CodedInputStream input)
+ throws java.io.IOException {
+ return com.google.protobuf.GeneratedMessageV3.parseWithIOException(PARSER, input);
+ }
+
+ public static SingerInfo parseFrom(
+ com.google.protobuf.CodedInputStream input,
+ com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+ throws java.io.IOException {
+ return com.google.protobuf.GeneratedMessageV3.parseWithIOException(
+ PARSER, input, extensionRegistry);
+ }
+
+ @Override
+ public Builder newBuilderForType() {
+ return newBuilder();
+ }
+
+ public static Builder newBuilder() {
+ return DEFAULT_INSTANCE.toBuilder();
+ }
+
+ public static Builder newBuilder(SingerInfo prototype) {
+ return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype);
+ }
+
+ @Override
+ public Builder toBuilder() {
+ return this == DEFAULT_INSTANCE ? new Builder() : new Builder().mergeFrom(this);
+ }
+
+ @Override
+ protected Builder newBuilderForType(BuilderParent parent) {
+ Builder builder = new Builder(parent);
+ return builder;
+ }
+ /** Protobuf type {@code examples.spanner.music.SingerInfo} */
+ public static final class Builder
+ extends com.google.protobuf.GeneratedMessageV3.Builder
+ implements
+ // @@protoc_insertion_point(builder_implements:examples.spanner.music.SingerInfo)
+ SingerInfoOrBuilder {
+ public static final com.google.protobuf.Descriptors.Descriptor getDescriptor() {
+ return SingerProto.internal_static_examples_spanner_music_SingerInfo_descriptor;
+ }
+
+ @Override
+ protected FieldAccessorTable internalGetFieldAccessorTable() {
+ return SingerProto.internal_static_examples_spanner_music_SingerInfo_fieldAccessorTable
+ .ensureFieldAccessorsInitialized(SingerInfo.class, Builder.class);
+ }
+
+ // Construct using com.example.spanner.SingerProto.SingerInfo.newBuilder()
+ private Builder() {}
+
+ private Builder(BuilderParent parent) {
+ super(parent);
+ }
+
+ @Override
+ public Builder clear() {
+ super.clear();
+ bitField0_ = 0;
+ singerId_ = 0L;
+ birthDate_ = "";
+ nationality_ = "";
+ genre_ = 0;
+ return this;
+ }
+
+ @Override
+ public com.google.protobuf.Descriptors.Descriptor getDescriptorForType() {
+ return SingerProto.internal_static_examples_spanner_music_SingerInfo_descriptor;
+ }
+
+ @Override
+ public SingerInfo getDefaultInstanceForType() {
+ return SingerInfo.getDefaultInstance();
+ }
+
+ @Override
+ public SingerInfo build() {
+ SingerInfo result = buildPartial();
+ if (!result.isInitialized()) {
+ throw newUninitializedMessageException(result);
+ }
+ return result;
+ }
+
+ @Override
+ public SingerInfo buildPartial() {
+ SingerInfo result = new SingerInfo(this);
+ if (bitField0_ != 0) {
+ buildPartial0(result);
+ }
+ onBuilt();
+ return result;
+ }
+
+ private void buildPartial0(SingerInfo result) {
+ int from_bitField0_ = bitField0_;
+ int to_bitField0_ = 0;
+ if (((from_bitField0_ & 0x00000001) != 0)) {
+ result.singerId_ = singerId_;
+ to_bitField0_ |= 0x00000001;
+ }
+ if (((from_bitField0_ & 0x00000002) != 0)) {
+ result.birthDate_ = birthDate_;
+ to_bitField0_ |= 0x00000002;
+ }
+ if (((from_bitField0_ & 0x00000004) != 0)) {
+ result.nationality_ = nationality_;
+ to_bitField0_ |= 0x00000004;
+ }
+ if (((from_bitField0_ & 0x00000008) != 0)) {
+ result.genre_ = genre_;
+ to_bitField0_ |= 0x00000008;
+ }
+ result.bitField0_ |= to_bitField0_;
+ }
+
+ @Override
+ public Builder clone() {
+ return super.clone();
+ }
+
+ @Override
+ public Builder setField(com.google.protobuf.Descriptors.FieldDescriptor field, Object value) {
+ return super.setField(field, value);
+ }
+
+ @Override
+ public Builder clearField(com.google.protobuf.Descriptors.FieldDescriptor field) {
+ return super.clearField(field);
+ }
+
+ @Override
+ public Builder clearOneof(com.google.protobuf.Descriptors.OneofDescriptor oneof) {
+ return super.clearOneof(oneof);
+ }
+
+ @Override
+ public Builder setRepeatedField(
+ com.google.protobuf.Descriptors.FieldDescriptor field, int index, Object value) {
+ return super.setRepeatedField(field, index, value);
+ }
+
+ @Override
+ public Builder addRepeatedField(
+ com.google.protobuf.Descriptors.FieldDescriptor field, Object value) {
+ return super.addRepeatedField(field, value);
+ }
+
+ @Override
+ public Builder mergeFrom(com.google.protobuf.Message other) {
+ if (other instanceof SingerInfo) {
+ return mergeFrom((SingerInfo) other);
+ } else {
+ super.mergeFrom(other);
+ return this;
+ }
+ }
+
+ public Builder mergeFrom(SingerInfo other) {
+ if (other == SingerInfo.getDefaultInstance()) return this;
+ if (other.hasSingerId()) {
+ setSingerId(other.getSingerId());
+ }
+ if (other.hasBirthDate()) {
+ birthDate_ = other.birthDate_;
+ bitField0_ |= 0x00000002;
+ onChanged();
+ }
+ if (other.hasNationality()) {
+ nationality_ = other.nationality_;
+ bitField0_ |= 0x00000004;
+ onChanged();
+ }
+ if (other.hasGenre()) {
+ setGenre(other.getGenre());
+ }
+ this.mergeUnknownFields(other.getUnknownFields());
+ onChanged();
+ return this;
+ }
+
+ @Override
+ public final boolean isInitialized() {
+ return true;
+ }
+
+ @Override
+ public Builder mergeFrom(
+ com.google.protobuf.CodedInputStream input,
+ com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+ throws java.io.IOException {
+ if (extensionRegistry == null) {
+ throw new NullPointerException();
+ }
+ try {
+ boolean done = false;
+ while (!done) {
+ int tag = input.readTag();
+ switch (tag) {
+ case 0:
+ done = true;
+ break;
+ case 8:
+ {
+ singerId_ = input.readInt64();
+ bitField0_ |= 0x00000001;
+ break;
+ } // case 8
+ case 18:
+ {
+ birthDate_ = input.readStringRequireUtf8();
+ bitField0_ |= 0x00000002;
+ break;
+ } // case 18
+ case 26:
+ {
+ nationality_ = input.readStringRequireUtf8();
+ bitField0_ |= 0x00000004;
+ break;
+ } // case 26
+ case 32:
+ {
+ genre_ = input.readEnum();
+ bitField0_ |= 0x00000008;
+ break;
+ } // case 32
+ default:
+ {
+ if (!super.parseUnknownField(input, extensionRegistry, tag)) {
+ done = true; // was an endgroup tag
+ }
+ break;
+ } // default:
+ } // switch (tag)
+ } // while (!done)
+ } catch (com.google.protobuf.InvalidProtocolBufferException e) {
+ throw e.unwrapIOException();
+ } finally {
+ onChanged();
+ } // finally
+ return this;
+ }
+
+ private int bitField0_;
+
+ private long singerId_;
+ /**
+ * optional int64 singer_id = 1;
+ *
+ * @return Whether the singerId field is set.
+ */
+ @Override
+ public boolean hasSingerId() {
+ return ((bitField0_ & 0x00000001) != 0);
+ }
+ /**
+ * optional int64 singer_id = 1;
+ *
+ * @return The singerId.
+ */
+ @Override
+ public long getSingerId() {
+ return singerId_;
+ }
+ /**
+ * optional int64 singer_id = 1;
+ *
+ * @param value The singerId to set.
+ * @return This builder for chaining.
+ */
+ public Builder setSingerId(long value) {
+
+ singerId_ = value;
+ bitField0_ |= 0x00000001;
+ onChanged();
+ return this;
+ }
+ /**
+ * optional int64 singer_id = 1;
+ *
+ * @return This builder for chaining.
+ */
+ public Builder clearSingerId() {
+ bitField0_ = (bitField0_ & ~0x00000001);
+ singerId_ = 0L;
+ onChanged();
+ return this;
+ }
+
+ private Object birthDate_ = "";
+ /**
+ * optional string birth_date = 2;
+ *
+ * @return Whether the birthDate field is set.
+ */
+ public boolean hasBirthDate() {
+ return ((bitField0_ & 0x00000002) != 0);
+ }
+ /**
+ * optional string birth_date = 2;
+ *
+ * @return The birthDate.
+ */
+ public String getBirthDate() {
+ Object ref = birthDate_;
+ if (!(ref instanceof String)) {
+ com.google.protobuf.ByteString bs = (com.google.protobuf.ByteString) ref;
+ String s = bs.toStringUtf8();
+ birthDate_ = s;
+ return s;
+ } else {
+ return (String) ref;
+ }
+ }
+ /**
+ * optional string birth_date = 2;
+ *
+ * @return The bytes for birthDate.
+ */
+ public com.google.protobuf.ByteString getBirthDateBytes() {
+ Object ref = birthDate_;
+ if (ref instanceof String) {
+ com.google.protobuf.ByteString b =
+ com.google.protobuf.ByteString.copyFromUtf8((String) ref);
+ birthDate_ = b;
+ return b;
+ } else {
+ return (com.google.protobuf.ByteString) ref;
+ }
+ }
+ /**
+ * optional string birth_date = 2;
+ *
+ * @param value The birthDate to set.
+ * @return This builder for chaining.
+ */
+ public Builder setBirthDate(String value) {
+ if (value == null) {
+ throw new NullPointerException();
+ }
+ birthDate_ = value;
+ bitField0_ |= 0x00000002;
+ onChanged();
+ return this;
+ }
+ /**
+ * optional string birth_date = 2;
+ *
+ * @return This builder for chaining.
+ */
+ public Builder clearBirthDate() {
+ birthDate_ = getDefaultInstance().getBirthDate();
+ bitField0_ = (bitField0_ & ~0x00000002);
+ onChanged();
+ return this;
+ }
+ /**
+ * optional string birth_date = 2;
+ *
+ * @param value The bytes for birthDate to set.
+ * @return This builder for chaining.
+ */
+ public Builder setBirthDateBytes(com.google.protobuf.ByteString value) {
+ if (value == null) {
+ throw new NullPointerException();
+ }
+ checkByteStringIsUtf8(value);
+ birthDate_ = value;
+ bitField0_ |= 0x00000002;
+ onChanged();
+ return this;
+ }
+
+ private Object nationality_ = "";
+ /**
+ * optional string nationality = 3;
+ *
+ * @return Whether the nationality field is set.
+ */
+ public boolean hasNationality() {
+ return ((bitField0_ & 0x00000004) != 0);
+ }
+ /**
+ * optional string nationality = 3;
+ *
+ * @return The nationality.
+ */
+ public String getNationality() {
+ Object ref = nationality_;
+ if (!(ref instanceof String)) {
+ com.google.protobuf.ByteString bs = (com.google.protobuf.ByteString) ref;
+ String s = bs.toStringUtf8();
+ nationality_ = s;
+ return s;
+ } else {
+ return (String) ref;
+ }
+ }
+ /**
+ * optional string nationality = 3;
+ *
+ * @return The bytes for nationality.
+ */
+ public com.google.protobuf.ByteString getNationalityBytes() {
+ Object ref = nationality_;
+ if (ref instanceof String) {
+ com.google.protobuf.ByteString b =
+ com.google.protobuf.ByteString.copyFromUtf8((String) ref);
+ nationality_ = b;
+ return b;
+ } else {
+ return (com.google.protobuf.ByteString) ref;
+ }
+ }
+ /**
+ * optional string nationality = 3;
+ *
+ * @param value The nationality to set.
+ * @return This builder for chaining.
+ */
+ public Builder setNationality(String value) {
+ if (value == null) {
+ throw new NullPointerException();
+ }
+ nationality_ = value;
+ bitField0_ |= 0x00000004;
+ onChanged();
+ return this;
+ }
+ /**
+ * optional string nationality = 3;
+ *
+ * @return This builder for chaining.
+ */
+ public Builder clearNationality() {
+ nationality_ = getDefaultInstance().getNationality();
+ bitField0_ = (bitField0_ & ~0x00000004);
+ onChanged();
+ return this;
+ }
+ /**
+ * optional string nationality = 3;
+ *
+ * @param value The bytes for nationality to set.
+ * @return This builder for chaining.
+ */
+ public Builder setNationalityBytes(com.google.protobuf.ByteString value) {
+ if (value == null) {
+ throw new NullPointerException();
+ }
+ checkByteStringIsUtf8(value);
+ nationality_ = value;
+ bitField0_ |= 0x00000004;
+ onChanged();
+ return this;
+ }
+
+ private int genre_ = 0;
+ /**
+ * optional .examples.spanner.music.Genre genre = 4;
+ *
+ * @return Whether the genre field is set.
+ */
+ @Override
+ public boolean hasGenre() {
+ return ((bitField0_ & 0x00000008) != 0);
+ }
+ /**
+ * optional .examples.spanner.music.Genre genre = 4;
+ *
+ * @return The enum numeric value on the wire for genre.
+ */
+ @Override
+ public int getGenreValue() {
+ return genre_;
+ }
+ /**
+ * optional .examples.spanner.music.Genre genre = 4;
+ *
+ * @param value The enum numeric value on the wire for genre to set.
+ * @return This builder for chaining.
+ */
+ public Builder setGenreValue(int value) {
+ genre_ = value;
+ bitField0_ |= 0x00000008;
+ onChanged();
+ return this;
+ }
+ /**
+ * optional .examples.spanner.music.Genre genre = 4;
+ *
+ * @return The genre.
+ */
+ @Override
+ public Genre getGenre() {
+ Genre result = Genre.forNumber(genre_);
+ return result == null ? Genre.UNRECOGNIZED : result;
+ }
+ /**
+ * optional .examples.spanner.music.Genre genre = 4;
+ *
+ * @param value The genre to set.
+ * @return This builder for chaining.
+ */
+ public Builder setGenre(Genre value) {
+ if (value == null) {
+ throw new NullPointerException();
+ }
+ bitField0_ |= 0x00000008;
+ genre_ = value.getNumber();
+ onChanged();
+ return this;
+ }
+ /**
+ * optional .examples.spanner.music.Genre genre = 4;
+ *
+ * @return This builder for chaining.
+ */
+ public Builder clearGenre() {
+ bitField0_ = (bitField0_ & ~0x00000008);
+ genre_ = 0;
+ onChanged();
+ return this;
+ }
+
+ @Override
+ public final Builder setUnknownFields(
+ final com.google.protobuf.UnknownFieldSet unknownFields) {
+ return super.setUnknownFields(unknownFields);
+ }
+
+ @Override
+ public final Builder mergeUnknownFields(
+ final com.google.protobuf.UnknownFieldSet unknownFields) {
+ return super.mergeUnknownFields(unknownFields);
+ }
+
+ // @@protoc_insertion_point(builder_scope:examples.spanner.music.SingerInfo)
+ }
+
+ // @@protoc_insertion_point(class_scope:examples.spanner.music.SingerInfo)
+ private static final SingerInfo DEFAULT_INSTANCE;
+
+ static {
+ DEFAULT_INSTANCE = new SingerInfo();
+ }
+
+ public static SingerInfo getDefaultInstance() {
+ return DEFAULT_INSTANCE;
+ }
+
+ private static final com.google.protobuf.Parser PARSER =
+ new com.google.protobuf.AbstractParser() {
+ @Override
+ public SingerInfo parsePartialFrom(
+ com.google.protobuf.CodedInputStream input,
+ com.google.protobuf.ExtensionRegistryLite extensionRegistry)
+ throws com.google.protobuf.InvalidProtocolBufferException {
+ Builder builder = newBuilder();
+ try {
+ builder.mergeFrom(input, extensionRegistry);
+ } catch (com.google.protobuf.InvalidProtocolBufferException e) {
+ throw e.setUnfinishedMessage(builder.buildPartial());
+ } catch (com.google.protobuf.UninitializedMessageException e) {
+ throw e.asInvalidProtocolBufferException()
+ .setUnfinishedMessage(builder.buildPartial());
+ } catch (java.io.IOException e) {
+ throw new com.google.protobuf.InvalidProtocolBufferException(e)
+ .setUnfinishedMessage(builder.buildPartial());
+ }
+ return builder.buildPartial();
+ }
+ };
+
+ public static com.google.protobuf.Parser parser() {
+ return PARSER;
+ }
+
+ @Override
+ public com.google.protobuf.Parser getParserForType() {
+ return PARSER;
+ }
+
+ @Override
+ public SingerInfo getDefaultInstanceForType() {
+ return DEFAULT_INSTANCE;
+ }
+ }
+
+ private static final com.google.protobuf.Descriptors.Descriptor
+ internal_static_examples_spanner_music_SingerInfo_descriptor;
+ private static final com.google.protobuf.GeneratedMessageV3.FieldAccessorTable
+ internal_static_examples_spanner_music_SingerInfo_fieldAccessorTable;
+
+ public static com.google.protobuf.Descriptors.FileDescriptor getDescriptor() {
+ return descriptor;
+ }
+
+ private static com.google.protobuf.Descriptors.FileDescriptor descriptor;
+
+ static {
+ String[] descriptorData = {
+ "\n\014singer.proto\022\026examples.spanner.music\"\301"
+ + "\001\n\nSingerInfo\022\026\n\tsinger_id\030\001 \001(\003H\000\210\001\001\022\027\n"
+ + "\nbirth_date\030\002 \001(\tH\001\210\001\001\022\030\n\013nationality\030\003 "
+ + "\001(\tH\002\210\001\001\0221\n\005genre\030\004 \001(\0162\035.examples.spann"
+ + "er.music.GenreH\003\210\001\001B\014\n\n_singer_idB\r\n\013_bi"
+ + "rth_dateB\016\n\014_nationalityB\010\n\006_genre*.\n\005Ge"
+ + "nre\022\007\n\003POP\020\000\022\010\n\004JAZZ\020\001\022\010\n\004FOLK\020\002\022\010\n\004ROCK"
+ + "\020\003B$\n\023com.example.spannerB\013SingerProtoP\000"
+ + "b\006proto3"
+ };
+ descriptor =
+ com.google.protobuf.Descriptors.FileDescriptor.internalBuildGeneratedFileFrom(
+ descriptorData, new com.google.protobuf.Descriptors.FileDescriptor[] {});
+ internal_static_examples_spanner_music_SingerInfo_descriptor =
+ getDescriptor().getMessageTypes().get(0);
+ internal_static_examples_spanner_music_SingerInfo_fieldAccessorTable =
+ new com.google.protobuf.GeneratedMessageV3.FieldAccessorTable(
+ internal_static_examples_spanner_music_SingerInfo_descriptor,
+ new String[] {
+ "SingerId", "BirthDate", "Nationality", "Genre",
+ });
+ }
+
+ // @@protoc_insertion_point(outer_class_scope)
+}
diff --git a/samples/snippets/src/main/java/com/example/spanner/UpdateProtoDataSample.java b/samples/snippets/src/main/java/com/example/spanner/UpdateProtoDataSample.java
new file mode 100644
index 00000000000..37712a27dc3
--- /dev/null
+++ b/samples/snippets/src/main/java/com/example/spanner/UpdateProtoDataSample.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2024 Google Inc.
+ *
+ * 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.example.spanner;
+
+// [START spanner_update_data_with_proto_types]
+
+import com.example.spanner.SingerProto.Genre;
+import com.example.spanner.SingerProto.SingerInfo;
+import com.google.cloud.spanner.DatabaseClient;
+import com.google.cloud.spanner.DatabaseId;
+import com.google.cloud.spanner.Mutation;
+import com.google.cloud.spanner.Spanner;
+import com.google.cloud.spanner.SpannerOptions;
+import com.google.common.collect.ImmutableList;
+import com.google.protobuf.AbstractMessage;
+import com.google.protobuf.ProtocolMessageEnum;
+import java.util.Collections;
+import java.util.List;
+
+class UpdateProtoDataSample {
+
+ static void updateProtoData() {
+ // TODO(developer): Replace these variables before running the sample.
+ String projectId = "my-project";
+ String instanceId = "my-instance";
+ String databaseId = "my-database";
+
+ try (Spanner spanner =
+ SpannerOptions.newBuilder().setProjectId(projectId).build().getService()) {
+ DatabaseClient client =
+ spanner.getDatabaseClient(DatabaseId.of(projectId, instanceId, databaseId));
+ updateProtoData(client);
+ }
+ }
+
+ static void updateProtoData(DatabaseClient client) {
+ SingerInfo singerInfo =
+ SingerInfo.newBuilder()
+ .setSingerId(2)
+ .setBirthDate("February")
+ .setNationality("Country2")
+ .setGenre(Genre.FOLK)
+ .build();
+ Genre singerGenre = Genre.FOLK;
+ List singerInfoList = Collections.singletonList(singerInfo);
+ List singerGenreList = Collections.singletonList(singerGenre);
+
+ client.write(
+ ImmutableList.of(
+ Mutation.newInsertOrUpdateBuilder("Singers")
+ .set("SingerId")
+ .to(2L)
+ .set("SingerInfo")
+ .to(singerInfo)
+ .set("SingerInfoArray")
+ .toProtoMessageArray(singerInfoList, SingerInfo.getDescriptor())
+ .set("SingerGenre")
+ .to(singerGenre)
+ .set("SingerGenreArray")
+ .toProtoEnumArray(singerGenreList, Genre.getDescriptor())
+ .build(),
+ Mutation.newInsertOrUpdateBuilder("Singers")
+ .set("SingerId")
+ .to(3L)
+ .set("SingerInfo")
+ .to(null, SingerInfo.getDescriptor())
+ .set("SingerInfoArray")
+ .toProtoMessageArray(null, SingerInfo.getDescriptor())
+ .set("SingerGenre")
+ .to(null, Genre.getDescriptor())
+ .set("SingerGenreArray")
+ .toProtoEnumArray(null, Genre.getDescriptor())
+ .build()));
+ System.out.println("Data updated");
+ }
+}
+// [END spanner_update_data_with_proto_types]
diff --git a/samples/snippets/src/main/java/com/example/spanner/UpdateProtoDataSampleUsingDml.java b/samples/snippets/src/main/java/com/example/spanner/UpdateProtoDataSampleUsingDml.java
new file mode 100644
index 00000000000..9b85f774eb7
--- /dev/null
+++ b/samples/snippets/src/main/java/com/example/spanner/UpdateProtoDataSampleUsingDml.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2024 Google Inc.
+ *
+ * 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.example.spanner;
+
+// [START spanner_update_data_with_proto_types_with_dml]
+
+import com.example.spanner.SingerProto.Genre;
+import com.example.spanner.SingerProto.SingerInfo;
+import com.google.cloud.spanner.DatabaseClient;
+import com.google.cloud.spanner.DatabaseId;
+import com.google.cloud.spanner.Spanner;
+import com.google.cloud.spanner.SpannerOptions;
+import com.google.cloud.spanner.Statement;
+import com.google.protobuf.AbstractMessage;
+import com.google.protobuf.ProtocolMessageEnum;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+class UpdateProtoDataSampleUsingDml {
+
+ static void updateProtoDataUsingDml() {
+ // TODO(developer): Replace these variables before running the sample.
+ String projectId = "my-project";
+ String instanceId = "my-instance";
+ String databaseId = "my-database";
+
+ try (Spanner spanner =
+ SpannerOptions.newBuilder().setProjectId(projectId).build().getService()) {
+ DatabaseClient client =
+ spanner.getDatabaseClient(DatabaseId.of(projectId, instanceId, databaseId));
+ updateProtoDataUsingDml(client);
+ }
+ }
+
+ static void updateProtoDataUsingDml(DatabaseClient client) {
+ SingerInfo singerInfo =
+ SingerInfo.newBuilder()
+ .setSingerId(1)
+ .setBirthDate("January")
+ .setNationality("Country1")
+ .setGenre(Genre.ROCK)
+ .build();
+ Genre singerGenre = Genre.ROCK;
+ List singerInfoList = Collections.singletonList(singerInfo);
+ List singerGenreList = Collections.singletonList(singerGenre);
+
+ client
+ .readWriteTransaction()
+ .run(
+ transaction -> {
+ Statement statement1 =
+ Statement.newBuilder(
+ "UPDATE Singers SET SingerInfo = @singerInfo, "
+ + "SingerInfoArray=@singerInfoArray, "
+ + "SingerGenre=@singerGenre, SingerGenreArray=@singerGenreArray "
+ + "WHERE SingerId = 1")
+ .bind("singerInfo")
+ .to(singerInfo)
+ .bind("singerInfoArray")
+ .toProtoMessageArray(singerInfoList, SingerInfo.getDescriptor())
+ .bind("singerGenre")
+ .to(singerGenre)
+ .bind("singerGenreArray")
+ .toProtoEnumArray(singerGenreList, Genre.getDescriptor())
+ .build();
+
+ Statement statement2 =
+ Statement.newBuilder(
+ "UPDATE Singers SET SingerInfo.nationality = @singerNationality "
+ + "WHERE SingerId = 1")
+ .bind("singerNationality")
+ .to("Country2")
+ .build();
+
+ transaction.batchUpdate(Arrays.asList(statement1, statement2));
+ return null;
+ });
+
+ System.out.println("record(s) updated");
+ }
+}
+// [END spanner_update_data_with_proto_types_with_dml]
diff --git a/samples/snippets/src/main/resources/com/example/spanner/README.md b/samples/snippets/src/main/resources/com/example/spanner/README.md
new file mode 100644
index 00000000000..6dc4f7aa59f
--- /dev/null
+++ b/samples/snippets/src/main/resources/com/example/spanner/README.md
@@ -0,0 +1,6 @@
+#### To generate SingerProto.java and descriptors.pb file from singer.proto using `protoc`
+```shell
+cd samples/snippets/src/main/resources/
+protoc --proto_path=com/example/spanner/ --include_imports --descriptor_set_out=com/example/spanner/descriptors.pb
+ --java_out=. com/example/spanner/singer.proto
+```
diff --git a/samples/snippets/src/main/resources/com/example/spanner/descriptors.pb b/samples/snippets/src/main/resources/com/example/spanner/descriptors.pb
new file mode 100644
index 00000000000..dd9cf8d4344
Binary files /dev/null and b/samples/snippets/src/main/resources/com/example/spanner/descriptors.pb differ
diff --git a/samples/snippets/src/main/resources/com/example/spanner/singer.proto b/samples/snippets/src/main/resources/com/example/spanner/singer.proto
new file mode 100644
index 00000000000..12b213f3fae
--- /dev/null
+++ b/samples/snippets/src/main/resources/com/example/spanner/singer.proto
@@ -0,0 +1,21 @@
+syntax = "proto3";
+
+package examples.spanner.music;
+
+option java_package = "com.example.spanner";
+option java_outer_classname = "SingerProto";
+option java_multiple_files = false;
+
+message SingerInfo {
+ optional int64 singer_id = 1;
+ optional string birth_date = 2;
+ optional string nationality = 3;
+ optional Genre genre = 4;
+}
+
+enum Genre {
+ POP = 0;
+ JAZZ = 1;
+ FOLK = 2;
+ ROCK = 3;
+}
diff --git a/samples/snippets/src/test/java/com/example/spanner/ProtoColumnSampleIT.java b/samples/snippets/src/test/java/com/example/spanner/ProtoColumnSampleIT.java
new file mode 100644
index 00000000000..ba59f7b2384
--- /dev/null
+++ b/samples/snippets/src/test/java/com/example/spanner/ProtoColumnSampleIT.java
@@ -0,0 +1,129 @@
+/*
+ * 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.example.spanner;
+
+import static com.example.spanner.SampleRunner.runSample;
+import static org.junit.Assert.assertTrue;
+
+import com.google.cloud.spanner.DatabaseAdminClient;
+import com.google.cloud.spanner.DatabaseClient;
+import com.google.cloud.spanner.DatabaseId;
+import com.google.cloud.spanner.KeySet;
+import com.google.cloud.spanner.Mutation;
+import com.google.cloud.spanner.Spanner;
+import com.google.cloud.spanner.SpannerOptions;
+import com.google.common.collect.ImmutableList;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.concurrent.TimeUnit;
+import org.junit.After;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Integration tests for Proto Column type */
+@RunWith(JUnit4.class)
+public class ProtoColumnSampleIT extends SampleTestBase {
+
+ private static DatabaseId databaseId;
+ private static Spanner spanner;
+
+ @BeforeClass
+ public static void createTestDatabase() throws Exception {
+ spanner = SpannerOptions.newBuilder().setProjectId(projectId).build().getService();
+ DatabaseAdminClient databaseAdminClient = spanner.getDatabaseAdminClient();
+ final String database = idGenerator.generateDatabaseId();
+ databaseAdminClient
+ .createDatabase(
+ instanceId,
+ database,
+ ImmutableList.of(
+ "CREATE TABLE Singers (\n"
+ + " SingerId INT64 NOT NULL,\n"
+ + " FirstName STRING(1024),\n"
+ + " LastName STRING(1024),\n"
+ + " ) PRIMARY KEY (SingerId)"))
+ .get(10, TimeUnit.MINUTES);
+ databaseId = DatabaseId.of(projectId, instanceId, database);
+
+ final DatabaseClient client = spanner.getDatabaseClient(databaseId);
+ client.write(
+ Arrays.asList(
+ Mutation.newInsertOrUpdateBuilder("Singers")
+ .set("SingerId")
+ .to(1L)
+ .set("FirstName")
+ .to("Marc")
+ .set("LastName")
+ .to("Richards")
+ .build(),
+ Mutation.newInsertOrUpdateBuilder("Singers")
+ .set("SingerId")
+ .to(2L)
+ .set("FirstName")
+ .to("Catalina")
+ .set("LastName")
+ .to("Smith")
+ .build(),
+ Mutation.newInsertOrUpdateBuilder("Singers")
+ .set("SingerId")
+ .to(3L)
+ .set("FirstName")
+ .to("Alice")
+ .set("LastName")
+ .to("Trentor")
+ .build()));
+ }
+
+ @After
+ public void removeTestData() {
+ final DatabaseClient client = spanner.getDatabaseClient(databaseId);
+ client.write(Collections.singletonList(Mutation.delete("Singers", KeySet.all())));
+ }
+
+ @Test
+ public void testProtoColumns() throws Exception {
+ InputStream in =
+ ProtoColumnSampleIT.class
+ .getClassLoader()
+ .getResourceAsStream("com/example/spanner/descriptors.pb");
+ System.out.println(in);
+ final DatabaseClient client = spanner.getDatabaseClient(databaseId);
+ System.out.println(databaseId.toString());
+ System.out.println("Adding Proto columns schema to table ...");
+ String out =
+ runSample(
+ () ->
+ AddProtoColumnSample.addProtoColumn(
+ projectId, instanceId, databaseId.getDatabase()));
+ assertTrue(out.contains("Added Proto columns"));
+
+ System.out.println("Update data with Proto Columns ...");
+ out = runSample(() -> UpdateProtoDataSample.updateProtoData(client));
+ assertTrue(out.contains("Data updated"));
+
+ System.out.println("Update data with Proto Columns using DML ...");
+ out = runSample(() -> UpdateProtoDataSampleUsingDml.updateProtoDataUsingDml(client));
+ assertTrue(out.contains("record(s) updated"));
+
+ System.out.println("Query data with Proto Columns ...");
+ out = runSample(() -> QueryWithProtoParameterSample.queryWithProtoParameter(client));
+ assertTrue(out.contains("2 singer_id: 2"));
+ }
+}
diff --git a/versions.txt b/versions.txt
index 0f6c4274453..30323409a7b 100644
--- a/versions.txt
+++ b/versions.txt
@@ -1,13 +1,13 @@
# Format:
# module:released-version:current-version
-proto-google-cloud-spanner-admin-instance-v1:6.66.0:6.66.0
-proto-google-cloud-spanner-v1:6.66.0:6.66.0
-proto-google-cloud-spanner-admin-database-v1:6.66.0:6.66.0
-grpc-google-cloud-spanner-v1:6.66.0:6.66.0
-grpc-google-cloud-spanner-admin-instance-v1:6.66.0:6.66.0
-grpc-google-cloud-spanner-admin-database-v1:6.66.0:6.66.0
-google-cloud-spanner:6.66.0:6.66.0
-google-cloud-spanner-executor:6.66.0:6.66.0
-proto-google-cloud-spanner-executor-v1:6.66.0:6.66.0
-grpc-google-cloud-spanner-executor-v1:6.66.0:6.66.0
+proto-google-cloud-spanner-admin-instance-v1:6.67.0:6.67.0
+proto-google-cloud-spanner-v1:6.67.0:6.67.0
+proto-google-cloud-spanner-admin-database-v1:6.67.0:6.67.0
+grpc-google-cloud-spanner-v1:6.67.0:6.67.0
+grpc-google-cloud-spanner-admin-instance-v1:6.67.0:6.67.0
+grpc-google-cloud-spanner-admin-database-v1:6.67.0:6.67.0
+google-cloud-spanner:6.67.0:6.67.0
+google-cloud-spanner-executor:6.67.0:6.67.0
+proto-google-cloud-spanner-executor-v1:6.67.0:6.67.0
+grpc-google-cloud-spanner-executor-v1:6.67.0:6.67.0