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.cloud google-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.cloud google-cloud-spanner-parent - 6.66.0 + 6.67.0 @@ -92,7 +92,7 @@ com.google.cloud google-cloud-spanner - 6.65.1 + 6.66.0 commons-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.0 com.google.cloud google-cloud-spanner-bom - 6.66.0 + 6.67.0 pom com.google.cloud sdk-platform-java-config - 3.30.0 + 3.30.1 Google Cloud Spanner BOM @@ -53,43 +53,43 @@ com.google.cloud google-cloud-spanner - 6.66.0 + 6.67.0 com.google.cloud google-cloud-spanner test-jar - 6.66.0 + 6.67.0 com.google.api.grpc grpc-google-cloud-spanner-v1 - 6.66.0 + 6.67.0 com.google.api.grpc grpc-google-cloud-spanner-admin-instance-v1 - 6.66.0 + 6.67.0 com.google.api.grpc grpc-google-cloud-spanner-admin-database-v1 - 6.66.0 + 6.67.0 com.google.api.grpc proto-google-cloud-spanner-admin-instance-v1 - 6.66.0 + 6.67.0 com.google.api.grpc proto-google-cloud-spanner-v1 - 6.66.0 + 6.67.0 com.google.api.grpc proto-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.0 com.google.cloud google-cloud-spanner-executor - 6.66.0 + 6.67.0 jar Google Cloud Spanner Executor com.google.cloud google-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/Connection com.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.0 com.google.cloud google-cloud-spanner - 6.66.0 + 6.67.0 jar Google Cloud Spanner https://github.com/googleapis/java-spanner @@ -11,12 +11,11 @@ com.google.cloud google-cloud-spanner-parent - 6.66.0 + 6.67.0 google-cloud-spanner 0.31.1 - 1.37.0 com.google.cloud.spanner.GceTestEnvConfig projects/gcloud-devel/instances/spanner-testing-east1 gcloud-devel @@ -414,25 +413,26 @@ io.opentelemetry opentelemetry-sdk - ${opentelemetry.version} + test + + + io.opentelemetry + opentelemetry-sdk-common test io.opentelemetry opentelemetry-sdk-metrics - ${opentelemetry.version} test io.opentelemetry opentelemetry-sdk-trace - ${opentelemetry.version} test io.opentelemetry opentelemetry-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 data() { @Mock private SpannerRpc rpc; @Mock private SpannerOptions spannerOptions; private final TraceWrapper tracer = - new TraceWrapper(Tracing.getTracer(), OpenTelemetry.noop().getTracer("")); + new TraceWrapper(Tracing.getTracer(), OpenTelemetry.noop().getTracer(""), false); @Mock private ISpan span; @Captor ArgumentCaptor> options; diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolMaintainerTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolMaintainerTest.java index 9e55851ef4d..db4e79113fc 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolMaintainerTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolMaintainerTest.java @@ -134,7 +134,7 @@ private SessionPool createPool(SessionPoolOptions options) throws Exception { client.getSessionClient(db), clock, Position.FIRST, - new TraceWrapper(Tracing.getTracer(), OpenTelemetry.noop().getTracer("")), + new TraceWrapper(Tracing.getTracer(), OpenTelemetry.noop().getTracer(""), false), OpenTelemetry.noop()); pool.idleSessionRemovedListener = input -> { diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolStressTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolStressTest.java index 6d2d1f19efe..33771962828 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolStressTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolStressTest.java @@ -228,7 +228,7 @@ public void stressTest() throws Exception { mockSpanner.getSessionClient(db), clock, Position.RANDOM, - new TraceWrapper(Tracing.getTracer(), OpenTelemetry.noop().getTracer("")), + new TraceWrapper(Tracing.getTracer(), OpenTelemetry.noop().getTracer(""), false), OpenTelemetry.noop()); pool.idleSessionRemovedListener = pooled -> { diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolTest.java index ab7eb80cf90..8ffc4f21a10 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolTest.java @@ -153,7 +153,7 @@ public class SessionPoolTest extends BaseSessionPoolTest { private String TEST_DATABASE_ROLE = "my-role"; private final TraceWrapper tracer = - new TraceWrapper(Tracing.getTracer(), OpenTelemetry.noop().getTracer("")); + new TraceWrapper(Tracing.getTracer(), OpenTelemetry.noop().getTracer(""), false); @Parameters(name = "min sessions = {0}") public static Collection data() { diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SingerProto.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SingerProto.java index 56fb82c15d2..c409f34177b 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SingerProto.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SingerProto.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 Google LLC + * 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. @@ -16,6 +16,8 @@ // Generated by the protocol buffer compiler. DO NOT EDIT! // source: src/test/resources/com/google/cloud/spanner/singer.proto + +// Protobuf Java Version: 3.25.1 package com.google.cloud.spanner; public final class SingerProto { @@ -26,7 +28,7 @@ public static void registerAllExtensions(com.google.protobuf.ExtensionRegistryLi public static void registerAllExtensions(com.google.protobuf.ExtensionRegistry registry) { registerAllExtensions((com.google.protobuf.ExtensionRegistryLite) registry); } - /** Protobuf enum {@code spanner.examples.music.Genre} */ + /** Protobuf enum {@code examples.spanner.music.Genre} */ public enum Genre implements com.google.protobuf.ProtocolMessageEnum { /** POP = 0; */ POP(0), @@ -36,6 +38,7 @@ public enum Genre implements com.google.protobuf.ProtocolMessageEnum { FOLK(2), /** ROCK = 3; */ ROCK(3), + UNRECOGNIZED(-1), ; /** POP = 0; */ @@ -48,6 +51,9 @@ public enum Genre implements com.google.protobuf.ProtocolMessageEnum { 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; } @@ -56,7 +62,7 @@ public final int getNumber() { * @return The enum associated with the given numeric wire value. * @deprecated Use {@link #forNumber(int)} instead. */ - @java.lang.Deprecated + @Deprecated public static Genre valueOf(int value) { return forNumber(value); } @@ -92,6 +98,9 @@ public Genre findValueByNumber(int 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()); } @@ -100,14 +109,17 @@ public final com.google.protobuf.Descriptors.EnumDescriptor getDescriptorForType } public static final com.google.protobuf.Descriptors.EnumDescriptor getDescriptor() { - return com.google.cloud.spanner.SingerProto.getDescriptor().getEnumTypes().get(0); + 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 java.lang.IllegalArgumentException("EnumValueDescriptor is not for this type."); + throw new IllegalArgumentException("EnumValueDescriptor is not for this type."); + } + if (desc.getIndex() == -1) { + return UNRECOGNIZED; } return VALUES[desc.getIndex()]; } @@ -118,12 +130,12 @@ private Genre(int value) { this.value = value; } - // @@protoc_insertion_point(enum_scope:spanner.examples.music.Genre) + // @@protoc_insertion_point(enum_scope:examples.spanner.music.Genre) } public interface SingerInfoOrBuilder extends - // @@protoc_insertion_point(interface_extends:spanner.examples.music.SingerInfo) + // @@protoc_insertion_point(interface_extends:examples.spanner.music.SingerInfo) com.google.protobuf.MessageOrBuilder { /** @@ -150,7 +162,7 @@ public interface SingerInfoOrBuilder * * @return The birthDate. */ - java.lang.String getBirthDate(); + String getBirthDate(); /** * optional string birth_date = 2; * @@ -169,7 +181,7 @@ public interface SingerInfoOrBuilder * * @return The nationality. */ - java.lang.String getNationality(); + String getNationality(); /** * optional string nationality = 3; * @@ -178,22 +190,28 @@ public interface SingerInfoOrBuilder com.google.protobuf.ByteString getNationalityBytes(); /** - * optional .spanner.examples.music.Genre genre = 4; + * optional .examples.spanner.music.Genre genre = 4; * * @return Whether the genre field is set. */ boolean hasGenre(); /** - * optional .spanner.examples.music.Genre genre = 4; + * 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. */ - com.google.cloud.spanner.SingerProto.Genre getGenre(); + Genre getGenre(); } - /** Protobuf type {@code spanner.examples.music.SingerInfo} */ + /** Protobuf type {@code examples.spanner.music.SingerInfo} */ public static final class SingerInfo extends com.google.protobuf.GeneratedMessageV3 implements - // @@protoc_insertion_point(message_implements:spanner.examples.music.SingerInfo) + // @@protoc_insertion_point(message_implements:examples.spanner.music.SingerInfo) SingerInfoOrBuilder { private static final long serialVersionUID = 0L; // Use SingerInfo.newBuilder() to construct. @@ -207,113 +225,31 @@ private SingerInfo() { genre_ = 0; } - @java.lang.Override + @Override @SuppressWarnings({"unused"}) - protected java.lang.Object newInstance(UnusedPrivateParameter unused) { + protected Object newInstance(UnusedPrivateParameter unused) { return new SingerInfo(); } - @java.lang.Override - public final com.google.protobuf.UnknownFieldSet getUnknownFields() { - return this.unknownFields; - } - - private SingerInfo( - com.google.protobuf.CodedInputStream input, - com.google.protobuf.ExtensionRegistryLite extensionRegistry) - throws com.google.protobuf.InvalidProtocolBufferException { - this(); - if (extensionRegistry == null) { - throw new java.lang.NullPointerException(); - } - int mutable_bitField0_ = 0; - com.google.protobuf.UnknownFieldSet.Builder unknownFields = - com.google.protobuf.UnknownFieldSet.newBuilder(); - try { - boolean done = false; - while (!done) { - int tag = input.readTag(); - switch (tag) { - case 0: - done = true; - break; - case 8: - { - bitField0_ |= 0x00000001; - singerId_ = input.readInt64(); - break; - } - case 18: - { - com.google.protobuf.ByteString bs = input.readBytes(); - bitField0_ |= 0x00000002; - birthDate_ = bs; - break; - } - case 26: - { - com.google.protobuf.ByteString bs = input.readBytes(); - bitField0_ |= 0x00000004; - nationality_ = bs; - break; - } - case 32: - { - int rawValue = input.readEnum(); - @SuppressWarnings("deprecation") - com.google.cloud.spanner.SingerProto.Genre value = - com.google.cloud.spanner.SingerProto.Genre.valueOf(rawValue); - if (value == null) { - unknownFields.mergeVarintField(4, rawValue); - } else { - bitField0_ |= 0x00000008; - genre_ = rawValue; - } - break; - } - default: - { - if (!parseUnknownField(input, unknownFields, extensionRegistry, tag)) { - done = true; - } - break; - } - } - } - } catch (com.google.protobuf.InvalidProtocolBufferException e) { - throw e.setUnfinishedMessage(this); - } catch (java.io.IOException e) { - throw new com.google.protobuf.InvalidProtocolBufferException(e).setUnfinishedMessage(this); - } finally { - this.unknownFields = unknownFields.build(); - makeExtensionsImmutable(); - } - } - public static final com.google.protobuf.Descriptors.Descriptor getDescriptor() { - return com.google.cloud.spanner.SingerProto - .internal_static_spanner_examples_music_SingerInfo_descriptor; + return SingerProto.internal_static_examples_spanner_music_SingerInfo_descriptor; } - @java.lang.Override - protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable - internalGetFieldAccessorTable() { - return com.google.cloud.spanner.SingerProto - .internal_static_spanner_examples_music_SingerInfo_fieldAccessorTable - .ensureFieldAccessorsInitialized( - com.google.cloud.spanner.SingerProto.SingerInfo.class, - com.google.cloud.spanner.SingerProto.SingerInfo.Builder.class); + @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_; + private long singerId_ = 0L; /** * optional int64 singer_id = 1; * * @return Whether the singerId field is set. */ - @java.lang.Override + @Override public boolean hasSingerId() { return ((bitField0_ & 0x00000001) != 0); } @@ -322,19 +258,21 @@ public boolean hasSingerId() { * * @return The singerId. */ - @java.lang.Override + @Override public long getSingerId() { return singerId_; } public static final int BIRTH_DATE_FIELD_NUMBER = 2; - private volatile java.lang.Object birthDate_; + + @SuppressWarnings("serial") + private volatile Object birthDate_ = ""; /** * optional string birth_date = 2; * * @return Whether the birthDate field is set. */ - @java.lang.Override + @Override public boolean hasBirthDate() { return ((bitField0_ & 0x00000002) != 0); } @@ -343,17 +281,15 @@ public boolean hasBirthDate() { * * @return The birthDate. */ - @java.lang.Override - public java.lang.String getBirthDate() { - java.lang.Object ref = birthDate_; - if (ref instanceof java.lang.String) { - return (java.lang.String) ref; + @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; - java.lang.String s = bs.toStringUtf8(); - if (bs.isValidUtf8()) { - birthDate_ = s; - } + String s = bs.toStringUtf8(); + birthDate_ = s; return s; } } @@ -362,12 +298,12 @@ public java.lang.String getBirthDate() { * * @return The bytes for birthDate. */ - @java.lang.Override + @Override public com.google.protobuf.ByteString getBirthDateBytes() { - java.lang.Object ref = birthDate_; - if (ref instanceof java.lang.String) { + Object ref = birthDate_; + if (ref instanceof String) { com.google.protobuf.ByteString b = - com.google.protobuf.ByteString.copyFromUtf8((java.lang.String) ref); + com.google.protobuf.ByteString.copyFromUtf8((String) ref); birthDate_ = b; return b; } else { @@ -376,13 +312,15 @@ public com.google.protobuf.ByteString getBirthDateBytes() { } public static final int NATIONALITY_FIELD_NUMBER = 3; - private volatile java.lang.Object nationality_; + + @SuppressWarnings("serial") + private volatile Object nationality_ = ""; /** * optional string nationality = 3; * * @return Whether the nationality field is set. */ - @java.lang.Override + @Override public boolean hasNationality() { return ((bitField0_ & 0x00000004) != 0); } @@ -391,17 +329,15 @@ public boolean hasNationality() { * * @return The nationality. */ - @java.lang.Override - public java.lang.String getNationality() { - java.lang.Object ref = nationality_; - if (ref instanceof java.lang.String) { - return (java.lang.String) ref; + @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; - java.lang.String s = bs.toStringUtf8(); - if (bs.isValidUtf8()) { - nationality_ = s; - } + String s = bs.toStringUtf8(); + nationality_ = s; return s; } } @@ -410,12 +346,12 @@ public java.lang.String getNationality() { * * @return The bytes for nationality. */ - @java.lang.Override + @Override public com.google.protobuf.ByteString getNationalityBytes() { - java.lang.Object ref = nationality_; - if (ref instanceof java.lang.String) { + Object ref = nationality_; + if (ref instanceof String) { com.google.protobuf.ByteString b = - com.google.protobuf.ByteString.copyFromUtf8((java.lang.String) ref); + com.google.protobuf.ByteString.copyFromUtf8((String) ref); nationality_ = b; return b; } else { @@ -424,32 +360,39 @@ public com.google.protobuf.ByteString getNationalityBytes() { } public static final int GENRE_FIELD_NUMBER = 4; - private int genre_; + private int genre_ = 0; /** - * optional .spanner.examples.music.Genre genre = 4; + * optional .examples.spanner.music.Genre genre = 4; * * @return Whether the genre field is set. */ - @java.lang.Override + @Override public boolean hasGenre() { return ((bitField0_ & 0x00000008) != 0); } /** - * optional .spanner.examples.music.Genre genre = 4; + * 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. */ - @java.lang.Override - public com.google.cloud.spanner.SingerProto.Genre getGenre() { - @SuppressWarnings("deprecation") - com.google.cloud.spanner.SingerProto.Genre result = - com.google.cloud.spanner.SingerProto.Genre.valueOf(genre_); - return result == null ? com.google.cloud.spanner.SingerProto.Genre.POP : result; + @Override + public Genre getGenre() { + Genre result = Genre.forNumber(genre_); + return result == null ? Genre.UNRECOGNIZED : result; } private byte memoizedIsInitialized = -1; - @java.lang.Override + @Override public final boolean isInitialized() { byte isInitialized = memoizedIsInitialized; if (isInitialized == 1) return true; @@ -459,7 +402,7 @@ public final boolean isInitialized() { return true; } - @java.lang.Override + @Override public void writeTo(com.google.protobuf.CodedOutputStream output) throws java.io.IOException { if (((bitField0_ & 0x00000001) != 0)) { output.writeInt64(1, singerId_); @@ -473,10 +416,10 @@ public void writeTo(com.google.protobuf.CodedOutputStream output) throws java.io if (((bitField0_ & 0x00000008) != 0)) { output.writeEnum(4, genre_); } - unknownFields.writeTo(output); + getUnknownFields().writeTo(output); } - @java.lang.Override + @Override public int getSerializedSize() { int size = memoizedSize; if (size != -1) return size; @@ -494,21 +437,20 @@ public int getSerializedSize() { if (((bitField0_ & 0x00000008) != 0)) { size += com.google.protobuf.CodedOutputStream.computeEnumSize(4, genre_); } - size += unknownFields.getSerializedSize(); + size += getUnknownFields().getSerializedSize(); memoizedSize = size; return size; } - @java.lang.Override - public boolean equals(final java.lang.Object obj) { + @Override + public boolean equals(final Object obj) { if (obj == this) { return true; } - if (!(obj instanceof com.google.cloud.spanner.SingerProto.SingerInfo)) { + if (!(obj instanceof SingerInfo)) { return super.equals(obj); } - com.google.cloud.spanner.SingerProto.SingerInfo other = - (com.google.cloud.spanner.SingerProto.SingerInfo) obj; + SingerInfo other = (SingerInfo) obj; if (hasSingerId() != other.hasSingerId()) return false; if (hasSingerId()) { @@ -526,11 +468,11 @@ public boolean equals(final java.lang.Object obj) { if (hasGenre()) { if (genre_ != other.genre_) return false; } - if (!unknownFields.equals(other.unknownFields)) return false; + if (!getUnknownFields().equals(other.getUnknownFields())) return false; return true; } - @java.lang.Override + @Override public int hashCode() { if (memoizedHashCode != 0) { return memoizedHashCode; @@ -553,76 +495,74 @@ public int hashCode() { hash = (37 * hash) + GENRE_FIELD_NUMBER; hash = (53 * hash) + genre_; } - hash = (29 * hash) + unknownFields.hashCode(); + hash = (29 * hash) + getUnknownFields().hashCode(); memoizedHashCode = hash; return hash; } - public static com.google.cloud.spanner.SingerProto.SingerInfo parseFrom( - java.nio.ByteBuffer data) throws com.google.protobuf.InvalidProtocolBufferException { + public static SingerInfo parseFrom(java.nio.ByteBuffer data) + throws com.google.protobuf.InvalidProtocolBufferException { return PARSER.parseFrom(data); } - public static com.google.cloud.spanner.SingerProto.SingerInfo parseFrom( + 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 com.google.cloud.spanner.SingerProto.SingerInfo parseFrom( - com.google.protobuf.ByteString data) + public static SingerInfo parseFrom(com.google.protobuf.ByteString data) throws com.google.protobuf.InvalidProtocolBufferException { return PARSER.parseFrom(data); } - public static com.google.cloud.spanner.SingerProto.SingerInfo parseFrom( + 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 com.google.cloud.spanner.SingerProto.SingerInfo parseFrom(byte[] data) + public static SingerInfo parseFrom(byte[] data) throws com.google.protobuf.InvalidProtocolBufferException { return PARSER.parseFrom(data); } - public static com.google.cloud.spanner.SingerProto.SingerInfo parseFrom( + public static SingerInfo parseFrom( byte[] data, com.google.protobuf.ExtensionRegistryLite extensionRegistry) throws com.google.protobuf.InvalidProtocolBufferException { return PARSER.parseFrom(data, extensionRegistry); } - public static com.google.cloud.spanner.SingerProto.SingerInfo parseFrom( - java.io.InputStream input) throws java.io.IOException { + public static SingerInfo parseFrom(java.io.InputStream input) throws java.io.IOException { return com.google.protobuf.GeneratedMessageV3.parseWithIOException(PARSER, input); } - public static com.google.cloud.spanner.SingerProto.SingerInfo parseFrom( + 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 com.google.cloud.spanner.SingerProto.SingerInfo parseDelimitedFrom( - java.io.InputStream input) throws java.io.IOException { + public static SingerInfo parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { return com.google.protobuf.GeneratedMessageV3.parseDelimitedWithIOException(PARSER, input); } - public static com.google.cloud.spanner.SingerProto.SingerInfo parseDelimitedFrom( + 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 com.google.cloud.spanner.SingerProto.SingerInfo parseFrom( - com.google.protobuf.CodedInputStream input) throws java.io.IOException { + public static SingerInfo parseFrom(com.google.protobuf.CodedInputStream input) + throws java.io.IOException { return com.google.protobuf.GeneratedMessageV3.parseWithIOException(PARSER, input); } - public static com.google.cloud.spanner.SingerProto.SingerInfo parseFrom( + public static SingerInfo parseFrom( com.google.protobuf.CodedInputStream input, com.google.protobuf.ExtensionRegistryLite extensionRegistry) throws java.io.IOException { @@ -630,7 +570,7 @@ public static com.google.cloud.spanner.SingerProto.SingerInfo parseFrom( PARSER, input, extensionRegistry); } - @java.lang.Override + @Override public Builder newBuilderForType() { return newBuilder(); } @@ -639,94 +579,84 @@ public static Builder newBuilder() { return DEFAULT_INSTANCE.toBuilder(); } - public static Builder newBuilder(com.google.cloud.spanner.SingerProto.SingerInfo prototype) { + public static Builder newBuilder(SingerInfo prototype) { return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); } - @java.lang.Override + @Override public Builder toBuilder() { return this == DEFAULT_INSTANCE ? new Builder() : new Builder().mergeFrom(this); } - @java.lang.Override - protected Builder newBuilderForType( - com.google.protobuf.GeneratedMessageV3.BuilderParent parent) { + @Override + protected Builder newBuilderForType(BuilderParent parent) { Builder builder = new Builder(parent); return builder; } - /** Protobuf type {@code spanner.examples.music.SingerInfo} */ + /** Protobuf type {@code examples.spanner.music.SingerInfo} */ public static final class Builder extends com.google.protobuf.GeneratedMessageV3.Builder implements - // @@protoc_insertion_point(builder_implements:spanner.examples.music.SingerInfo) - com.google.cloud.spanner.SingerProto.SingerInfoOrBuilder { + // @@protoc_insertion_point(builder_implements:examples.spanner.music.SingerInfo) + SingerInfoOrBuilder { public static final com.google.protobuf.Descriptors.Descriptor getDescriptor() { - return com.google.cloud.spanner.SingerProto - .internal_static_spanner_examples_music_SingerInfo_descriptor; + return SingerProto.internal_static_examples_spanner_music_SingerInfo_descriptor; } - @java.lang.Override - protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable - internalGetFieldAccessorTable() { - return com.google.cloud.spanner.SingerProto - .internal_static_spanner_examples_music_SingerInfo_fieldAccessorTable - .ensureFieldAccessorsInitialized( - com.google.cloud.spanner.SingerProto.SingerInfo.class, - com.google.cloud.spanner.SingerProto.SingerInfo.Builder.class); + @Override + protected FieldAccessorTable internalGetFieldAccessorTable() { + return SingerProto.internal_static_examples_spanner_music_SingerInfo_fieldAccessorTable + .ensureFieldAccessorsInitialized(SingerInfo.class, Builder.class); } // Construct using com.google.cloud.spanner.SingerProto.SingerInfo.newBuilder() - private Builder() { - maybeForceBuilderInitialization(); - } + private Builder() {} - private Builder(com.google.protobuf.GeneratedMessageV3.BuilderParent parent) { + private Builder(BuilderParent parent) { super(parent); - maybeForceBuilderInitialization(); - } - - private void maybeForceBuilderInitialization() { - if (com.google.protobuf.GeneratedMessageV3.alwaysUseFieldBuilders) {} } - @java.lang.Override + @Override public Builder clear() { super.clear(); + bitField0_ = 0; singerId_ = 0L; - bitField0_ = (bitField0_ & ~0x00000001); birthDate_ = ""; - bitField0_ = (bitField0_ & ~0x00000002); nationality_ = ""; - bitField0_ = (bitField0_ & ~0x00000004); genre_ = 0; - bitField0_ = (bitField0_ & ~0x00000008); return this; } - @java.lang.Override + @Override public com.google.protobuf.Descriptors.Descriptor getDescriptorForType() { - return com.google.cloud.spanner.SingerProto - .internal_static_spanner_examples_music_SingerInfo_descriptor; + return SingerProto.internal_static_examples_spanner_music_SingerInfo_descriptor; } - @java.lang.Override - public com.google.cloud.spanner.SingerProto.SingerInfo getDefaultInstanceForType() { - return com.google.cloud.spanner.SingerProto.SingerInfo.getDefaultInstance(); + @Override + public SingerInfo getDefaultInstanceForType() { + return SingerInfo.getDefaultInstance(); } - @java.lang.Override - public com.google.cloud.spanner.SingerProto.SingerInfo build() { - com.google.cloud.spanner.SingerProto.SingerInfo result = buildPartial(); + @Override + public SingerInfo build() { + SingerInfo result = buildPartial(); if (!result.isInitialized()) { throw newUninitializedMessageException(result); } return result; } - @java.lang.Override - public com.google.cloud.spanner.SingerProto.SingerInfo buildPartial() { - com.google.cloud.spanner.SingerProto.SingerInfo result = - new com.google.cloud.spanner.SingerProto.SingerInfo(this); + @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)) { @@ -734,113 +664,144 @@ public com.google.cloud.spanner.SingerProto.SingerInfo buildPartial() { to_bitField0_ |= 0x00000001; } if (((from_bitField0_ & 0x00000002) != 0)) { + result.birthDate_ = birthDate_; to_bitField0_ |= 0x00000002; } - result.birthDate_ = birthDate_; if (((from_bitField0_ & 0x00000004) != 0)) { + result.nationality_ = nationality_; to_bitField0_ |= 0x00000004; } - result.nationality_ = nationality_; if (((from_bitField0_ & 0x00000008) != 0)) { + result.genre_ = genre_; to_bitField0_ |= 0x00000008; } - result.genre_ = genre_; - result.bitField0_ = to_bitField0_; - onBuilt(); - return result; + result.bitField0_ |= to_bitField0_; } - @java.lang.Override + @Override public Builder clone() { return super.clone(); } - @java.lang.Override - public Builder setField( - com.google.protobuf.Descriptors.FieldDescriptor field, java.lang.Object value) { + @Override + public Builder setField(com.google.protobuf.Descriptors.FieldDescriptor field, Object value) { return super.setField(field, value); } - @java.lang.Override + @Override public Builder clearField(com.google.protobuf.Descriptors.FieldDescriptor field) { return super.clearField(field); } - @java.lang.Override + @Override public Builder clearOneof(com.google.protobuf.Descriptors.OneofDescriptor oneof) { return super.clearOneof(oneof); } - @java.lang.Override + @Override public Builder setRepeatedField( - com.google.protobuf.Descriptors.FieldDescriptor field, - int index, - java.lang.Object value) { + com.google.protobuf.Descriptors.FieldDescriptor field, int index, Object value) { return super.setRepeatedField(field, index, value); } - @java.lang.Override + @Override public Builder addRepeatedField( - com.google.protobuf.Descriptors.FieldDescriptor field, java.lang.Object value) { + com.google.protobuf.Descriptors.FieldDescriptor field, Object value) { return super.addRepeatedField(field, value); } - @java.lang.Override + @Override public Builder mergeFrom(com.google.protobuf.Message other) { - if (other instanceof com.google.cloud.spanner.SingerProto.SingerInfo) { - return mergeFrom((com.google.cloud.spanner.SingerProto.SingerInfo) other); + if (other instanceof SingerInfo) { + return mergeFrom((SingerInfo) other); } else { super.mergeFrom(other); return this; } } - public Builder mergeFrom(com.google.cloud.spanner.SingerProto.SingerInfo other) { - if (other == com.google.cloud.spanner.SingerProto.SingerInfo.getDefaultInstance()) - return this; + public Builder mergeFrom(SingerInfo other) { + if (other == SingerInfo.getDefaultInstance()) return this; if (other.hasSingerId()) { setSingerId(other.getSingerId()); } if (other.hasBirthDate()) { - bitField0_ |= 0x00000002; birthDate_ = other.birthDate_; + bitField0_ |= 0x00000002; onChanged(); } if (other.hasNationality()) { - bitField0_ |= 0x00000004; nationality_ = other.nationality_; + bitField0_ |= 0x00000004; onChanged(); } if (other.hasGenre()) { setGenre(other.getGenre()); } - this.mergeUnknownFields(other.unknownFields); + this.mergeUnknownFields(other.getUnknownFields()); onChanged(); return this; } - @java.lang.Override + @Override public final boolean isInitialized() { return true; } - @java.lang.Override + @Override public Builder mergeFrom( com.google.protobuf.CodedInputStream input, com.google.protobuf.ExtensionRegistryLite extensionRegistry) throws java.io.IOException { - com.google.cloud.spanner.SingerProto.SingerInfo parsedMessage = null; + if (extensionRegistry == null) { + throw new NullPointerException(); + } try { - parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry); + 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) { - parsedMessage = - (com.google.cloud.spanner.SingerProto.SingerInfo) e.getUnfinishedMessage(); throw e.unwrapIOException(); } finally { - if (parsedMessage != null) { - mergeFrom(parsedMessage); - } - } + onChanged(); + } // finally return this; } @@ -852,7 +813,7 @@ public Builder mergeFrom( * * @return Whether the singerId field is set. */ - @java.lang.Override + @Override public boolean hasSingerId() { return ((bitField0_ & 0x00000001) != 0); } @@ -861,7 +822,7 @@ public boolean hasSingerId() { * * @return The singerId. */ - @java.lang.Override + @Override public long getSingerId() { return singerId_; } @@ -872,8 +833,9 @@ public long getSingerId() { * @return This builder for chaining. */ public Builder setSingerId(long value) { - bitField0_ |= 0x00000001; + singerId_ = value; + bitField0_ |= 0x00000001; onChanged(); return this; } @@ -889,7 +851,7 @@ public Builder clearSingerId() { return this; } - private java.lang.Object birthDate_ = ""; + private Object birthDate_ = ""; /** * optional string birth_date = 2; * @@ -903,17 +865,15 @@ public boolean hasBirthDate() { * * @return The birthDate. */ - public java.lang.String getBirthDate() { - java.lang.Object ref = birthDate_; - if (!(ref instanceof java.lang.String)) { + public String getBirthDate() { + Object ref = birthDate_; + if (!(ref instanceof String)) { com.google.protobuf.ByteString bs = (com.google.protobuf.ByteString) ref; - java.lang.String s = bs.toStringUtf8(); - if (bs.isValidUtf8()) { - birthDate_ = s; - } + String s = bs.toStringUtf8(); + birthDate_ = s; return s; } else { - return (java.lang.String) ref; + return (String) ref; } } /** @@ -922,10 +882,10 @@ public java.lang.String getBirthDate() { * @return The bytes for birthDate. */ public com.google.protobuf.ByteString getBirthDateBytes() { - java.lang.Object ref = birthDate_; + Object ref = birthDate_; if (ref instanceof String) { com.google.protobuf.ByteString b = - com.google.protobuf.ByteString.copyFromUtf8((java.lang.String) ref); + com.google.protobuf.ByteString.copyFromUtf8((String) ref); birthDate_ = b; return b; } else { @@ -938,12 +898,12 @@ public com.google.protobuf.ByteString getBirthDateBytes() { * @param value The birthDate to set. * @return This builder for chaining. */ - public Builder setBirthDate(java.lang.String value) { + public Builder setBirthDate(String value) { if (value == null) { throw new NullPointerException(); } - bitField0_ |= 0x00000002; birthDate_ = value; + bitField0_ |= 0x00000002; onChanged(); return this; } @@ -953,8 +913,8 @@ public Builder setBirthDate(java.lang.String value) { * @return This builder for chaining. */ public Builder clearBirthDate() { - bitField0_ = (bitField0_ & ~0x00000002); birthDate_ = getDefaultInstance().getBirthDate(); + bitField0_ = (bitField0_ & ~0x00000002); onChanged(); return this; } @@ -968,13 +928,14 @@ public Builder setBirthDateBytes(com.google.protobuf.ByteString value) { if (value == null) { throw new NullPointerException(); } - bitField0_ |= 0x00000002; + checkByteStringIsUtf8(value); birthDate_ = value; + bitField0_ |= 0x00000002; onChanged(); return this; } - private java.lang.Object nationality_ = ""; + private Object nationality_ = ""; /** * optional string nationality = 3; * @@ -988,17 +949,15 @@ public boolean hasNationality() { * * @return The nationality. */ - public java.lang.String getNationality() { - java.lang.Object ref = nationality_; - if (!(ref instanceof java.lang.String)) { + public String getNationality() { + Object ref = nationality_; + if (!(ref instanceof String)) { com.google.protobuf.ByteString bs = (com.google.protobuf.ByteString) ref; - java.lang.String s = bs.toStringUtf8(); - if (bs.isValidUtf8()) { - nationality_ = s; - } + String s = bs.toStringUtf8(); + nationality_ = s; return s; } else { - return (java.lang.String) ref; + return (String) ref; } } /** @@ -1007,10 +966,10 @@ public java.lang.String getNationality() { * @return The bytes for nationality. */ public com.google.protobuf.ByteString getNationalityBytes() { - java.lang.Object ref = nationality_; + Object ref = nationality_; if (ref instanceof String) { com.google.protobuf.ByteString b = - com.google.protobuf.ByteString.copyFromUtf8((java.lang.String) ref); + com.google.protobuf.ByteString.copyFromUtf8((String) ref); nationality_ = b; return b; } else { @@ -1023,12 +982,12 @@ public com.google.protobuf.ByteString getNationalityBytes() { * @param value The nationality to set. * @return This builder for chaining. */ - public Builder setNationality(java.lang.String value) { + public Builder setNationality(String value) { if (value == null) { throw new NullPointerException(); } - bitField0_ |= 0x00000004; nationality_ = value; + bitField0_ |= 0x00000004; onChanged(); return this; } @@ -1038,8 +997,8 @@ public Builder setNationality(java.lang.String value) { * @return This builder for chaining. */ public Builder clearNationality() { - bitField0_ = (bitField0_ & ~0x00000004); nationality_ = getDefaultInstance().getNationality(); + bitField0_ = (bitField0_ & ~0x00000004); onChanged(); return this; } @@ -1053,41 +1012,61 @@ public Builder setNationalityBytes(com.google.protobuf.ByteString value) { if (value == null) { throw new NullPointerException(); } - bitField0_ |= 0x00000004; + checkByteStringIsUtf8(value); nationality_ = value; + bitField0_ |= 0x00000004; onChanged(); return this; } private int genre_ = 0; /** - * optional .spanner.examples.music.Genre genre = 4; + * optional .examples.spanner.music.Genre genre = 4; * * @return Whether the genre field is set. */ - @java.lang.Override + @Override public boolean hasGenre() { return ((bitField0_ & 0x00000008) != 0); } /** - * optional .spanner.examples.music.Genre genre = 4; + * 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. */ - @java.lang.Override - public com.google.cloud.spanner.SingerProto.Genre getGenre() { - @SuppressWarnings("deprecation") - com.google.cloud.spanner.SingerProto.Genre result = - com.google.cloud.spanner.SingerProto.Genre.valueOf(genre_); - return result == null ? com.google.cloud.spanner.SingerProto.Genre.POP : result; + @Override + public Genre getGenre() { + Genre result = Genre.forNumber(genre_); + return result == null ? Genre.UNRECOGNIZED : result; } /** - * optional .spanner.examples.music.Genre genre = 4; + * optional .examples.spanner.music.Genre genre = 4; * * @param value The genre to set. * @return This builder for chaining. */ - public Builder setGenre(com.google.cloud.spanner.SingerProto.Genre value) { + public Builder setGenre(Genre value) { if (value == null) { throw new NullPointerException(); } @@ -1097,7 +1076,7 @@ public Builder setGenre(com.google.cloud.spanner.SingerProto.Genre value) { return this; } /** - * optional .spanner.examples.music.Genre genre = 4; + * optional .examples.spanner.music.Genre genre = 4; * * @return This builder for chaining. */ @@ -1108,41 +1087,52 @@ public Builder clearGenre() { return this; } - @java.lang.Override + @Override public final Builder setUnknownFields( final com.google.protobuf.UnknownFieldSet unknownFields) { return super.setUnknownFields(unknownFields); } - @java.lang.Override + @Override public final Builder mergeUnknownFields( final com.google.protobuf.UnknownFieldSet unknownFields) { return super.mergeUnknownFields(unknownFields); } - // @@protoc_insertion_point(builder_scope:spanner.examples.music.SingerInfo) + // @@protoc_insertion_point(builder_scope:examples.spanner.music.SingerInfo) } - // @@protoc_insertion_point(class_scope:spanner.examples.music.SingerInfo) - private static final com.google.cloud.spanner.SingerProto.SingerInfo DEFAULT_INSTANCE; + // @@protoc_insertion_point(class_scope:examples.spanner.music.SingerInfo) + private static final SingerInfo DEFAULT_INSTANCE; static { - DEFAULT_INSTANCE = new com.google.cloud.spanner.SingerProto.SingerInfo(); + DEFAULT_INSTANCE = new SingerInfo(); } - public static com.google.cloud.spanner.SingerProto.SingerInfo getDefaultInstance() { + public static SingerInfo getDefaultInstance() { return DEFAULT_INSTANCE; } - @java.lang.Deprecated - public static final com.google.protobuf.Parser PARSER = + private static final com.google.protobuf.Parser PARSER = new com.google.protobuf.AbstractParser() { - @java.lang.Override + @Override public SingerInfo parsePartialFrom( com.google.protobuf.CodedInputStream input, com.google.protobuf.ExtensionRegistryLite extensionRegistry) throws com.google.protobuf.InvalidProtocolBufferException { - return new SingerInfo(input, extensionRegistry); + 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(); } }; @@ -1150,21 +1140,21 @@ public static com.google.protobuf.Parser parser() { return PARSER; } - @java.lang.Override + @Override public com.google.protobuf.Parser getParserForType() { return PARSER; } - @java.lang.Override - public com.google.cloud.spanner.SingerProto.SingerInfo getDefaultInstanceForType() { + @Override + public SingerInfo getDefaultInstanceForType() { return DEFAULT_INSTANCE; } } private static final com.google.protobuf.Descriptors.Descriptor - internal_static_spanner_examples_music_SingerInfo_descriptor; + internal_static_examples_spanner_music_SingerInfo_descriptor; private static final com.google.protobuf.GeneratedMessageV3.FieldAccessorTable - internal_static_spanner_examples_music_SingerInfo_fieldAccessorTable; + internal_static_examples_spanner_music_SingerInfo_fieldAccessorTable; public static com.google.protobuf.Descriptors.FileDescriptor getDescriptor() { return descriptor; @@ -1173,24 +1163,26 @@ public static com.google.protobuf.Descriptors.FileDescriptor getDescriptor() { private static com.google.protobuf.Descriptors.FileDescriptor descriptor; static { - java.lang.String[] descriptorData = { - "\n\014singer.proto\022\026spanner.examples.music\"v" - + "\n\nSingerInfo\022\021\n\tsinger_id\030\001 \001(\003\022\022\n\nbirth" - + "_date\030\002 \001(\t\022\023\n\013nationality\030\003 \001(\t\022,\n\005genr" - + "e\030\004 \001(\0162\035.spanner.examples.music.Genre*." - + "\n\005Genre\022\007\n\003POP\020\000\022\010\n\004JAZZ\020\001\022\010\n\004FOLK\020\002\022\010\n\004" - + "ROCK\020\003B)\n\030com.google.cloud.spannerB\013Sing" - + "erProtoP\000" + 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\030com.google.cloud.spannerB\013SingerPr" + + "otoP\000b\006proto3" }; descriptor = com.google.protobuf.Descriptors.FileDescriptor.internalBuildGeneratedFileFrom( descriptorData, new com.google.protobuf.Descriptors.FileDescriptor[] {}); - internal_static_spanner_examples_music_SingerInfo_descriptor = + internal_static_examples_spanner_music_SingerInfo_descriptor = getDescriptor().getMessageTypes().get(0); - internal_static_spanner_examples_music_SingerInfo_fieldAccessorTable = + internal_static_examples_spanner_music_SingerInfo_fieldAccessorTable = new com.google.protobuf.GeneratedMessageV3.FieldAccessorTable( - internal_static_spanner_examples_music_SingerInfo_descriptor, - new java.lang.String[] { + internal_static_examples_spanner_music_SingerInfo_descriptor, + new String[] { "SingerId", "BirthDate", "Nationality", "Genre", }); } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpanTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpanTest.java index fdfe4871680..9560256b485 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpanTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpanTest.java @@ -506,12 +506,13 @@ public void transactionRunnerWithError() { Map spans = failOnOverkillTraceComponent.getSpans(); if (isMultiplexedSessionsEnabled()) { - assertEquals(spans.toString(), 4, spans.size()); + assertEquals(spans.toString(), 5, spans.size()); assertThat(spans).containsEntry("CloudSpannerOperation.CreateMultiplexedSession", true); } else { - assertThat(spans.size()).isEqualTo(3); + assertThat(spans.size()).isEqualTo(4); } assertThat(spans).containsEntry("CloudSpanner.ReadWriteTransaction", true); + assertThat(spans).containsEntry("CloudSpannerOperation.ExecuteUpdate", true); assertThat(spans).containsEntry("CloudSpannerOperation.BatchCreateSessions", true); assertThat(spans).containsEntry("CloudSpannerOperation.BatchCreateSessionsRequest", true); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerOptionsTestHelper.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerOptionsTestHelper.java new file mode 100644 index 00000000000..8f8c0a30a8c --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerOptionsTestHelper.java @@ -0,0 +1,23 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.cloud.spanner; + +public class SpannerOptionsTestHelper { + + public static void resetActiveTracingFramework() { + SpannerOptions.resetActiveTracingFramework(); + } +} diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionContextImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionContextImplTest.java index 53120eda441..c1da423760d 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionContextImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionContextImplTest.java @@ -17,6 +17,7 @@ package com.google.cloud.spanner; import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyMap; import static org.mockito.Mockito.doNothing; @@ -34,6 +35,7 @@ import com.google.spanner.v1.CommitRequest; import com.google.spanner.v1.ExecuteBatchDmlRequest; import com.google.spanner.v1.ExecuteBatchDmlResponse; +import io.opentelemetry.api.common.Attributes; import java.util.Collections; import org.junit.Before; import org.junit.Test; @@ -67,7 +69,17 @@ public void setup() { doNothing().when(span).setStatus(any(Throwable.class)); doNothing().when(span).end(); doNothing().when(span).addAnnotation("Starting Commit"); + when(tracer.createStatementAttributes(any(Statement.class), any())) + .thenReturn(Attributes.empty()); + when(tracer.createStatementBatchAttributes(any(Iterable.class), any())) + .thenReturn(Attributes.empty()); when(tracer.spanBuilderWithExplicitParent(SpannerImpl.COMMIT, span)).thenReturn(span); + when(tracer.spanBuilderWithExplicitParent( + eq(SpannerImpl.COMMIT), eq(span), any(Attributes.class))) + .thenReturn(span); + when(tracer.spanBuilderWithExplicitParent( + eq(SpannerImpl.BATCH_UPDATE), eq(span), any(Attributes.class))) + .thenReturn(span); } private TransactionContextImpl createContext() { @@ -187,7 +199,7 @@ public void testReturnCommitStats() { .setSession(session.getName()) .setTransactionId(transactionId) .build(); - verify(rpc).commitAsync(Mockito.eq(request), anyMap()); + verify(rpc).commitAsync(eq(request), anyMap()); } } @@ -210,6 +222,8 @@ private void batchDml(int status) { .setRpc(rpc) .setTransactionId(ByteString.copyFromUtf8("test")) .setOptions(Options.fromTransactionOptions()) + .setTracer(tracer) + .setSpan(span) .build()) { impl.batchUpdate(Collections.singletonList(statement)); } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionRunnerImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionRunnerImplTest.java index d30df09d4c3..6a707a490dc 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionRunnerImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionRunnerImplTest.java @@ -112,7 +112,7 @@ public static void setupOpenTelemetry() { @Before public void setUp() { MockitoAnnotations.initMocks(this); - tracer = new TraceWrapper(Tracing.getTracer(), OpenTelemetry.noop().getTracer("")); + tracer = new TraceWrapper(Tracing.getTracer(), OpenTelemetry.noop().getTracer(""), false); firstRun = true; when(session.newTransaction(Options.fromTransactionOptions())).thenReturn(txn); when(session.getTracer()).thenReturn(tracer); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionImplTest.java index 7b57e3f9014..a14f91a4815 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionImplTest.java @@ -69,6 +69,7 @@ import com.google.cloud.spanner.TransactionRunner; import com.google.cloud.spanner.Type; import com.google.cloud.spanner.connection.AbstractStatementParser.ParsedStatement; +import com.google.cloud.spanner.connection.AbstractStatementParser.StatementType; import com.google.cloud.spanner.connection.ConnectionImpl.UnitOfWorkType; import com.google.cloud.spanner.connection.ConnectionStatementExecutorImpl.StatementTimeoutGetter; import com.google.cloud.spanner.connection.ReadOnlyStalenessUtil.GetExactStaleness; @@ -1751,7 +1752,10 @@ public void testTransactionTagNotAllowedAfterTransactionStarted() { new ConnectionImpl( connectionOptions, spannerPool, ddlClient, dbClient, mock(BatchClient.class)) { @Override - UnitOfWork createNewUnitOfWork(boolean isInternalMetadataQuery, boolean forceSingleUse) { + UnitOfWork createNewUnitOfWork( + boolean isInternalMetadataQuery, + boolean forceSingleUse, + StatementType statementType) { return unitOfWork; } }) { diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DdlBatchTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DdlBatchTest.java index 5ef4c5291d1..74cc925ee85 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DdlBatchTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DdlBatchTest.java @@ -51,6 +51,7 @@ import com.google.protobuf.Timestamp; import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata; import io.grpc.Status; +import io.opentelemetry.api.trace.Span; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -128,6 +129,7 @@ private DdlBatch createSubject(DdlClient ddlClient, DatabaseClient dbClient) { .setDdlClient(ddlClient) .setDatabaseClient(dbClient) .withStatementExecutor(new StatementExecutor()) + .setSpan(Span.getInvalid()) .build(); } @@ -387,6 +389,7 @@ public void testUpdateCount() throws InterruptedException, ExecutionException { .withStatementExecutor(new StatementExecutor()) .setDdlClient(client) .setDatabaseClient(mock(DatabaseClient.class)) + .setSpan(Span.getInvalid()) .build(); batch.executeDdlAsync( CallType.SYNC, @@ -425,6 +428,7 @@ public void testFailedUpdateCount() throws InterruptedException, ExecutionExcept .withStatementExecutor(new StatementExecutor()) .setDdlClient(client) .setDatabaseClient(mock(DatabaseClient.class)) + .setSpan(Span.getInvalid()) .build(); batch.executeDdlAsync( CallType.SYNC, @@ -467,6 +471,7 @@ public void testFailedAfterFirstStatement() throws InterruptedException, Executi .withStatementExecutor(new StatementExecutor()) .setDdlClient(client) .setDatabaseClient(mock(DatabaseClient.class)) + .setSpan(Span.getInvalid()) .build(); batch.executeDdlAsync( CallType.SYNC, diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DmlBatchTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DmlBatchTest.java index 289683e34f5..d71376df40f 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DmlBatchTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DmlBatchTest.java @@ -38,6 +38,7 @@ import com.google.cloud.spanner.connection.AbstractStatementParser.StatementType; import com.google.cloud.spanner.connection.UnitOfWork.CallType; import com.google.cloud.spanner.connection.UnitOfWork.UnitOfWorkState; +import io.opentelemetry.api.trace.Span; import java.util.Arrays; import java.util.Collections; import org.junit.Test; @@ -65,6 +66,7 @@ private DmlBatch createSubject(UnitOfWork transaction) { return DmlBatch.newBuilder() .setTransaction(transaction) .withStatementExecutor(new StatementExecutor()) + .setSpan(Span.getInvalid()) .build(); } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/OpenTelemetryTracingTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/OpenTelemetryTracingTest.java new file mode 100644 index 00000000000..e51c32d36e2 --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/OpenTelemetryTracingTest.java @@ -0,0 +1,592 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.spanner.connection; + +import static com.google.cloud.spanner.connection.Repeat.twice; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.google.cloud.spanner.ResultSet; +import com.google.cloud.spanner.SpannerOptions; +import com.google.cloud.spanner.SpannerOptions.SpannerEnvironment; +import com.google.cloud.spanner.SpannerOptionsTestHelper; +import com.google.cloud.spanner.Statement; +import com.google.common.collect.ImmutableList; +import com.google.longrunning.Operation; +import com.google.protobuf.Any; +import com.google.protobuf.Empty; +import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.data.EventData; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import java.util.List; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.stream.Collectors; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class OpenTelemetryTracingTest extends AbstractMockServerTest { + private static InMemorySpanExporter spanExporter; + + private static OpenTelemetrySdk openTelemetry; + + @BeforeClass + public static void setupOpenTelemetry() { + SpannerOptionsTestHelper.resetActiveTracingFramework(); + SpannerOptions.enableOpenTelemetryTraces(); + GlobalOpenTelemetry.resetForTest(); + + spanExporter = InMemorySpanExporter.create(); + + SdkTracerProvider tracerProvider = + SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(spanExporter)) + .build(); + + openTelemetry = + OpenTelemetrySdk.builder() + .setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance())) + .setTracerProvider(tracerProvider) + .buildAndRegisterGlobal(); + } + + @AfterClass + public static void closeOpenTelemetry() { + SpannerPool.closeSpannerPool(); + if (openTelemetry != null) { + openTelemetry.close(); + } + } + + @After + public void clearRequests() { + mockSpanner.clearRequests(); + spanExporter.reset(); + } + + Connection createTestConnection() { + return createTestConnection(getBaseUrl() + ";enableExtendedTracing=true"); + } + + Connection createTestConnection(String url) { + return ConnectionOptions.newBuilder() + .setTracingPrefix("CloudSpannerJdbc") + .setUri(url) + .build() + .getConnection(); + } + + @Test + public void testSingleUseQuery_withoutSqlStatement() { + try (Connection connection = createTestConnection(getBaseUrl())) { + connection.setAutocommit(true); + try (ResultSet resultSet = connection.executeQuery(SELECT1_STATEMENT)) { + assertTrue(resultSet.next()); + assertFalse(resultSet.next()); + } + } + assertEquals(CompletableResultCode.ofSuccess(), spanExporter.flush()); + List spans = spanExporter.getFinishedSpanItems(); + // There are two query spans: One for getting the database dialect, and one for the actual + // test query. + assertEquals( + 2, + spans.stream() + .filter(span -> span.getName().equals("CloudSpannerOperation.ExecuteStreamingQuery")) + .count()); + // Verify that both do not have any db.statement attribute. + assertEquals( + 2, + spans.stream() + .filter( + span -> + span.getName().equals("CloudSpannerOperation.ExecuteStreamingQuery") + && span.getAttributes().get(AttributeKey.stringKey("db.statement")) == null) + .count()); + } + + @Test + public void testSingleUseQuery_withoutSqlStatement_usingEnvVar() { + SpannerPool.closeSpannerPool(); + SpannerOptions.useEnvironment( + new SpannerEnvironment() { + @Override + public boolean isEnableExtendedTracing() { + return true; + } + }); + + try (Connection connection = createTestConnection(getBaseUrl())) { + connection.setAutocommit(true); + try (ResultSet resultSet = connection.executeQuery(SELECT1_STATEMENT)) { + assertTrue(resultSet.next()); + assertFalse(resultSet.next()); + } + } finally { + SpannerOptions.useDefaultEnvironment(); + } + assertEquals(CompletableResultCode.ofSuccess(), spanExporter.flush()); + List spans = spanExporter.getFinishedSpanItems(); + assertContains("CloudSpannerJdbc.SingleUseTransaction", spans); + assertContains("CloudSpanner.ReadOnlyTransaction", spans); + assertContains( + "CloudSpannerOperation.ExecuteStreamingQuery", + Attributes.of(AttributeKey.stringKey("db.statement"), SELECT1_STATEMENT.getSql()), + spans); + assertParent( + "CloudSpannerJdbc.SingleUseTransaction", "CloudSpanner.ReadOnlyTransaction", spans); + assertParent( + "CloudSpanner.ReadOnlyTransaction", + "CloudSpannerOperation.ExecuteStreamingQuery", + Attributes.of(AttributeKey.stringKey("db.statement"), SELECT1_STATEMENT.getSql()), + spans); + } + + @Test + public void testSingleUseQuery() { + try (Connection connection = createTestConnection()) { + connection.setAutocommit(true); + try (ResultSet resultSet = connection.executeQuery(SELECT1_STATEMENT)) { + assertTrue(resultSet.next()); + assertFalse(resultSet.next()); + } + } + assertEquals(CompletableResultCode.ofSuccess(), spanExporter.flush()); + List spans = spanExporter.getFinishedSpanItems(); + assertContains("CloudSpannerJdbc.SingleUseTransaction", spans); + assertContains("CloudSpanner.ReadOnlyTransaction", spans); + assertContains( + "CloudSpannerOperation.ExecuteStreamingQuery", + Attributes.of(AttributeKey.stringKey("db.statement"), SELECT1_STATEMENT.getSql()), + spans); + assertParent( + "CloudSpannerJdbc.SingleUseTransaction", "CloudSpanner.ReadOnlyTransaction", spans); + assertParent( + "CloudSpanner.ReadOnlyTransaction", + "CloudSpannerOperation.ExecuteStreamingQuery", + Attributes.of(AttributeKey.stringKey("db.statement"), SELECT1_STATEMENT.getSql()), + spans); + } + + @Test + public void testSingleUseUpdate() { + try (Connection connection = createTestConnection()) { + connection.setAutocommit(true); + connection.executeUpdate(INSERT_STATEMENT); + } + assertEquals(CompletableResultCode.ofSuccess(), spanExporter.flush()); + List spans = spanExporter.getFinishedSpanItems(); + assertContains("CloudSpannerJdbc.SingleUseTransaction", spans); + assertContains("CloudSpanner.ReadWriteTransaction", spans); + assertContains( + "CloudSpannerOperation.ExecuteUpdate", + Attributes.of(AttributeKey.stringKey("db.statement"), INSERT_STATEMENT.getSql()), + spans); + assertContains("CloudSpannerOperation.Commit", spans); + + assertParent( + "CloudSpannerJdbc.SingleUseTransaction", "CloudSpanner.ReadWriteTransaction", spans); + assertParent( + "CloudSpanner.ReadWriteTransaction", + "CloudSpannerOperation.ExecuteUpdate", + Attributes.of(AttributeKey.stringKey("db.statement"), INSERT_STATEMENT.getSql()), + spans); + assertParent("CloudSpanner.ReadWriteTransaction", "CloudSpannerOperation.Commit", spans); + } + + @Test + public void testSingleUseBatchUpdate() { + try (Connection connection = createTestConnection()) { + connection.setAutocommit(true); + connection.startBatchDml(); + connection.executeUpdate(INSERT_STATEMENT); + connection.executeUpdate(INSERT_STATEMENT); + connection.runBatch(); + } + assertEquals(CompletableResultCode.ofSuccess(), spanExporter.flush()); + List spans = spanExporter.getFinishedSpanItems(); + assertContains("CloudSpannerJdbc.SingleUseTransaction", spans); + assertContains("CloudSpanner.ReadWriteTransaction", spans); + assertContains( + "CloudSpannerOperation.BatchUpdate", + Attributes.of( + AttributeKey.stringArrayKey("db.statement"), + ImmutableList.of(INSERT_STATEMENT.getSql(), INSERT_STATEMENT.getSql())), + spans); + assertContains("CloudSpannerOperation.Commit", spans); + + assertParent( + "CloudSpannerJdbc.SingleUseTransaction", "CloudSpanner.ReadWriteTransaction", spans); + assertParent( + "CloudSpanner.ReadWriteTransaction", + "CloudSpannerOperation.BatchUpdate", + Attributes.of( + AttributeKey.stringArrayKey("db.statement"), + ImmutableList.of(INSERT_STATEMENT.getSql(), INSERT_STATEMENT.getSql())), + spans); + assertParent("CloudSpanner.ReadWriteTransaction", "CloudSpannerOperation.Commit", spans); + } + + @Test + public void testSingleUseDdl() { + String ddl = "CREATE TABLE foo (id int64) PRIMARY KEY (id)"; + addUpdateDdlResponse(); + + try (Connection connection = createTestConnection()) { + connection.setAutocommit(true); + connection.execute(Statement.of(ddl)); + } + assertEquals(CompletableResultCode.ofSuccess(), spanExporter.flush()); + List spans = spanExporter.getFinishedSpanItems(); + assertContains( + "CloudSpannerJdbc.DdlStatement", + Attributes.of(AttributeKey.stringKey("db.statement"), ddl), + spans); + } + + @Test + public void testSingleUseDdlBatch() { + String ddl1 = "CREATE TABLE foo (id int64, value string(max)) PRIMARY KEY (id)"; + String ddl2 = "CREATE INDEX idx_foo ON foo (value)"; + addUpdateDdlResponse(); + + try (Connection connection = createTestConnection()) { + connection.setAutocommit(true); + connection.startBatchDdl(); + connection.execute(Statement.of(ddl1)); + connection.execute(Statement.of(ddl2)); + connection.runBatch(); + } + assertEquals(CompletableResultCode.ofSuccess(), spanExporter.flush()); + List spans = spanExporter.getFinishedSpanItems(); + assertContains( + "CloudSpannerJdbc.DdlBatch", + Attributes.of(AttributeKey.stringArrayKey("db.statement"), ImmutableList.of(ddl1, ddl2)), + spans); + } + + @Test + public void testMultiUseReadOnlyQueries() { + try (Connection connection = createTestConnection()) { + connection.setAutocommit(false); + connection.setReadOnly(true); + twice( + () -> { + try (ResultSet resultSet = connection.executeQuery(SELECT1_STATEMENT)) { + assertTrue(resultSet.next()); + assertFalse(resultSet.next()); + } + }); + connection.commit(); + } + assertEquals(CompletableResultCode.ofSuccess(), spanExporter.flush()); + List spans = spanExporter.getFinishedSpanItems(); + assertContains("CloudSpannerJdbc.ReadOnlyTransaction", spans); + assertContains("CloudSpanner.ReadOnlyTransaction", spans); + assertContains( + "CloudSpannerOperation.ExecuteStreamingQuery", + 2, + Attributes.of(AttributeKey.stringKey("db.statement"), SELECT1_STATEMENT.getSql()), + spans); + assertParent("CloudSpannerJdbc.ReadOnlyTransaction", "CloudSpanner.ReadOnlyTransaction", spans); + assertParent( + "CloudSpanner.ReadOnlyTransaction", + "CloudSpannerOperation.ExecuteStreamingQuery", + Attributes.of(AttributeKey.stringKey("db.statement"), SELECT1_STATEMENT.getSql()), + spans); + } + + @Test + public void testMultiUseReadWriteQueries() { + try (Connection connection = createTestConnection()) { + connection.setAutocommit(false); + connection.setReadOnly(false); + twice( + () -> { + try (ResultSet resultSet = connection.executeQuery(SELECT1_STATEMENT)) { + assertTrue(resultSet.next()); + assertFalse(resultSet.next()); + } + }); + connection.commit(); + } + assertEquals(CompletableResultCode.ofSuccess(), spanExporter.flush()); + List spans = spanExporter.getFinishedSpanItems(); + assertContains("CloudSpannerJdbc.ReadWriteTransaction", spans); + assertContains("CloudSpanner.ReadWriteTransaction", spans); + assertContains( + "CloudSpannerOperation.ExecuteStreamingQuery", + 2, + Attributes.of(AttributeKey.stringKey("db.statement"), SELECT1_STATEMENT.getSql()), + spans); + assertContains("CloudSpannerOperation.Commit", spans); + assertParent( + "CloudSpannerJdbc.ReadWriteTransaction", "CloudSpanner.ReadWriteTransaction", spans); + assertParent( + "CloudSpanner.ReadWriteTransaction", + "CloudSpannerOperation.ExecuteStreamingQuery", + Attributes.of(AttributeKey.stringKey("db.statement"), SELECT1_STATEMENT.getSql()), + spans); + assertParent("CloudSpanner.ReadWriteTransaction", "CloudSpannerOperation.Commit", spans); + } + + @Test + public void testMultiUseReadWriteUpdates() { + try (Connection connection = createTestConnection()) { + connection.setAutocommit(false); + connection.setReadOnly(false); + assertEquals(1L, connection.executeUpdate(INSERT_STATEMENT)); + assertEquals(1L, connection.executeUpdate(INSERT_STATEMENT)); + connection.commit(); + } + assertEquals(CompletableResultCode.ofSuccess(), spanExporter.flush()); + List spans = spanExporter.getFinishedSpanItems(); + assertContains("CloudSpannerJdbc.ReadWriteTransaction", spans); + assertContains("CloudSpanner.ReadWriteTransaction", spans); + assertContains( + "CloudSpannerOperation.ExecuteUpdate", + 2, + Attributes.of(AttributeKey.stringKey("db.statement"), INSERT_STATEMENT.getSql()), + spans); + assertContains("CloudSpannerOperation.Commit", spans); + assertParent( + "CloudSpannerJdbc.ReadWriteTransaction", "CloudSpanner.ReadWriteTransaction", spans); + assertParent( + "CloudSpanner.ReadWriteTransaction", + "CloudSpannerOperation.ExecuteUpdate", + Attributes.of(AttributeKey.stringKey("db.statement"), INSERT_STATEMENT.getSql()), + spans); + assertParent("CloudSpanner.ReadWriteTransaction", "CloudSpannerOperation.Commit", spans); + } + + @Test + public void testMultiUseReadWriteBatchUpdates() { + try (Connection connection = createTestConnection()) { + connection.setAutocommit(false); + connection.setReadOnly(false); + + twice( + () -> { + connection.startBatchDml(); + connection.executeUpdate(INSERT_STATEMENT); + connection.executeUpdate(INSERT_STATEMENT); + connection.runBatch(); + }); + + connection.commit(); + } + assertEquals(CompletableResultCode.ofSuccess(), spanExporter.flush()); + List spans = spanExporter.getFinishedSpanItems(); + assertContains("CloudSpannerJdbc.ReadWriteTransaction", spans); + assertContains("CloudSpanner.ReadWriteTransaction", spans); + assertContains( + "CloudSpannerOperation.BatchUpdate", + 2, + Attributes.of( + AttributeKey.stringArrayKey("db.statement"), + ImmutableList.of(INSERT_STATEMENT.getSql(), INSERT_STATEMENT.getSql())), + spans); + assertContains("CloudSpannerOperation.Commit", spans); + assertParent( + "CloudSpannerJdbc.ReadWriteTransaction", "CloudSpanner.ReadWriteTransaction", spans); + assertParent( + "CloudSpanner.ReadWriteTransaction", + "CloudSpannerOperation.BatchUpdate", + Attributes.of( + AttributeKey.stringArrayKey("db.statement"), + ImmutableList.of(INSERT_STATEMENT.getSql(), INSERT_STATEMENT.getSql())), + spans); + assertParent("CloudSpanner.ReadWriteTransaction", "CloudSpannerOperation.Commit", spans); + } + + @Test + public void testMultiUseReadWriteAborted() { + try (Connection connection = createTestConnection()) { + connection.setAutocommit(false); + connection.setReadOnly(false); + assertEquals(1L, connection.executeUpdate(INSERT_STATEMENT)); + mockSpanner.abortNextStatement(); + connection.commit(); + } + assertEquals(CompletableResultCode.ofSuccess(), spanExporter.flush()); + List spans = spanExporter.getFinishedSpanItems(); + assertContains("CloudSpannerJdbc.ReadWriteTransaction", spans); + assertContains("CloudSpanner.ReadWriteTransaction", 1, Attributes.empty(), spans); + SpanData transactionSpan = + spans.stream() + .filter(span -> span.getName().equals("CloudSpannerJdbc.ReadWriteTransaction")) + .findFirst() + .orElseThrow(IllegalStateException::new); + assertEquals(1, transactionSpan.getTotalRecordedEvents()); + EventData event = transactionSpan.getEvents().get(0); + assertEquals( + "Transaction aborted. Backing off for 0 milliseconds and retrying.", event.getName()); + // The transaction is retried, so we get the ExecuteUpdate and Commit spans twice. + assertContains( + "CloudSpannerOperation.ExecuteUpdate", + 2, + Attributes.of(AttributeKey.stringKey("db.statement"), INSERT_STATEMENT.getSql()), + spans); + assertContains("CloudSpannerOperation.Commit", 2, Attributes.empty(), spans); + + assertParent( + "CloudSpannerJdbc.ReadWriteTransaction", "CloudSpanner.ReadWriteTransaction", spans); + assertParent( + "CloudSpanner.ReadWriteTransaction", + "CloudSpannerOperation.ExecuteUpdate", + Attributes.of(AttributeKey.stringKey("db.statement"), INSERT_STATEMENT.getSql()), + spans); + assertParent("CloudSpanner.ReadWriteTransaction", "CloudSpannerOperation.Commit", spans); + } + + @Test + public void testTransactionTag() { + try (Connection connection = createTestConnection()) { + connection.setAutocommit(false); + connection.setReadOnly(false); + connection.setTransactionTag("my_tag"); + assertEquals(1L, connection.executeUpdate(INSERT_STATEMENT)); + connection.commit(); + } + assertEquals(CompletableResultCode.ofSuccess(), spanExporter.flush()); + List spans = spanExporter.getFinishedSpanItems(); + assertContains( + "CloudSpanner.ReadWriteTransaction", + 1, + Attributes.of(AttributeKey.stringKey("transaction.tag"), "my_tag"), + spans); + } + + @Test + public void testStatementTag() { + try (Connection connection = createTestConnection()) { + connection.setAutocommit(false); + connection.setReadOnly(false); + connection.setStatementTag("my_tag"); + assertEquals(1L, connection.executeUpdate(INSERT_STATEMENT)); + connection.commit(); + } + assertEquals(CompletableResultCode.ofSuccess(), spanExporter.flush()); + List spans = spanExporter.getFinishedSpanItems(); + assertContains( + "CloudSpannerOperation.ExecuteUpdate", + 1, + Attributes.of(AttributeKey.stringKey("statement.tag"), "my_tag"), + spans); + } + + void assertContains(String expected, List spans) { + assertTrue( + "Expected " + spansToString(spans) + " to contain " + expected, + spans.stream().anyMatch(span -> span.getName().equals(expected))); + } + + void assertContains(String expected, Attributes attributes, List spans) { + assertContains(expected, 1, attributes, spans); + } + + void assertContains(String expected, int count, Attributes attributes, List spans) { + assertEquals( + "Expected " + spansToString(spans) + " to contain " + expected, + count, + spans.stream().filter(span -> equalsSpan(span, expected, attributes)).count()); + } + + boolean equalsSpan(SpanData span, String name, Attributes attributes) { + if (!span.getName().equals(name)) { + return false; + } + for (Entry, Object> entry : attributes.asMap().entrySet()) { + if (!span.getAttributes().asMap().containsKey(entry.getKey())) { + return false; + } + if (!Objects.equals(entry.getValue(), span.getAttributes().get(entry.getKey()))) { + return false; + } + } + return true; + } + + void assertParent(String expectedParent, String child, List spans) { + SpanData parentSpan = getSpan(expectedParent, spans); + SpanData childSpan = getSpan(child, spans); + assertEquals(parentSpan.getSpanId(), childSpan.getParentSpanId()); + } + + void assertParent( + String expectedParent, String child, Attributes attributes, List spans) { + SpanData parentSpan = getSpan(expectedParent, spans); + List childSpans = getSpans(child, attributes, spans); + for (SpanData childSpan : childSpans) { + assertEquals(parentSpan.getSpanId(), childSpan.getParentSpanId()); + } + } + + SpanData getSpan(String name, List spans) { + return spans.stream() + .filter(span -> span.getName().equals(name)) + .findAny() + .orElseThrow(() -> new IllegalArgumentException("Span " + name + " not found")); + } + + SpanData getSpan(String name, Attributes attributes, List spans) { + return spans.stream() + .filter(span -> equalsSpan(span, name, attributes)) + .findAny() + .orElseThrow(() -> new IllegalArgumentException("Span " + name + " not found")); + } + + List getSpans(String name, Attributes attributes, List spans) { + return spans.stream() + .filter(span -> equalsSpan(span, name, attributes)) + .collect(Collectors.toList()); + } + + private String spansToString(List spans) { + return spans.stream().map(SpanData::getName).collect(Collectors.joining("\n", "\n", "\n")); + } + + private void addUpdateDdlResponse() { + mockDatabaseAdmin.addResponse( + Operation.newBuilder() + .setMetadata( + Any.pack( + UpdateDatabaseDdlMetadata.newBuilder() + .setDatabase("projects/proj/instances/inst/databases/db") + .build())) + .setName("projects/proj/instances/inst/databases/db/operations/1") + .setDone(true) + .setResponse(Any.pack(Empty.getDefaultInstance())) + .build()); + } +} diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReadOnlyTransactionTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReadOnlyTransactionTest.java index c1419565b72..752b467b484 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReadOnlyTransactionTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReadOnlyTransactionTest.java @@ -50,6 +50,7 @@ import com.google.cloud.spanner.connection.UnitOfWork.CallType; import com.google.cloud.spanner.connection.UnitOfWork.UnitOfWorkState; import com.google.spanner.v1.ResultSetStats; +import io.opentelemetry.api.trace.Span; import java.util.Arrays; import java.util.Calendar; import java.util.List; @@ -180,6 +181,7 @@ private ReadOnlyTransaction createSubject(TimestampBound staleness) { .setBatchClient(mock(BatchClient.class)) .setReadOnlyStaleness(staleness) .withStatementExecutor(new StatementExecutor()) + .setSpan(Span.getInvalid()) .build(); } @@ -320,6 +322,7 @@ public void testExecuteQueryWithOptionsTest() { .setBatchClient(mock(BatchClient.class)) .setReadOnlyStaleness(TimestampBound.strong()) .withStatementExecutor(new StatementExecutor()) + .setSpan(Span.getInvalid()) .build(); ResultSet expectedWithOptions = DirectExecuteResultSet.ofResultSet(resWithOptions); assertThat( diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReadWriteTransactionTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReadWriteTransactionTest.java index 8e643cf6e24..136680b8a06 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReadWriteTransactionTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReadWriteTransactionTest.java @@ -62,6 +62,7 @@ import io.grpc.Metadata; import io.grpc.StatusRuntimeException; import io.grpc.protobuf.ProtoUtils; +import io.opentelemetry.api.trace.Span; import java.math.BigDecimal; import java.util.Arrays; import java.util.Collections; @@ -176,6 +177,7 @@ private ReadWriteTransaction createSubject( .setSavepointSupport(SavepointSupport.FAIL_AFTER_ROLLBACK) .setTransactionRetryListeners(Collections.emptyList()) .withStatementExecutor(new StatementExecutor()) + .setSpan(Span.getInvalid()) .build(); } @@ -475,6 +477,7 @@ public void testRetry() { .setTransactionRetryListeners(Collections.emptyList()) .setDatabaseClient(client) .withStatementExecutor(new StatementExecutor()) + .setSpan(Span.getInvalid()) .build(); subject.executeUpdateAsync(CallType.SYNC, update1); subject.executeUpdateAsync(CallType.SYNC, update2); @@ -503,6 +506,7 @@ public void testChecksumResultSet() { .setTransactionRetryListeners(Collections.emptyList()) .setDatabaseClient(client) .withStatementExecutor(new StatementExecutor()) + .setSpan(Span.getInvalid()) .build(); ParsedStatement parsedStatement = mock(ParsedStatement.class); Statement statement = Statement.of("SELECT * FROM FOO"); @@ -737,6 +741,7 @@ public void testChecksumResultSetWithArray() { .setTransactionRetryListeners(Collections.emptyList()) .setDatabaseClient(client) .withStatementExecutor(new StatementExecutor()) + .setSpan(Span.getInvalid()) .build(); ParsedStatement parsedStatement = mock(ParsedStatement.class); Statement statement = Statement.of("SELECT * FROM FOO"); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/SavepointTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/SavepointTest.java index e5ec5c9478c..1ea76845c40 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/SavepointTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/SavepointTest.java @@ -24,6 +24,7 @@ import com.google.cloud.spanner.SpannerException; import com.google.cloud.spanner.connection.AbstractMultiUseTransaction.Savepoint; import com.google.common.collect.ImmutableList; +import io.opentelemetry.api.trace.Span; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -32,7 +33,10 @@ public class SavepointTest { static class TestTransaction extends ReadOnlyTransaction { TestTransaction() { - super(ReadOnlyTransaction.newBuilder().withStatementExecutor(mock(StatementExecutor.class))); + super( + ReadOnlyTransaction.newBuilder() + .setSpan(Span.getInvalid()) + .withStatementExecutor(mock(StatementExecutor.class))); } } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/SingleUseTransactionTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/SingleUseTransactionTest.java index 60a5ede129a..0ab419f294b 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/SingleUseTransactionTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/SingleUseTransactionTest.java @@ -59,6 +59,7 @@ import com.google.common.base.Preconditions; import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata; import com.google.spanner.v1.ResultSetStats; +import io.opentelemetry.api.trace.Span; import java.util.Arrays; import java.util.Calendar; import java.util.Collections; @@ -452,6 +453,7 @@ public TransactionRunner allowNestedTransaction() { .setStatementTimeout( timeout == 0L ? nullTimeout() : timeout(timeout, TimeUnit.MILLISECONDS)) .withStatementExecutor(executor) + .setSpan(Span.getInvalid()) .build(); } @@ -616,6 +618,7 @@ public void testExecuteQueryWithOptionsTest() { .setAutocommitDmlMode(AutocommitDmlMode.TRANSACTIONAL) .withStatementExecutor(executor) .setReadOnlyStaleness(TimestampBound.strong()) + .setSpan(Span.getInvalid()) .build(); assertThat( get( diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITProtoColumnTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITProtoColumnTest.java index 57d4d465aeb..7e525ebaa15 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITProtoColumnTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITProtoColumnTest.java @@ -21,7 +21,6 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThrows; import static org.junit.Assume.assumeFalse; -import static org.junit.Assume.assumeTrue; import com.google.cloud.ByteArray; import com.google.cloud.spanner.Database; @@ -70,17 +69,10 @@ public class ITProtoColumnTest { private static DatabaseAdminClient dbAdminClient; private static DatabaseClient databaseClient; - public static boolean isUsingAllowlistedProject() { - String projectId = System.getProperty("spanner.gce.config.project_id", ""); - return projectId.equalsIgnoreCase("gcloud-devel") - || projectId.equalsIgnoreCase("span-cloud-testing"); - } - @BeforeClass public static void setUpDatabase() throws Exception { assumeFalse( "Proto Column is not supported in the emulator", EmulatorSpannerHelper.isUsingEmulator()); - assumeTrue("Proto Column is not yet enabled in production", isUsingAllowlistedProject()); RemoteSpannerHelper testHelper = env.getTestHelper(); databaseID = DatabaseId.of(testHelper.getInstanceId(), testHelper.getUniqueDatabaseId()); dbAdminClient = testHelper.getClient().getDatabaseAdminClient(); @@ -101,15 +93,15 @@ public static void createDatabase() throws Exception { databaseToCreate, Arrays.asList( "CREATE PROTO BUNDLE (" - + "spanner.examples.music.SingerInfo," - + "spanner.examples.music.Genre," + + "examples.spanner.music.SingerInfo," + + "examples.spanner.music.Genre," + ")", "CREATE TABLE Singers (" + " SingerId INT64 NOT NULL," + " FirstName STRING(1024)," + " LastName STRING(1024)," - + " SingerInfo spanner.examples.music.SingerInfo," - + " SingerGenre spanner.examples.music.Genre," + + " SingerInfo examples.spanner.music.SingerInfo," + + " SingerGenre examples.spanner.music.Genre," + " SingerNationality STRING(1024) AS (SingerInfo.nationality) STORED," + " ) PRIMARY KEY (SingerNationality, SingerGenre)", "CREATE TABLE Types (" @@ -118,10 +110,10 @@ public static void createDatabase() throws Exception { + " Bytes BYTES(MAX)," + " Int64Array ARRAY," + " BytesArray ARRAY," - + " ProtoMessage spanner.examples.music.SingerInfo," - + " ProtoEnum spanner.examples.music.Genre," - + " ProtoMessageArray ARRAY," - + " ProtoEnumArray ARRAY," + + " ProtoMessage examples.spanner.music.SingerInfo," + + " ProtoEnum examples.spanner.music.Genre," + + " ProtoMessageArray ARRAY," + + " ProtoEnumArray ARRAY," + " ) PRIMARY KEY (RowID)", "CREATE INDEX SingerByNationalityAndGenre ON Singers(SingerNationality, SingerGenre)" + " STORING (SingerId, FirstName, LastName)")) @@ -139,7 +131,7 @@ public static void createDatabase() throws Exception { @AfterClass public static void afterClass() throws Exception { try { - if (!isUsingEmulator() && isUsingAllowlistedProject()) { + if (!isUsingEmulator()) { dbAdminClient.dropDatabase( databaseID.getInstanceId().getInstance(), databaseID.getDatabase()); } @@ -169,7 +161,6 @@ public void after() throws Exception { public void testProtoColumnsUpdateAndRead() { assumeFalse( "Proto Column is not supported in the emulator", EmulatorSpannerHelper.isUsingEmulator()); - assumeTrue("Proto Column is not yet enabled in production", isUsingAllowlistedProject()); SingerInfo singerInfo = SingerInfo.newBuilder().setSingerId(1).setNationality("Country1").build(); ByteArray singerInfoBytes = ByteArray.copyFrom(singerInfo.toByteArray()); @@ -277,7 +268,6 @@ public void testProtoColumnsUpdateAndRead() { public void testProtoColumnsDMLParameterizedQueriesPKAndIndexes() { assumeFalse( "Proto Column is not supported in the emulator", EmulatorSpannerHelper.isUsingEmulator()); - assumeTrue("Proto Column is not yet enabled in production", isUsingAllowlistedProject()); SingerInfo singerInfo1 = SingerInfo.newBuilder().setSingerId(1).setNationality("Country1").build(); @@ -384,7 +374,6 @@ public void testProtoColumnsDMLParameterizedQueriesPKAndIndexes() { public void testProtoMessageDeserializationError() { assumeFalse( "Proto Column is not supported in the emulator", EmulatorSpannerHelper.isUsingEmulator()); - assumeTrue("Proto Column is not yet enabled in production", isUsingAllowlistedProject()); SingerInfo singerInfo = SingerInfo.newBuilder().setSingerId(1).setNationality("Country1").build(); diff --git a/google-cloud-spanner/src/test/resources/com/google/cloud/spanner/README.md b/google-cloud-spanner/src/test/resources/com/google/cloud/spanner/README.md new file mode 100644 index 00000000000..967cb32a298 --- /dev/null +++ b/google-cloud-spanner/src/test/resources/com/google/cloud/spanner/README.md @@ -0,0 +1,5 @@ +#### To generate SingerProto.java and descriptors.pb file from singer.proto using `protoc` +```shell +cd google-cloud-spanner/src/test/resources/com/google/cloud/spanner +protoc --proto_path=. --include_imports --descriptor_set_out=descriptors.pb --java_out=. singer.proto +``` diff --git a/google-cloud-spanner/src/test/resources/com/google/cloud/spanner/descriptors.pb b/google-cloud-spanner/src/test/resources/com/google/cloud/spanner/descriptors.pb index 3ebb79420b3..115e5fccbb5 100644 Binary files a/google-cloud-spanner/src/test/resources/com/google/cloud/spanner/descriptors.pb and b/google-cloud-spanner/src/test/resources/com/google/cloud/spanner/descriptors.pb differ diff --git a/google-cloud-spanner/src/test/resources/com/google/cloud/spanner/singer.proto b/google-cloud-spanner/src/test/resources/com/google/cloud/spanner/singer.proto index b62b48ed67f..570a3f6f705 100644 --- a/google-cloud-spanner/src/test/resources/com/google/cloud/spanner/singer.proto +++ b/google-cloud-spanner/src/test/resources/com/google/cloud/spanner/singer.proto @@ -1,6 +1,6 @@ -syntax = "proto2"; +syntax = "proto3"; -package spanner.examples.music; +package examples.spanner.music; option java_package = "com.google.cloud.spanner"; option java_outer_classname = "SingerProto"; diff --git a/grpc-google-cloud-spanner-admin-database-v1/pom.xml b/grpc-google-cloud-spanner-admin-database-v1/pom.xml index c2dabff690d..3141df9325b 100644 --- a/grpc-google-cloud-spanner-admin-database-v1/pom.xml +++ b/grpc-google-cloud-spanner-admin-database-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc grpc-google-cloud-spanner-admin-database-v1 - 6.66.0 + 6.67.0 grpc-google-cloud-spanner-admin-database-v1 GRPC library for grpc-google-cloud-spanner-admin-database-v1 com.google.cloud google-cloud-spanner-parent - 6.66.0 + 6.67.0 diff --git a/grpc-google-cloud-spanner-admin-instance-v1/pom.xml b/grpc-google-cloud-spanner-admin-instance-v1/pom.xml index 1d37e0a7ecc..543d026ef24 100644 --- a/grpc-google-cloud-spanner-admin-instance-v1/pom.xml +++ b/grpc-google-cloud-spanner-admin-instance-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc grpc-google-cloud-spanner-admin-instance-v1 - 6.66.0 + 6.67.0 grpc-google-cloud-spanner-admin-instance-v1 GRPC library for grpc-google-cloud-spanner-admin-instance-v1 com.google.cloud google-cloud-spanner-parent - 6.66.0 + 6.67.0 diff --git a/grpc-google-cloud-spanner-executor-v1/pom.xml b/grpc-google-cloud-spanner-executor-v1/pom.xml index 02b9b9a4cf3..314730e3e39 100644 --- a/grpc-google-cloud-spanner-executor-v1/pom.xml +++ b/grpc-google-cloud-spanner-executor-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc grpc-google-cloud-spanner-executor-v1 - 6.66.0 + 6.67.0 grpc-google-cloud-spanner-executor-v1 GRPC library for google-cloud-spanner com.google.cloud google-cloud-spanner-parent - 6.66.0 + 6.67.0 diff --git a/grpc-google-cloud-spanner-v1/pom.xml b/grpc-google-cloud-spanner-v1/pom.xml index 416ec81e640..23522c79c20 100644 --- a/grpc-google-cloud-spanner-v1/pom.xml +++ b/grpc-google-cloud-spanner-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc grpc-google-cloud-spanner-v1 - 6.66.0 + 6.67.0 grpc-google-cloud-spanner-v1 GRPC library for grpc-google-cloud-spanner-v1 com.google.cloud google-cloud-spanner-parent - 6.66.0 + 6.67.0 diff --git a/pom.xml b/pom.xml index 380dd7a5a33..6813b780292 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.google.cloud google-cloud-spanner-parent pom - 6.66.0 + 6.67.0 Google Cloud Spanner Parent https://github.com/googleapis/java-spanner @@ -14,7 +14,7 @@ com.google.cloud sdk-platform-java-config - 3.30.0 + 3.30.1 @@ -61,47 +61,47 @@ com.google.api.grpc proto-google-cloud-spanner-admin-instance-v1 - 6.66.0 + 6.67.0 com.google.api.grpc proto-google-cloud-spanner-executor-v1 - 6.66.0 + 6.67.0 com.google.api.grpc grpc-google-cloud-spanner-executor-v1 - 6.66.0 + 6.67.0 com.google.api.grpc proto-google-cloud-spanner-v1 - 6.66.0 + 6.67.0 com.google.api.grpc proto-google-cloud-spanner-admin-database-v1 - 6.66.0 + 6.67.0 com.google.api.grpc grpc-google-cloud-spanner-v1 - 6.66.0 + 6.67.0 com.google.api.grpc grpc-google-cloud-spanner-admin-instance-v1 - 6.66.0 + 6.67.0 com.google.api.grpc grpc-google-cloud-spanner-admin-database-v1 - 6.66.0 + 6.67.0 com.google.cloud google-cloud-spanner - 6.66.0 + 6.67.0 diff --git a/proto-google-cloud-spanner-admin-database-v1/pom.xml b/proto-google-cloud-spanner-admin-database-v1/pom.xml index 703d3a48917..9d45999d711 100644 --- a/proto-google-cloud-spanner-admin-database-v1/pom.xml +++ b/proto-google-cloud-spanner-admin-database-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc proto-google-cloud-spanner-admin-database-v1 - 6.66.0 + 6.67.0 proto-google-cloud-spanner-admin-database-v1 PROTO library for proto-google-cloud-spanner-admin-database-v1 com.google.cloud google-cloud-spanner-parent - 6.66.0 + 6.67.0 diff --git a/proto-google-cloud-spanner-admin-instance-v1/pom.xml b/proto-google-cloud-spanner-admin-instance-v1/pom.xml index 234537b46d0..2e7ea00da31 100644 --- a/proto-google-cloud-spanner-admin-instance-v1/pom.xml +++ b/proto-google-cloud-spanner-admin-instance-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc proto-google-cloud-spanner-admin-instance-v1 - 6.66.0 + 6.67.0 proto-google-cloud-spanner-admin-instance-v1 PROTO library for proto-google-cloud-spanner-admin-instance-v1 com.google.cloud google-cloud-spanner-parent - 6.66.0 + 6.67.0 diff --git a/proto-google-cloud-spanner-executor-v1/pom.xml b/proto-google-cloud-spanner-executor-v1/pom.xml index ad9fe73b971..f86a9d02d68 100644 --- a/proto-google-cloud-spanner-executor-v1/pom.xml +++ b/proto-google-cloud-spanner-executor-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc proto-google-cloud-spanner-executor-v1 - 6.66.0 + 6.67.0 proto-google-cloud-spanner-executor-v1 Proto library for google-cloud-spanner com.google.cloud google-cloud-spanner-parent - 6.66.0 + 6.67.0 diff --git a/proto-google-cloud-spanner-v1/pom.xml b/proto-google-cloud-spanner-v1/pom.xml index 14c9f9b5ad3..81cbf177728 100644 --- a/proto-google-cloud-spanner-v1/pom.xml +++ b/proto-google-cloud-spanner-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc proto-google-cloud-spanner-v1 - 6.66.0 + 6.67.0 proto-google-cloud-spanner-v1 PROTO library for proto-google-cloud-spanner-v1 com.google.cloud google-cloud-spanner-parent - 6.66.0 + 6.67.0 diff --git a/samples/install-without-bom/pom.xml b/samples/install-without-bom/pom.xml index bc846dfdc77..2999cbebb48 100644 --- a/samples/install-without-bom/pom.xml +++ b/samples/install-without-bom/pom.xml @@ -24,7 +24,7 @@ UTF-8 0.31.1 2.41.0 - 3.43.0 + 3.44.0
@@ -33,7 +33,7 @@ com.google.cloud google-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-tests nam11 us-east1 java-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-8 0.31.1 2.41.0 - 3.43.0 + 3.44.0
@@ -32,7 +32,7 @@ com.google.cloud google-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-tests nam11 us-east1 java-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-tests nam11 us-east1 java-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