diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 445b4bf82f..6830220756 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -36,11 +36,14 @@ jobs: JOB_TYPE: test dependencies: runs-on: ubuntu-latest + strategy: + matrix: + java: [8, 11] steps: - uses: actions/checkout@v2 - uses: actions/setup-java@v1 with: - java-version: 8 + java-version: ${{matrix.java}} - run: java -version - run: .kokoro/dependencies.sh linkage-monitor: diff --git a/.github/workflows/integration-tests-against-emulator.yaml b/.github/workflows/integration-tests-against-emulator.yaml new file mode 100644 index 0000000000..adb38152d1 --- /dev/null +++ b/.github/workflows/integration-tests-against-emulator.yaml @@ -0,0 +1,29 @@ +on: + push: + branches: + - master + pull_request: +name: integration-tests-against-emulator +jobs: + units: + runs-on: ubuntu-latest + + services: + emulator: + image: gcr.io/cloud-spanner-emulator/emulator:latest + ports: + - 9010:9010 + - 9020:9020 + + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-java@v1 + with: + java-version: 8 + - run: java -version + - run: .kokoro/build.sh + - run: mvn -B -Dspanner.testenv.instance="" -Penable-integration-tests -DtrimStackTrace=false -Dclirr.skip=true -Denforcer.skip=true -fae verify + env: + JOB_TYPE: test + SPANNER_EMULATOR_HOST: localhost:9010 + GOOGLE_CLOUD_PROJECT: emulator-test-project diff --git a/.kokoro/build.sh b/.kokoro/build.sh index f46042a8f0..112a2cce85 100755 --- a/.kokoro/build.sh +++ b/.kokoro/build.sh @@ -39,7 +39,7 @@ retry_with_backoff 3 10 \ # if GOOGLE_APPLICATION_CREDIENTIALS is specified as a relative path prepend Kokoro root directory onto it if [[ ! -z "${GOOGLE_APPLICATION_CREDENTIALS}" && "${GOOGLE_APPLICATION_CREDENTIALS}" != /* ]]; then - export GOOGLE_APPLICATION_CREDENTIALS=$(realpath ${KOKORO_ROOT}/src/${GOOGLE_APPLICATION_CREDENTIALS}) + export GOOGLE_APPLICATION_CREDENTIALS=$(realpath ${KOKORO_GFILE_DIR}/${GOOGLE_APPLICATION_CREDENTIALS}) fi RETURN_CODE=0 diff --git a/.kokoro/continuous/samples/common.cfg b/.kokoro/continuous/samples/common.cfg new file mode 100644 index 0000000000..e6b577f86f --- /dev/null +++ b/.kokoro/continuous/samples/common.cfg @@ -0,0 +1,25 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "JOB_TYPE" + value: "samples" +} + +env_vars: { + key: "GCLOUD_PROJECT" + value: "gcloud-devel" +} + +env_vars: { + key: "GOOGLE_APPLICATION_CREDENTIALS" + value: "keystore/73713_java_it_service_account" +} + +before_action { + fetch_keystore { + keystore_resource { + keystore_config_id: 73713 + keyname: "java_it_service_account" + } + } +} diff --git a/.kokoro/continuous/samples/samples-java11.cfg b/.kokoro/continuous/samples/samples-java11.cfg new file mode 100644 index 0000000000..709f2b4c73 --- /dev/null +++ b/.kokoro/continuous/samples/samples-java11.cfg @@ -0,0 +1,7 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Configure the docker image for kokoro-trampoline. +env_vars: { + key: "TRAMPOLINE_IMAGE" + value: "gcr.io/cloud-devrel-kokoro-resources/java11" +} diff --git a/.kokoro/continuous/samples/samples-java8.cfg b/.kokoro/continuous/samples/samples-java8.cfg new file mode 100644 index 0000000000..3b017fc80f --- /dev/null +++ b/.kokoro/continuous/samples/samples-java8.cfg @@ -0,0 +1,7 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Configure the docker image for kokoro-trampoline. +env_vars: { + key: "TRAMPOLINE_IMAGE" + value: "gcr.io/cloud-devrel-kokoro-resources/java8" +} diff --git a/.kokoro/dependencies.sh b/.kokoro/dependencies.sh index cf3bb4347e..cee4f11e75 100755 --- a/.kokoro/dependencies.sh +++ b/.kokoro/dependencies.sh @@ -41,8 +41,10 @@ echo "****************** DEPENDENCY LIST COMPLETENESS CHECK *******************" ## Run dependency list completeness check function completenessCheck() { # Output dep list with compile scope generated using the original pom + # Running mvn dependency:list on Java versions that support modules will also include the module of the dependency. + # This is stripped from the output as it is not present in the flattened pom. msg "Generating dependency list using original pom..." - mvn dependency:list -f pom.xml -Dsort=true | grep '\[INFO] .*:.*:.*:.*:.*' | grep -v ':test$' >.org-list.txt + mvn dependency:list -f pom.xml -Dsort=true | grep '\[INFO] .*:.*:.*:.*:.*' | sed -e s/\\s--\\smodule.*// | grep -v ':test$' >.org-list.txt # Output dep list generated using the flattened pom (test scope deps are ommitted) msg "Generating dependency list using flattened pom..." diff --git a/.kokoro/nightly/integration.cfg b/.kokoro/nightly/integration.cfg index 40c4abb7bf..0048c8ece7 100644 --- a/.kokoro/nightly/integration.cfg +++ b/.kokoro/nightly/integration.cfg @@ -28,14 +28,10 @@ env_vars: { env_vars: { key: "GOOGLE_APPLICATION_CREDENTIALS" - value: "keystore/73713_java_it_service_account" + value: "secret_manager/java-it-service-account" } -before_action { - fetch_keystore { - keystore_resource { - keystore_config_id: 73713 - keyname: "java_it_service_account" - } - } +env_vars: { + key: "SECRET_MANAGER_KEYS" + value: "java-it-service-account" } diff --git a/.kokoro/nightly/samples.cfg b/.kokoro/nightly/samples.cfg index 20aabd55de..f25429314f 100644 --- a/.kokoro/nightly/samples.cfg +++ b/.kokoro/nightly/samples.cfg @@ -24,19 +24,15 @@ env_vars: { env_vars: { key: "GOOGLE_APPLICATION_CREDENTIALS" - value: "keystore/73713_java_it_service_account" + value: "secret_manager/java-docs-samples-service-account" } env_vars: { - key: "ENABLE_BUILD_COP" - value: "true" + key: "SECRET_MANAGER_KEYS" + value: "java-docs-samples-service-account" } -before_action { - fetch_keystore { - keystore_resource { - keystore_config_id: 73713 - keyname: "java_it_service_account" - } - } +env_vars: { + key: "ENABLE_BUILD_COP" + value: "true" } diff --git a/.kokoro/nightly/samples/common.cfg b/.kokoro/nightly/samples/common.cfg new file mode 100644 index 0000000000..be513e11d6 --- /dev/null +++ b/.kokoro/nightly/samples/common.cfg @@ -0,0 +1,32 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "JOB_TYPE" + value: "samples" +} + +# TODO: remove this after we've migrated all tests and scripts +env_vars: { + key: "GCLOUD_PROJECT" + value: "java-docs-samples-testing" +} + +env_vars: { + key: "GOOGLE_CLOUD_PROJECT" + value: "java-docs-samples-testing" +} + +env_vars: { + key: "GOOGLE_APPLICATION_CREDENTIALS" + value: "secret_manager/java-docs-samples-service-account" +} + +env_vars: { + key: "SECRET_MANAGER_KEYS" + value: "java-docs-samples-service-account" +} + +env_vars: { + key: "ENABLE_BUILD_COP" + value: "true" +} diff --git a/.kokoro/nightly/samples/samples-java11.cfg b/.kokoro/nightly/samples/samples-java11.cfg new file mode 100644 index 0000000000..709f2b4c73 --- /dev/null +++ b/.kokoro/nightly/samples/samples-java11.cfg @@ -0,0 +1,7 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Configure the docker image for kokoro-trampoline. +env_vars: { + key: "TRAMPOLINE_IMAGE" + value: "gcr.io/cloud-devrel-kokoro-resources/java11" +} diff --git a/.kokoro/nightly/samples/samples-java8.cfg b/.kokoro/nightly/samples/samples-java8.cfg new file mode 100644 index 0000000000..3b017fc80f --- /dev/null +++ b/.kokoro/nightly/samples/samples-java8.cfg @@ -0,0 +1,7 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Configure the docker image for kokoro-trampoline. +env_vars: { + key: "TRAMPOLINE_IMAGE" + value: "gcr.io/cloud-devrel-kokoro-resources/java8" +} diff --git a/.kokoro/populate-secrets.sh b/.kokoro/populate-secrets.sh new file mode 100755 index 0000000000..f52514257e --- /dev/null +++ b/.kokoro/populate-secrets.sh @@ -0,0 +1,43 @@ +#!/bin/bash +# Copyright 2020 Google LLC. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -eo pipefail + +function now { date +"%Y-%m-%d %H:%M:%S" | tr -d '\n' ;} +function msg { println "$*" >&2 ;} +function println { printf '%s\n' "$(now) $*" ;} + + +# Populates requested secrets set in SECRET_MANAGER_KEYS from service account: +# kokoro-trampoline@cloud-devrel-kokoro-resources.iam.gserviceaccount.com +SECRET_LOCATION="${KOKORO_GFILE_DIR}/secret_manager" +msg "Creating folder on disk for secrets: ${SECRET_LOCATION}" +mkdir -p ${SECRET_LOCATION} +for key in $(echo ${SECRET_MANAGER_KEYS} | sed "s/,/ /g") +do + msg "Retrieving secret ${key}" + docker run --entrypoint=gcloud \ + --volume=${KOKORO_GFILE_DIR}:${KOKORO_GFILE_DIR} \ + gcr.io/google.com/cloudsdktool/cloud-sdk \ + secrets versions access latest \ + --project cloud-devrel-kokoro-resources \ + --secret ${key} > \ + "${SECRET_LOCATION}/${key}" + if [[ $? == 0 ]]; then + msg "Secret written to ${SECRET_LOCATION}/${key}" + else + msg "Error retrieving secret ${key}" + fi +done diff --git a/.kokoro/presubmit/integration.cfg b/.kokoro/presubmit/integration.cfg index 522e5b1010..dded67a9d5 100644 --- a/.kokoro/presubmit/integration.cfg +++ b/.kokoro/presubmit/integration.cfg @@ -24,14 +24,10 @@ env_vars: { env_vars: { key: "GOOGLE_APPLICATION_CREDENTIALS" - value: "keystore/73713_java_it_service_account" + value: "secret_manager/java-it-service-account" } -before_action { - fetch_keystore { - keystore_resource { - keystore_config_id: 73713 - keyname: "java_it_service_account" - } - } +env_vars: { + key: "SECRET_MANAGER_KEYS" + value: "java-it-service-account" } diff --git a/.kokoro/presubmit/samples.cfg b/.kokoro/presubmit/samples.cfg index 1171aead01..01e0960047 100644 --- a/.kokoro/presubmit/samples.cfg +++ b/.kokoro/presubmit/samples.cfg @@ -24,14 +24,10 @@ env_vars: { env_vars: { key: "GOOGLE_APPLICATION_CREDENTIALS" - value: "keystore/73713_java_it_service_account" + value: "secret_manager/java-docs-samples-service-account" } -before_action { - fetch_keystore { - keystore_resource { - keystore_config_id: 73713 - keyname: "java_it_service_account" - } - } -} +env_vars: { + key: "SECRET_MANAGER_KEYS" + value: "java-docs-samples-service-account" +} \ No newline at end of file diff --git a/.kokoro/presubmit/samples/common.cfg b/.kokoro/presubmit/samples/common.cfg new file mode 100644 index 0000000000..51c9224422 --- /dev/null +++ b/.kokoro/presubmit/samples/common.cfg @@ -0,0 +1,27 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +env_vars: { + key: "JOB_TYPE" + value: "samples" +} + +# TODO: remove this after we've migrated all tests and scripts +env_vars: { + key: "GCLOUD_PROJECT" + value: "java-docs-samples-testing" +} + +env_vars: { + key: "GOOGLE_CLOUD_PROJECT" + value: "java-docs-samples-testing" +} + +env_vars: { + key: "GOOGLE_APPLICATION_CREDENTIALS" + value: "secret_manager/java-docs-samples-service-account" +} + +env_vars: { + key: "SECRET_MANAGER_KEYS" + value: "java-docs-samples-service-account" +} diff --git a/.kokoro/presubmit/samples/samples-java11.cfg b/.kokoro/presubmit/samples/samples-java11.cfg new file mode 100644 index 0000000000..709f2b4c73 --- /dev/null +++ b/.kokoro/presubmit/samples/samples-java11.cfg @@ -0,0 +1,7 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Configure the docker image for kokoro-trampoline. +env_vars: { + key: "TRAMPOLINE_IMAGE" + value: "gcr.io/cloud-devrel-kokoro-resources/java11" +} diff --git a/.kokoro/presubmit/samples/samples-java8.cfg b/.kokoro/presubmit/samples/samples-java8.cfg new file mode 100644 index 0000000000..3b017fc80f --- /dev/null +++ b/.kokoro/presubmit/samples/samples-java8.cfg @@ -0,0 +1,7 @@ +# Format: //devtools/kokoro/config/proto/build.proto + +# Configure the docker image for kokoro-trampoline. +env_vars: { + key: "TRAMPOLINE_IMAGE" + value: "gcr.io/cloud-devrel-kokoro-resources/java8" +} diff --git a/.kokoro/trampoline.sh b/.kokoro/trampoline.sh index ba17ce0146..9da0f83987 100644 --- a/.kokoro/trampoline.sh +++ b/.kokoro/trampoline.sh @@ -21,4 +21,6 @@ function cleanup() { echo "cleanup"; } trap cleanup EXIT + +$(dirname $0)/populate-secrets.sh # Secret Manager secrets. python3 "${KOKORO_GFILE_DIR}/trampoline_v1.py" diff --git a/CHANGELOG.md b/CHANGELOG.md index 5aa75531c6..c5d23b9efa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,57 @@ # Changelog +## [1.58.0](https://www.github.com/googleapis/java-spanner/compare/v1.57.0...v1.58.0) (2020-07-07) + + +### Features + +* add async api ([#81](https://www.github.com/googleapis/java-spanner/issues/81)) ([462839b](https://www.github.com/googleapis/java-spanner/commit/462839b625e58e235581b8ba10b398e1d222eaaf)) +* support setting compression option ([#192](https://www.github.com/googleapis/java-spanner/issues/192)) ([965e95e](https://www.github.com/googleapis/java-spanner/commit/965e95e70ccd9c62abd6513b0011aab136e48e26)) + + +### Bug Fixes + +* set default values for streaming retry ([#316](https://www.github.com/googleapis/java-spanner/issues/316)) ([543373b](https://www.github.com/googleapis/java-spanner/commit/543373b22336be72b10026fda9f0b55939ab94b4)) + + +### Performance Improvements + +* use streaming RPC for PDML ([#287](https://www.github.com/googleapis/java-spanner/issues/287)) ([df47c13](https://www.github.com/googleapis/java-spanner/commit/df47c13a4c00bdf5e6eafa01bbb64c12a96d7fb8)) + + +### Dependencies + +* update dependency com.google.cloud:google-cloud-shared-dependencies to v0.8.2 ([#315](https://www.github.com/googleapis/java-spanner/issues/315)) ([3d6fb9f](https://www.github.com/googleapis/java-spanner/commit/3d6fb9fd7dc6b2b5b2ff9935228701ac795c9167)) + +## [1.57.0](https://www.github.com/googleapis/java-spanner/compare/v1.56.0...v1.57.0) (2020-06-29) + + +### Features + +* **deps:** adopt flatten plugin and google-cloud-shared-dependencies and update ExecutorProvider ([#302](https://www.github.com/googleapis/java-spanner/issues/302)) ([5aef6c3](https://www.github.com/googleapis/java-spanner/commit/5aef6c3f6d3e9564cb8728ad51718feb6b64475a)) + +## [1.56.0](https://www.github.com/googleapis/java-spanner/compare/v1.55.1...v1.56.0) (2020-06-17) + + +### Features + +* add num_sessions_in_pool metric ([#128](https://www.github.com/googleapis/java-spanner/issues/128)) ([3a7a8ad](https://www.github.com/googleapis/java-spanner/commit/3a7a8ad79f1de3371d32a1298406990cb7bbf5be)) + + +### Bug Fixes + +* backend now supports optimizer version for DML ([#252](https://www.github.com/googleapis/java-spanner/issues/252)) ([24b986b](https://www.github.com/googleapis/java-spanner/commit/24b986b03a785f4c5ee978dcdc57f51687701e52)) +* include an explicit version for javax-annotations-api ([#261](https://www.github.com/googleapis/java-spanner/issues/261)) ([e256d22](https://www.github.com/googleapis/java-spanner/commit/e256d22f33d5f091ea90ed81c0b0f8600beae96c)) +* inconsistent json and yaml spanner configs ([#238](https://www.github.com/googleapis/java-spanner/issues/238)) ([627fdc1](https://www.github.com/googleapis/java-spanner/commit/627fdc13d64ab7b51934d4866ff753f7b08dabe4)) +* test allowed a too old staleness ([#214](https://www.github.com/googleapis/java-spanner/issues/214)) ([f4fa6bf](https://www.github.com/googleapis/java-spanner/commit/f4fa6bfca4bb821cbda426c4cb7bf32f091a2913)) +* use millis to prevent rounding errors ([#260](https://www.github.com/googleapis/java-spanner/issues/260)) ([22ed458](https://www.github.com/googleapis/java-spanner/commit/22ed45816098f5e50104935b66bc55297ea7f7b7)) + + +### Dependencies + +* include test-jar in bom ([#253](https://www.github.com/googleapis/java-spanner/issues/253)) ([4e86a37](https://www.github.com/googleapis/java-spanner/commit/4e86a374aacbcfc34d64809b7d9606f21176f6b9)) +* update dependency org.json:json to v20200518 ([#239](https://www.github.com/googleapis/java-spanner/issues/239)) ([e3d7921](https://www.github.com/googleapis/java-spanner/commit/e3d79214ac4d6e72992acdddb7ddeb2148b1ae15)) + ### [1.55.1](https://www.github.com/googleapis/java-spanner/compare/v1.55.0...v1.55.1) (2020-05-21) diff --git a/README.md b/README.md index d0334a6260..23bd5c6cde 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ If you are using Maven with [BOM][libraries-bom], add this to your pom.xml file com.google.cloud libraries-bom - 5.7.0 + 8.0.0 pom import @@ -38,7 +38,7 @@ If you are using Maven without BOM, add this to your dependencies: com.google.cloud google-cloud-spanner - 1.55.1 + 1.58.0 ``` @@ -47,11 +47,11 @@ If you are using Maven without BOM, add this to your dependencies: If you are using Gradle, add this to your dependencies ```Groovy -compile 'com.google.cloud:google-cloud-spanner:1.55.1' +compile 'com.google.cloud:google-cloud-spanner:1.58.0' ``` If you are using SBT, add this to your dependencies ```Scala -libraryDependencies += "com.google.cloud" % "google-cloud-spanner" % "1.55.1" +libraryDependencies += "com.google.cloud" % "google-cloud-spanner" % "1.58.0" ``` [//]: # ({x-version-update-end}) diff --git a/google-cloud-spanner-bom/pom.xml b/google-cloud-spanner-bom/pom.xml index 53e8fac348..e736a74215 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 - 1.55.2-SNAPSHOT + 1.58.1-SNAPSHOT pom com.google.cloud google-cloud-shared-config - 0.8.0 + 0.9.2 Google Cloud Spanner BOM @@ -64,43 +64,43 @@ com.google.api.grpc proto-google-cloud-spanner-admin-instance-v1 - 1.55.2-SNAPSHOT + 1.58.1-SNAPSHOT com.google.api.grpc grpc-google-cloud-spanner-v1 - 1.55.2-SNAPSHOT + 1.58.1-SNAPSHOT com.google.api.grpc proto-google-cloud-spanner-v1 - 1.55.2-SNAPSHOT + 1.58.1-SNAPSHOT com.google.api.grpc proto-google-cloud-spanner-admin-database-v1 - 1.55.2-SNAPSHOT + 1.58.1-SNAPSHOT com.google.cloud google-cloud-spanner - 1.55.2-SNAPSHOT + 1.58.1-SNAPSHOT com.google.cloud google-cloud-spanner test-jar - 1.55.1 + 1.58.1-SNAPSHOT com.google.api.grpc grpc-google-cloud-spanner-admin-instance-v1 - 1.55.2-SNAPSHOT + 1.58.1-SNAPSHOT com.google.api.grpc grpc-google-cloud-spanner-admin-database-v1 - 1.55.2-SNAPSHOT + 1.58.1-SNAPSHOT diff --git a/google-cloud-spanner/clirr-ignored-differences.xml b/google-cloud-spanner/clirr-ignored-differences.xml index 4a089a5682..13462a986c 100644 --- a/google-cloud-spanner/clirr-ignored-differences.xml +++ b/google-cloud-spanner/clirr-ignored-differences.xml @@ -93,7 +93,6 @@ com/google/cloud/spanner/DatabaseAdminClient com.google.cloud.spanner.Backup updateBackup(java.lang.String, java.lang.String, com.google.cloud.Timestamp) - 7012 com/google/cloud/spanner/spi/v1/SpannerRpc @@ -147,6 +146,88 @@ com.google.api.gax.paging.Page listDatabases() + + + 7012 + com/google/cloud/spanner/spi/v1/SpannerRpc + com.google.api.core.ApiFuture executeQueryAsync(com.google.spanner.v1.ExecuteSqlRequest, java.util.Map) + + + 7012 + com/google/cloud/spanner/DatabaseClient + * runAsync(*) + + + 7012 + com/google/cloud/spanner/DatabaseClient + * transactionManagerAsync(*) + + + 7012 + com/google/cloud/spanner/Spanner + * getAsyncExecutorProvider(*) + + + 7012 + com/google/cloud/spanner/ReadContext + * executeQueryAsync(*) + + + 7012 + com/google/cloud/spanner/ReadContext + * readAsync(*) + + + 7012 + com/google/cloud/spanner/ReadContext + * readRowAsync(*) + + + 7012 + com/google/cloud/spanner/ReadContext + * readUsingIndexAsync(*) + + + 7012 + com/google/cloud/spanner/ReadContext + * readRowUsingIndexAsync(*) + + + 7012 + com/google/cloud/spanner/TransactionContext + * batchUpdateAsync(*) + + + 7012 + com/google/cloud/spanner/TransactionContext + * executeUpdateAsync(*) + + + 7012 + com/google/cloud/spanner/spi/v1/SpannerRpc + * beginTransactionAsync(*) + + + 7012 + com/google/cloud/spanner/spi/v1/SpannerRpc + * commitAsync(*) + + + 7012 + com/google/cloud/spanner/spi/v1/SpannerRpc + * rollbackAsync(*) + + + 7012 + com/google/cloud/spanner/spi/v1/SpannerRpc + * executeBatchDmlAsync(*) + + + 7012 + com/google/cloud/spanner/connection/Connection + * executeQueryAsync(*) + + 7012 @@ -176,4 +257,10 @@ com.google.api.gax.retrying.RetrySettings getPartitionedDmlRetrySettings() + + + 7012 + com/google/cloud/spanner/spi/v1/SpannerRpc + com.google.api.gax.rpc.ServerStream executeStreamingPartitionedDml(com.google.spanner.v1.ExecuteSqlRequest, java.util.Map, org.threeten.bp.Duration) + diff --git a/google-cloud-spanner/pom.xml b/google-cloud-spanner/pom.xml index 9aec315b34..5101cf7396 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 - 1.55.2-SNAPSHOT + 1.58.1-SNAPSHOT jar Google Cloud Spanner https://github.com/googleapis/java-spanner @@ -11,7 +11,7 @@ com.google.cloud google-cloud-spanner-parent - 1.55.2-SNAPSHOT + 1.58.1-SNAPSHOT google-cloud-spanner @@ -102,21 +102,11 @@ - - org.apache.maven.plugins - maven-surefire-plugin - 3.0.0-M4 - - - org.apache.maven.plugins - maven-failsafe-plugin - 3.0.0-M4 - org.apache.maven.plugins maven-dependency-plugin - io.grpc:grpc-protobuf-lite,org.hamcrest:hamcrest,org.hamcrest:hamcrest-core,com.google.errorprone:error_prone_annotations,org.openjdk.jmh:jmh-generator-annprocess,com.google.api.grpc:grpc-google-cloud-spanner-v1,com.google.api.grpc:grpc-google-cloud-spanner-admin-instance-v1,com.google.api.grpc:grpc-google-cloud-spanner-admin-database-v1 + io.grpc:grpc-protobuf-lite,org.hamcrest:hamcrest,org.hamcrest:hamcrest-core,com.google.errorprone:error_prone_annotations,org.openjdk.jmh:jmh-generator-annprocess,com.google.api.grpc:grpc-google-cloud-spanner-v1,com.google.api.grpc:grpc-google-cloud-spanner-admin-instance-v1,com.google.api.grpc:grpc-google-cloud-spanner-admin-database-v1,javax.annotation:javax.annotation-api 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 685e9a1e31..bc4a868564 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 @@ -21,16 +21,24 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutureCallback; +import com.google.api.core.ApiFutures; +import com.google.api.core.SettableApiFuture; +import com.google.api.gax.core.ExecutorProvider; import com.google.cloud.Timestamp; import com.google.cloud.spanner.AbstractResultSet.CloseableIterator; import com.google.cloud.spanner.AbstractResultSet.GrpcResultSet; import com.google.cloud.spanner.AbstractResultSet.GrpcStreamIterator; import com.google.cloud.spanner.AbstractResultSet.ResumableStreamIterator; +import com.google.cloud.spanner.AsyncResultSet.CallbackResponse; +import com.google.cloud.spanner.AsyncResultSet.ReadyCallback; import com.google.cloud.spanner.Options.QueryOption; import com.google.cloud.spanner.Options.ReadOption; import com.google.cloud.spanner.SessionImpl.SessionTransaction; import com.google.cloud.spanner.spi.v1.SpannerRpc; import com.google.common.annotations.VisibleForTesting; +import com.google.common.util.concurrent.MoreExecutors; import com.google.protobuf.ByteString; import com.google.spanner.v1.BeginTransactionRequest; import com.google.spanner.v1.ExecuteBatchDmlRequest; @@ -62,6 +70,7 @@ abstract static class Builder, T extends AbstractReadCon private Span span = Tracing.getTracer().getCurrentSpan(); private int defaultPrefetchChunks = SpannerOptions.Builder.DEFAULT_PREFETCH_CHUNKS; private QueryOptions defaultQueryOptions = SpannerOptions.Builder.DEFAULT_QUERY_OPTIONS; + private ExecutorProvider executorProvider; Builder() {} @@ -95,9 +104,25 @@ B setDefaultQueryOptions(QueryOptions defaultQueryOptions) { return self(); } + B setExecutorProvider(ExecutorProvider executorProvider) { + this.executorProvider = executorProvider; + return self(); + } + abstract T build(); } + /** + * {@link AsyncResultSet} that supports adding listeners that are called when all rows from the + * underlying result stream have been fetched. + */ + interface ListenableAsyncResultSet extends AsyncResultSet { + /** Adds a listener to this {@link AsyncResultSet}. */ + void addListener(Runnable listener); + + void removeListener(Runnable listener); + } + /** * A {@code ReadContext} for standalone reads. This can only be used for a single operation, since * each standalone read may see a different timestamp of Cloud Spanner data. @@ -350,7 +375,8 @@ void initTransaction() { final Object lock = new Object(); final SessionImpl session; final SpannerRpc rpc; - final Span span; + final ExecutorProvider executorProvider; + Span span; private final int defaultPrefetchChunks; private final QueryOptions defaultQueryOptions; @@ -374,6 +400,12 @@ void initTransaction() { this.defaultPrefetchChunks = builder.defaultPrefetchChunks; this.defaultQueryOptions = builder.defaultQueryOptions; this.span = builder.span; + this.executorProvider = builder.executorProvider; + } + + @Override + public void setSpan(Span span) { + this.span = span; } long getSeqNo() { @@ -386,12 +418,38 @@ public final ResultSet read( return readInternal(table, null, keys, columns, options); } + @Override + public ListenableAsyncResultSet readAsync( + String table, KeySet keys, Iterable columns, ReadOption... options) { + Options readOptions = Options.fromReadOptions(options); + final int bufferRows = + readOptions.hasBufferRows() + ? readOptions.bufferRows() + : AsyncResultSetImpl.DEFAULT_BUFFER_SIZE; + return new AsyncResultSetImpl( + executorProvider, readInternal(table, null, keys, columns, options), bufferRows); + } + @Override public final ResultSet readUsingIndex( String table, String index, KeySet keys, Iterable columns, ReadOption... options) { return readInternal(table, checkNotNull(index), keys, columns, options); } + @Override + public ListenableAsyncResultSet readUsingIndexAsync( + String table, String index, KeySet keys, Iterable columns, ReadOption... options) { + Options readOptions = Options.fromReadOptions(options); + final int bufferRows = + readOptions.hasBufferRows() + ? readOptions.bufferRows() + : AsyncResultSetImpl.DEFAULT_BUFFER_SIZE; + return new AsyncResultSetImpl( + executorProvider, + readInternal(table, checkNotNull(index), keys, columns, options), + bufferRows); + } + @Nullable @Override public final Struct readRow(String table, Key key, Iterable columns) { @@ -400,6 +458,13 @@ public final Struct readRow(String table, Key key, Iterable columns) { } } + @Override + public final ApiFuture readRowAsync(String table, Key key, Iterable columns) { + try (AsyncResultSet resultSet = readAsync(table, KeySet.singleKey(key), columns)) { + return consumeSingleRowAsync(resultSet); + } + } + @Nullable @Override public final Struct readRowUsingIndex( @@ -409,12 +474,35 @@ public final Struct readRowUsingIndex( } } + @Override + public final ApiFuture readRowUsingIndexAsync( + String table, String index, Key key, Iterable columns) { + try (AsyncResultSet resultSet = + readUsingIndexAsync(table, index, KeySet.singleKey(key), columns)) { + return consumeSingleRowAsync(resultSet); + } + } + @Override public final ResultSet executeQuery(Statement statement, QueryOption... options) { return executeQueryInternal( statement, com.google.spanner.v1.ExecuteSqlRequest.QueryMode.NORMAL, options); } + @Override + public ListenableAsyncResultSet executeQueryAsync(Statement statement, QueryOption... options) { + Options readOptions = Options.fromQueryOptions(options); + final int bufferRows = + readOptions.hasBufferRows() + ? readOptions.bufferRows() + : AsyncResultSetImpl.DEFAULT_BUFFER_SIZE; + return new AsyncResultSetImpl( + executorProvider, + executeQueryInternal( + statement, com.google.spanner.v1.ExecuteSqlRequest.QueryMode.NORMAL, options), + bufferRows); + } + @Override public final ResultSet analyzeQuery(Statement statement, QueryAnalyzeMode readContextQueryMode) { switch (readContextQueryMode) { @@ -666,4 +754,71 @@ private Struct consumeSingleRow(ResultSet resultSet) { } return row; } + + static ApiFuture consumeSingleRowAsync(AsyncResultSet resultSet) { + final SettableApiFuture result = SettableApiFuture.create(); + // We can safely use a directExecutor here, as we will only be consuming one row, and we will + // not be doing any blocking stuff in the handler. + final SettableApiFuture row = SettableApiFuture.create(); + ApiFutures.addCallback( + resultSet.setCallback(MoreExecutors.directExecutor(), ConsumeSingleRowCallback.create(row)), + new ApiFutureCallback() { + @Override + public void onFailure(Throwable t) { + result.setException(t); + } + + @Override + public void onSuccess(Void input) { + try { + result.set(row.get()); + } catch (Throwable t) { + result.setException(t); + } + } + }, + MoreExecutors.directExecutor()); + return result; + } + + /** + * {@link ReadyCallback} for returning the first row in a result set as a future {@link Struct}. + */ + private static class ConsumeSingleRowCallback implements ReadyCallback { + private final SettableApiFuture result; + private Struct row; + + static ConsumeSingleRowCallback create(SettableApiFuture result) { + return new ConsumeSingleRowCallback(result); + } + + private ConsumeSingleRowCallback(SettableApiFuture result) { + this.result = result; + } + + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + try { + switch (resultSet.tryNext()) { + case DONE: + result.set(row); + return CallbackResponse.DONE; + case NOT_READY: + return CallbackResponse.CONTINUE; + case OK: + if (row != null) { + throw newSpannerException( + ErrorCode.INTERNAL, "Multiple rows returned for single key"); + } + row = resultSet.getCurrentRowAsStruct(); + return CallbackResponse.CONTINUE; + default: + throw new IllegalStateException(); + } + } catch (Throwable t) { + result.setException(t); + return CallbackResponse.DONE; + } + } + } } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractResultSet.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractResultSet.java index 7b248bfb9d..1dfea318e0 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractResultSet.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractResultSet.java @@ -495,7 +495,7 @@ private static Struct decodeStructValue(Type structType, ListValue structValue) return new GrpcStruct(structType, fields); } - private static Object decodeArrayValue(Type elementType, ListValue listValue) { + static Object decodeArrayValue(Type elementType, ListValue listValue) { switch (elementType.getCode()) { case BOOL: // Use a view: element conversion is virtually free. @@ -855,8 +855,9 @@ private static ExponentialBackOff newBackOff() { return new ExponentialBackOff.Builder() .setMultiplier(STREAMING_RETRY_SETTINGS.getRetryDelayMultiplier()) .setInitialIntervalMillis( - (int) STREAMING_RETRY_SETTINGS.getInitialRetryDelay().toMillis()) - .setMaxIntervalMillis((int) STREAMING_RETRY_SETTINGS.getMaxRetryDelay().toMillis()) + Math.max(10, (int) STREAMING_RETRY_SETTINGS.getInitialRetryDelay().toMillis())) + .setMaxIntervalMillis( + Math.max(1000, (int) STREAMING_RETRY_SETTINGS.getMaxRetryDelay().toMillis())) .setMaxElapsedTimeMillis(Integer.MAX_VALUE) // Prevent Backoff.STOP from getting returned. .build(); } @@ -1009,7 +1010,7 @@ protected PartialResultSet computeNext() { } } - private static double valueProtoToFloat64(com.google.protobuf.Value proto) { + static double valueProtoToFloat64(com.google.protobuf.Value proto) { if (proto.getKindCase() == KindCase.STRING_VALUE) { switch (proto.getStringValue()) { case "-Infinity": @@ -1037,7 +1038,7 @@ private static double valueProtoToFloat64(com.google.protobuf.Value proto) { return proto.getNumberValue(); } - private static NullPointerException throwNotNull(int columnIndex) { + static NullPointerException throwNotNull(int columnIndex) { throw new NullPointerException( "Cannot call array getter for column " + columnIndex + " with null elements"); } @@ -1048,7 +1049,7 @@ private static NullPointerException throwNotNull(int columnIndex) { * {@code BigDecimal} respectively. Rather than construct new wrapper objects for each array * element, we use primitive arrays and a {@code BitSet} to track nulls. */ - private abstract static class PrimitiveArray extends AbstractList { + abstract static class PrimitiveArray extends AbstractList { private final A data; private final BitSet nulls; private final int size; @@ -1103,7 +1104,7 @@ A toPrimitiveArray(int columnIndex) { } } - private static class Int64Array extends PrimitiveArray { + static class Int64Array extends PrimitiveArray { Int64Array(ListValue protoList) { super(protoList); } @@ -1128,7 +1129,7 @@ Long get(long[] array, int i) { } } - private static class Float64Array extends PrimitiveArray { + static class Float64Array extends PrimitiveArray { Float64Array(ListValue protoList) { super(protoList); } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncResultSet.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncResultSet.java new file mode 100644 index 0000000000..c44a42994e --- /dev/null +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncResultSet.java @@ -0,0 +1,226 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.spanner; + +import com.google.api.core.ApiFuture; +import com.google.common.base.Function; +import com.google.common.collect.ImmutableList; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; + +/** Interface for result sets returned by async query methods. */ +public interface AsyncResultSet extends ResultSet { + + /** Response code from {@code tryNext()}. */ + enum CursorState { + /** Cursor has been moved to a new row. */ + OK, + /** Read is complete, all rows have been consumed, and there are no more. */ + DONE, + /** No further information known at this time, thus current row not available. */ + NOT_READY + } + + /** + * Non-blocking call that attempts to step the cursor to the next position in the stream. The + * cursor may be inspected only if the cursor returns {@code CursorState.OK}. + * + *

A caller will typically call {@link #tryNext()} in a loop inside the ReadyCallback, + * consuming all results available. For more information see {@link #setCallback(Executor, + * ReadyCallback)}. + * + *

Currently this method may only be called if a ReadyCallback has been registered. This is for + * safety purposes only, and may be relaxed in future. + * + * @return current cursor readiness state + * @throws SpannerException When an unrecoverable problem downstream occurs. Once this occurs you + * will get no further callbacks. You should return CallbackResponse.DONE back from callback. + */ + CursorState tryNext() throws SpannerException; + + enum CallbackResponse { + /** + * Tell the cursor to continue issuing callbacks when data is available. This is the standard + * "I'm ready for more" response. If cursor is not completely drained of all ready results the + * callback will be called again immediately. + */ + CONTINUE, + + /** + * Tell the cursor to suspend all callbacks until application calls {@link RowCursor#resume()}. + */ + PAUSE, + + /** + * Tell the cursor you are done receiving results, even if there are more results sitting in the + * buffer. Once you return DONE, you will receive no further callbacks. + * + *

Approximately equivalent to calling {@link RowCursor#cancel()}, and then returning {@code + * PAUSE}, but more clear, immediate, and idiomatic. + * + *

It is legal to commit a transaction that owns this read before actually returning {@code + * DONE}. + */ + DONE, + } + + /** + * Interface for receiving asynchronous callbacks when new data is ready. See {@link + * AsyncResultSet#setCallback(Executor, ReadyCallback)}. + */ + interface ReadyCallback { + CallbackResponse cursorReady(AsyncResultSet resultSet); + } + + /** + * Register a callback with the ResultSet to be made aware when more data is available, changing + * the usage pattern from sync to async. Details: + * + *

    + *
  • The callback will be called at least once. + *
  • The callback is run each time more results are available, or when we discover that there + * will be no more results. (unless paused, see below). Spurious callbacks are possible, see + * below. + *
  • Spanner guarantees that one callback is ever outstanding at a time. Also, future + * callbacks guarantee the "happens before" property with previous callbacks. + *
  • A callback normally consumes all available data in the ResultSet, and then returns {@link + * CallbackResponse#CONTINUE}. + *
  • If a callback returns {@link CallbackResponse#CONTINUE} with data still in the ResultSet, + * the callback is invoked again immediately! + *
  • Once a callback has returned {@link CallbackResponse#PAUSE} on the cursor no more + * callbacks will be run until a corresponding {@link #resume()}. + *
  • Callback will stop being called once any of the following occurs: + *
      + *
    1. Callback returns {@link CallbackResponse#DONE}. + *
    2. {@link ResultSet#tryNext()} returns {@link CursorState#DONE}. + *
    3. {@link ResultSet#tryNext()} throws an exception. + *
    + *
  • Callback may possibly be invoked after a call to {@link ResultSet#cancel()} call, but the + * subsequent call to {@link #tryNext()} will yield a SpannerException. + *
  • Spurious callbacks are possible where cursors are not actually ready. Typically callback + * should return {@link CallbackResponse#CONTINUE} any time it sees {@link + * CursorState#NOT_READY}. + *
+ * + *

Flow Control

+ * + * If no flow control is needed (say because result sizes are known in advance to be finite in + * size) then async processing is simple. The following is a code example that transfers work from + * the cursor to an upstream sink: + * + *
{@code
+   * @Override
+   * public CallbackResponse cursorReady(ResultSet cursor) {
+   *   try {
+   *     while (true) {
+   *       switch (cursor.tryNext()) {
+   *         case OK:    upstream.emit(cursor.getRow()); break;
+   *         case DONE:  upstream.done(); return CallbackResponse.DONE;
+   *         case NOT_READY:  return CallbackResponse.CONTINUE;
+   *       }
+   *     }
+   *   } catch (SpannerException e) {
+   *     upstream.doneWithError(e);
+   *     return CallbackResponse.DONE;
+   *   }
+   * }
+   * }
+ * + * Flow control may be needed if for example the upstream system may not always be ready to handle + * more data. In this case the app developer has two main options: + * + *
    + *
  • Semi-async: make {@code upstream.emit()} a blocking call. This will block the callback + * thread until progress is possible. When coding in this way the threads in the Executor + * provided to {@link #setCallback(Executor, ReadyCallback)} must be blockable without + * causing harm to progress in your system. + *
  • Full-async: call {@code cursor.pause()} and return from the callback with data still in + * the Cursor. Once in this state cursor waits until resume() is called before calling + * callback again. + *
+ * + * @param exec executor on which to run all callbacks. Typically use a threadpool. If the executor + * is one that runs the work on the submitting thread, you must be very careful not to throw + * RuntimeException up the stack, lest you do damage to calling components. For example, it + * may cause an event dispatcher thread to crash. + * @param cb ready callback + * @return An {@link ApiFuture} that returns null when the consumption of the {@link + * AsyncResultSet} has finished successfully. No more calls to the {@link ReadyCallback} will + * follow and all resources used by the {@link AsyncResultSet} have been cleaned up. The + * {@link ApiFuture} throws an {@link ExecutionException} if the consumption of the {@link + * AsyncResultSet} finished with an error. + */ + ApiFuture setCallback(Executor exec, ReadyCallback cb); + + /** + * Attempt to cancel this operation and free all resources. Non-blocking. This is a no-op for + * child row cursors and does not cancel the parent cursor. + */ + void cancel(); + + /** + * Resume callbacks from the cursor. If there is more data available, a callback will be + * dispatched immediately. This can be called from any thread. + */ + void resume(); + + /** + * Transforms the row cursor into an immutable list using the given transformer function. {@code + * transformer} will be called once per row, thus the returned list will contain one entry per + * row. The returned future will throw a {@link SpannerException} if the row cursor encountered + * any error or if the transformer threw an exception on any row. + * + *

The transformer will be run on the supplied executor. The implementation may batch multiple + * transformer invocations together into a single {@code Runnable} when possible to increase + * efficiency. At any point in time, there will be at most one invocation of the transformer in + * progress. + * + *

WARNING: This will result in materializing the entire list so this should be used + * judiciously after considering the memory requirements of the returned list. + * + *

WARNING: The {@code RowBase} object passed to transformer function is not immutable and is + * not guaranteed to remain valid after the transformer function returns. The same {@code RowBase} + * object might be passed multiple times to the transformer with different underlying data each + * time. So *NEVER* keep a reference to the {@code RowBase} outside of the transformer. + * Specifically do not use {@link com.google.common.base.Functions#identity()} function. + * + * @param transformer function which will be used to transform the row. It should not return null. + * @param executor executor on which the transformer will be run. This should ideally not be an + * inline executor such as {@code MoreExecutors.directExecutor()}; using such an executor may + * degrade the performance of the Spanner library. + */ + ApiFuture> toListAsync( + Function transformer, Executor executor); + + /** + * Transforms the row cursor into an immutable list using the given transformer function. {@code + * transformer} will be called once per row, thus the returned list will contain one entry per + * row. This method will block until all the rows have been yielded by the cursor. + * + *

WARNING: This will result in consuming the entire list so this should be used judiciously + * after considering the memory requirements of the returned list. + * + *

WARNING: The {@code RowBase} object passed to transformer function is not immutable and is + * not guaranteed to remain valid after the transformer function returns. The same {@code RowBase} + * object might be passed multiple times to the transformer with different underlying data each + * time. So *NEVER* keep a reference to the {@code RowBase} outside of the transformer. + * Specifically do not use {@link com.google.common.base.Functions#identity()} function. + * + * @param transformer function which will be used to transform the row. It should not return null. + */ + ImmutableList toList(Function transformer) throws SpannerException; +} diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncResultSetImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncResultSetImpl.java new file mode 100644 index 0000000000..07b5c8c7bb --- /dev/null +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncResultSetImpl.java @@ -0,0 +1,586 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.spanner; + +import com.google.api.core.ApiAsyncFunction; +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutures; +import com.google.api.core.ListenableFutureToApiFuture; +import com.google.api.core.SettableApiFuture; +import com.google.api.gax.core.ExecutorProvider; +import com.google.cloud.spanner.AbstractReadContext.ListenableAsyncResultSet; +import com.google.common.base.Function; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.ListeningScheduledExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.spanner.v1.ResultSetStats; +import java.util.Collection; +import java.util.LinkedList; +import java.util.concurrent.BlockingDeque; +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** Default implementation for {@link AsyncResultSet}. */ +class AsyncResultSetImpl extends ForwardingStructReader implements ListenableAsyncResultSet { + private static final Logger log = Logger.getLogger(AsyncResultSetImpl.class.getName()); + + /** State of an {@link AsyncResultSetImpl}. */ + private enum State { + INITIALIZED, + /** SYNC indicates that the {@link ResultSet} is used in sync pattern. */ + SYNC, + CONSUMING, + RUNNING, + PAUSED, + CANCELLED(true), + DONE(true); + + /** Does this state mean that the result set should permanently stop producing rows. */ + private final boolean shouldStop; + + private State() { + shouldStop = false; + } + + private State(boolean shouldStop) { + this.shouldStop = shouldStop; + } + } + + static final int DEFAULT_BUFFER_SIZE = 10; + private static final int MAX_WAIT_FOR_BUFFER_CONSUMPTION = 10; + private static final SpannerException CANCELLED_EXCEPTION = + SpannerExceptionFactory.newSpannerException( + ErrorCode.CANCELLED, "This AsyncResultSet has been cancelled"); + + private final Object monitor = new Object(); + private boolean closed; + + /** + * {@link ExecutorProvider} provides executor services that are used to fetch data from the + * backend and put these into the buffer for further consumption by the callback. + */ + private final ExecutorProvider executorProvider; + + private final ListeningScheduledExecutorService service; + + private final BlockingDeque buffer; + private Struct currentRow; + /** The underlying synchronous {@link ResultSet} that is producing the rows. */ + private final ResultSet delegateResultSet; + + /** + * Any exception that occurs while executing the query and iterating over the result set will be + * stored in this variable and propagated to the user through {@link #tryNext()}. + */ + private volatile SpannerException executionException; + + /** + * Executor for callbacks. Regardless of the type of executor that is provided, the {@link + * AsyncResultSetImpl} will ensure that at most 1 callback call will be active at any one time. + */ + private Executor executor; + + private ReadyCallback callback; + + /** + * Listeners that will be called when the {@link AsyncResultSetImpl} has finished fetching all + * rows and any underlying transaction or session can be closed. + */ + private Collection listeners = new LinkedList<>(); + + private State state = State.INITIALIZED; + + /** + * {@link #finished} indicates whether all the results from the underlying result set have been + * read. + */ + private volatile boolean finished; + + private volatile ApiFuture result; + + /** + * {@link #cursorReturnedDoneOrException} indicates whether {@link #tryNext()} has returned {@link + * CursorState#DONE} or a {@link SpannerException}. + */ + private volatile boolean cursorReturnedDoneOrException; + + /** + * {@link #pausedLatch} is used to pause the producer when the {@link AsyncResultSet} is paused. + * The production of rows that are put into the buffer is only paused once the buffer is full. + */ + private volatile CountDownLatch pausedLatch = new CountDownLatch(1); + /** + * {@link #bufferConsumptionLatch} is used to pause the producer when the buffer is full and the + * consumer needs some time to catch up. + */ + private volatile CountDownLatch bufferConsumptionLatch = new CountDownLatch(0); + /** + * {@link #consumingLatch} is used to pause the producer when all rows have been put into the + * buffer, but the consumer (the callback) has not yet received and processed all rows. + */ + private volatile CountDownLatch consumingLatch = new CountDownLatch(0); + + AsyncResultSetImpl(ExecutorProvider executorProvider, ResultSet delegate, int bufferSize) { + super(delegate); + this.executorProvider = Preconditions.checkNotNull(executorProvider); + this.delegateResultSet = Preconditions.checkNotNull(delegate); + this.service = MoreExecutors.listeningDecorator(executorProvider.getExecutor()); + this.buffer = new LinkedBlockingDeque<>(bufferSize); + } + + /** + * Closes the {@link AsyncResultSet}. {@link #close()} is non-blocking and may be called multiple + * times without side effects. An {@link AsyncResultSet} may be closed before all rows have been + * returned to the callback, and calling {@link #tryNext()} on a closed {@link AsyncResultSet} is + * allowed as long as this is done from within a {@link ReadyCallback}. Calling {@link #resume()} + * on a closed {@link AsyncResultSet} is also allowed. + */ + @Override + public void close() { + synchronized (monitor) { + if (this.closed) { + return; + } + if (state == State.INITIALIZED || state == State.SYNC) { + delegateResultSet.close(); + } + this.closed = true; + } + } + + /** + * Adds a listener that will be called when no more rows will be read from the underlying {@link + * ResultSet}, either because all rows have been read, or because {@link + * ReadyCallback#cursorReady(AsyncResultSet)} returned {@link CallbackResponse#DONE}. + */ + @Override + public void addListener(Runnable listener) { + Preconditions.checkState(state == State.INITIALIZED); + listeners.add(listener); + } + + @Override + public void removeListener(Runnable listener) { + Preconditions.checkState(state == State.INITIALIZED); + listeners.remove(listener); + } + + /** + * Tries to advance this {@link AsyncResultSet} to the next row. This method may only be called + * from within a {@link ReadyCallback}. + */ + @Override + public CursorState tryNext() throws SpannerException { + synchronized (monitor) { + if (state == State.CANCELLED) { + cursorReturnedDoneOrException = true; + throw CANCELLED_EXCEPTION; + } + if (buffer.isEmpty() && executionException != null) { + cursorReturnedDoneOrException = true; + throw executionException; + } + Preconditions.checkState( + this.callback != null, "tryNext may only be called after a callback has been set."); + Preconditions.checkState( + this.state == State.CONSUMING, + "tryNext may only be called from a DataReady callback. Current state: " + + this.state.name()); + + if (finished && buffer.isEmpty()) { + cursorReturnedDoneOrException = true; + return CursorState.DONE; + } + } + if (!buffer.isEmpty()) { + // Set the next row from the buffer as the current row of the StructReader. + replaceDelegate(currentRow = buffer.pop()); + synchronized (monitor) { + bufferConsumptionLatch.countDown(); + } + return CursorState.OK; + } + return CursorState.NOT_READY; + } + + private void closeDelegateResultSet() { + try { + delegateResultSet.close(); + } catch (Throwable t) { + log.log(Level.FINE, "Ignoring error from closing delegate result set", t); + } + } + + /** + * {@link CallbackRunnable} calls the {@link ReadyCallback} registered for this {@link + * AsyncResultSet}. + */ + private class CallbackRunnable implements Runnable { + @Override + public void run() { + try { + while (true) { + synchronized (monitor) { + if (cursorReturnedDoneOrException) { + break; + } + } + CallbackResponse response; + try { + response = callback.cursorReady(AsyncResultSetImpl.this); + } catch (Throwable e) { + synchronized (monitor) { + if (cursorReturnedDoneOrException + && state == State.CANCELLED + && e instanceof SpannerException + && ((SpannerException) e).getErrorCode() == ErrorCode.CANCELLED) { + // The callback did not catch the cancelled exception (which it should have), but + // we'll keep the cancelled state. + return; + } + executionException = SpannerExceptionFactory.newSpannerException(e); + cursorReturnedDoneOrException = true; + } + return; + } + synchronized (monitor) { + if (state == State.CANCELLED) { + if (cursorReturnedDoneOrException) { + return; + } + } else { + switch (response) { + case DONE: + state = State.DONE; + closeDelegateResultSet(); + return; + case PAUSE: + state = State.PAUSED; + // Make sure no-one else is waiting on the current pause latch and create a new + // one. + pausedLatch.countDown(); + pausedLatch = new CountDownLatch(1); + return; + case CONTINUE: + if (buffer.isEmpty()) { + // Call the callback once more if the entire result set has been processed but + // the callback has not yet received a CursorState.DONE or a CANCELLED error. + if (finished && !cursorReturnedDoneOrException) { + break; + } + state = State.RUNNING; + return; + } + break; + default: + throw new IllegalStateException("Unknown response: " + response); + } + } + } + } + } finally { + synchronized (monitor) { + // Count down all latches that the producer might be waiting on. + consumingLatch.countDown(); + while (bufferConsumptionLatch.getCount() > 0L) { + bufferConsumptionLatch.countDown(); + } + } + } + } + } + + private final CallbackRunnable callbackRunnable = new CallbackRunnable(); + + /** + * {@link ProduceRowsCallable} reads data from the underlying {@link ResultSet}, places these in + * the buffer and dispatches the {@link CallbackRunnable} when data is ready to be consumed. + */ + private class ProduceRowsCallable implements Callable { + @Override + public Void call() throws Exception { + boolean stop = false; + boolean hasNext = false; + try { + hasNext = delegateResultSet.next(); + } catch (Throwable e) { + synchronized (monitor) { + executionException = SpannerExceptionFactory.newSpannerException(e); + } + } + try { + while (!stop && hasNext) { + try { + synchronized (monitor) { + stop = state.shouldStop; + } + if (!stop) { + while (buffer.remainingCapacity() == 0 && !stop) { + waitIfPaused(); + // The buffer is full and we should let the callback consume a number of rows before + // we proceed with producing any more rows to prevent us from potentially waiting on + // a full buffer repeatedly. + // Wait until at least half of the buffer is available, or if it's a bigger buffer, + // wait until at least 10 rows can be placed in it. + // TODO: Make this more dynamic / configurable? + startCallbackWithBufferLatchIfNecessary( + Math.min( + Math.min(buffer.size() / 2 + 1, buffer.size()), + MAX_WAIT_FOR_BUFFER_CONSUMPTION)); + bufferConsumptionLatch.await(); + synchronized (monitor) { + stop = state.shouldStop; + } + } + } + if (!stop) { + buffer.put(delegateResultSet.getCurrentRowAsStruct()); + startCallbackIfNecessary(); + hasNext = delegateResultSet.next(); + } + } catch (Throwable e) { + synchronized (monitor) { + executionException = SpannerExceptionFactory.newSpannerException(e); + stop = true; + } + } + } + // We don't need any more data from the underlying result set, so we close it as soon as + // possible. Any error that might occur during this will be ignored. + closeDelegateResultSet(); + + // Ensure that the callback has been called at least once, even if the result set was + // cancelled. + synchronized (monitor) { + finished = true; + stop = cursorReturnedDoneOrException; + } + // Call the callback if there are still rows in the buffer that need to be processed. + while (!stop) { + waitIfPaused(); + startCallbackIfNecessary(); + // Make sure we wait until the callback runner has actually finished. + consumingLatch.await(); + synchronized (monitor) { + stop = cursorReturnedDoneOrException; + } + } + } finally { + if (executorProvider.shouldAutoClose()) { + service.shutdown(); + } + for (Runnable listener : listeners) { + listener.run(); + } + synchronized (monitor) { + if (executionException != null) { + throw executionException; + } + if (state == State.CANCELLED) { + throw CANCELLED_EXCEPTION; + } + } + } + return null; + } + + private void waitIfPaused() throws InterruptedException { + CountDownLatch pause; + synchronized (monitor) { + pause = pausedLatch; + } + pause.await(); + } + + private void startCallbackIfNecessary() { + startCallbackWithBufferLatchIfNecessary(0); + } + + private void startCallbackWithBufferLatchIfNecessary(int bufferLatch) { + synchronized (monitor) { + if ((state == State.RUNNING || state == State.CANCELLED) + && !cursorReturnedDoneOrException) { + consumingLatch = new CountDownLatch(1); + if (bufferLatch > 0) { + bufferConsumptionLatch = new CountDownLatch(bufferLatch); + } + if (state == State.RUNNING) { + state = State.CONSUMING; + } + executor.execute(callbackRunnable); + } + } + } + } + + /** Sets the callback for this {@link AsyncResultSet}. */ + @Override + public ApiFuture setCallback(Executor exec, ReadyCallback cb) { + synchronized (monitor) { + Preconditions.checkState(!closed, "This AsyncResultSet has been closed"); + Preconditions.checkState( + this.state == State.INITIALIZED, "callback may not be set multiple times"); + + // Start to fetch data and buffer these. + this.result = + new ListenableFutureToApiFuture<>(this.service.submit(new ProduceRowsCallable())); + this.executor = MoreExecutors.newSequentialExecutor(Preconditions.checkNotNull(exec)); + this.callback = Preconditions.checkNotNull(cb); + this.state = State.RUNNING; + pausedLatch.countDown(); + return result; + } + } + + Future getResult() { + return result; + } + + @Override + public void cancel() { + synchronized (monitor) { + Preconditions.checkState( + state != State.INITIALIZED && state != State.SYNC, + "cannot cancel a result set without a callback"); + state = State.CANCELLED; + pausedLatch.countDown(); + } + } + + @Override + public void resume() { + synchronized (monitor) { + Preconditions.checkState( + state != State.INITIALIZED && state != State.SYNC, + "cannot resume a result set without a callback"); + if (state == State.PAUSED) { + state = State.RUNNING; + pausedLatch.countDown(); + } + } + } + + private static class CreateListCallback implements ReadyCallback { + private final SettableApiFuture> future; + private final Function transformer; + private final ImmutableList.Builder builder = ImmutableList.builder(); + + private CreateListCallback( + SettableApiFuture> future, Function transformer) { + this.future = future; + this.transformer = transformer; + } + + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + try { + while (true) { + switch (resultSet.tryNext()) { + case DONE: + future.set(builder.build()); + return CallbackResponse.DONE; + case NOT_READY: + return CallbackResponse.CONTINUE; + case OK: + builder.add(transformer.apply(resultSet)); + break; + } + } + } catch (Throwable t) { + future.setException(t); + return CallbackResponse.DONE; + } + } + } + + @Override + public ApiFuture> toListAsync( + Function transformer, Executor executor) { + synchronized (monitor) { + Preconditions.checkState(!closed, "This AsyncResultSet has been closed"); + Preconditions.checkState( + this.state == State.INITIALIZED, "This AsyncResultSet has already been used."); + final SettableApiFuture> res = SettableApiFuture.>create(); + CreateListCallback callback = new CreateListCallback(res, transformer); + ApiFuture finished = setCallback(executor, callback); + return ApiFutures.transformAsync( + finished, + new ApiAsyncFunction>() { + @Override + public ApiFuture> apply(Void input) throws Exception { + return res; + } + }, + MoreExecutors.directExecutor()); + } + } + + @Override + public ImmutableList toList(Function transformer) + throws SpannerException { + ApiFuture> future = toListAsync(transformer, MoreExecutors.directExecutor()); + try { + return future.get(); + } catch (ExecutionException e) { + throw SpannerExceptionFactory.newSpannerException(e.getCause()); + } catch (Throwable e) { + throw SpannerExceptionFactory.newSpannerException(e); + } + } + + @Override + public boolean next() throws SpannerException { + synchronized (monitor) { + Preconditions.checkState( + this.state == State.INITIALIZED || this.state == State.SYNC, + "Cannot call next() on a result set with a callback."); + this.state = State.SYNC; + } + boolean res = delegateResultSet.next(); + currentRow = delegateResultSet.getCurrentRowAsStruct(); + return res; + } + + @Override + public ResultSetStats getStats() { + return delegateResultSet.getStats(); + } + + @Override + protected void checkValidState() { + synchronized (monitor) { + Preconditions.checkState( + state == State.SYNC || state == State.CONSUMING || state == State.CANCELLED, + "only allowed after a next() call or from within a ReadyCallback#cursorReady callback"); + Preconditions.checkState(state != State.SYNC || !closed, "ResultSet is closed"); + } + } + + @Override + public Struct getCurrentRowAsStruct() { + checkValidState(); + return currentRow; + } +} diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncRunner.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncRunner.java new file mode 100644 index 0000000000..3cae49e65b --- /dev/null +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncRunner.java @@ -0,0 +1,59 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.spanner; + +import com.google.api.core.ApiFuture; +import com.google.cloud.Timestamp; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; + +public interface AsyncRunner { + + /** + * Functional interface for executing a read/write transaction asynchronously that returns a + * result of type R. + */ + interface AsyncWork { + /** + * Performs a single transaction attempt. All reads/writes should be performed using {@code + * txn}. + * + *

Implementations of this method should not attempt to commit the transaction directly: + * returning normally will result in the runner attempting to commit the transaction once the + * returned future completes, retrying on abort. + * + *

In most cases, the implementation will not need to catch {@code SpannerException}s from + * Spanner operations, instead letting these propagate to the framework. The transaction runner + * will take appropriate action based on the type of exception. In particular, implementations + * should never catch an exception of type {@link SpannerErrors#isAborted}: these indicate that + * some reads may have returned inconsistent data and the transaction attempt must be aborted. + * + * @param txn the transaction + * @return future over the result of the work + */ + ApiFuture doWorkAsync(TransactionContext txn); + } + + /** Executes a read/write transaction asynchronously using the given executor. */ + ApiFuture runAsync(AsyncWork work, Executor executor); + + /** + * Returns the timestamp at which the transaction committed. {@link ApiFuture#get()} will throw an + * {@link ExecutionException} if the transaction did not commit. + */ + ApiFuture getCommitTimestamp(); +} diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncRunnerImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncRunnerImpl.java new file mode 100644 index 0000000000..5b83402919 --- /dev/null +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncRunnerImpl.java @@ -0,0 +1,81 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.spanner; + +import com.google.api.core.ApiFuture; +import com.google.api.core.SettableApiFuture; +import com.google.cloud.Timestamp; +import com.google.cloud.spanner.TransactionRunner.TransactionCallable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; + +class AsyncRunnerImpl implements AsyncRunner { + private final TransactionRunnerImpl delegate; + private final SettableApiFuture commitTimestamp = SettableApiFuture.create(); + + AsyncRunnerImpl(TransactionRunnerImpl delegate) { + this.delegate = delegate; + } + + @Override + public ApiFuture runAsync(final AsyncWork work, Executor executor) { + final SettableApiFuture res = SettableApiFuture.create(); + executor.execute( + new Runnable() { + @Override + public void run() { + try { + res.set(runTransaction(work)); + } catch (Throwable t) { + res.setException(t); + } finally { + setCommitTimestamp(); + } + } + }); + return res; + } + + private R runTransaction(final AsyncWork work) { + return delegate.run( + new TransactionCallable() { + @Override + public R run(TransactionContext transaction) throws Exception { + try { + return work.doWorkAsync(transaction).get(); + } catch (ExecutionException e) { + throw SpannerExceptionFactory.newSpannerException(e.getCause()); + } catch (InterruptedException e) { + throw SpannerExceptionFactory.propagateInterrupt(e); + } + } + }); + } + + private void setCommitTimestamp() { + try { + commitTimestamp.set(delegate.getCommitTimestamp()); + } catch (Throwable t) { + commitTimestamp.setException(t); + } + } + + @Override + public ApiFuture getCommitTimestamp() { + return commitTimestamp; + } +} diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManager.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManager.java new file mode 100644 index 0000000000..d519c68013 --- /dev/null +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManager.java @@ -0,0 +1,203 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.spanner; + +import com.google.api.core.ApiFuture; +import com.google.cloud.Timestamp; +import com.google.cloud.spanner.AsyncTransactionManager.AsyncTransactionFunction; +import com.google.cloud.spanner.AsyncTransactionManager.CommitTimestampFuture; +import com.google.cloud.spanner.AsyncTransactionManager.TransactionContextFuture; +import com.google.cloud.spanner.TransactionManager.TransactionState; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * An interface for managing the life cycle of a read write transaction including all its retries. + * See {@link TransactionContext} for a description of transaction semantics. + * + *

At any point in time there can be at most one active transaction in this manager. When that + * transaction is committed, if it fails with an {@code ABORTED} error, calling {@link + * #resetForRetryAsync()} would create a new {@link TransactionContextFuture}. The newly created + * transaction would use the same session thus increasing its lock priority. If the transaction is + * committed successfully, or is rolled back or commit fails with any error other than {@code + * ABORTED}, the manager is considered complete and no further transactions are allowed to be + * created in it. + * + *

Every {@code AsyncTransactionManager} should either be committed or rolled back. Failure to do + * so can cause resources to be leaked and deadlocks. Easiest way to guarantee this is by calling + * {@link #close()} in a finally block. + * + * @see DatabaseClient#transactionManagerAsync() + */ +public interface AsyncTransactionManager extends AutoCloseable { + /** + * {@link ApiFuture} that returns a {@link TransactionContext} and that supports chaining of + * multiple {@link TransactionContextFuture}s to form a transaction. + */ + public interface TransactionContextFuture extends ApiFuture { + /** + * Sets the first step to execute as part of this transaction after the transaction has started + * using the specified executor. {@link MoreExecutors#directExecutor()} can be be used for + * lightweight functions, but should be avoided for heavy or blocking operations. See also + * {@link ListenableFuture#addListener(Runnable, Executor)} for further information. + */ + AsyncTransactionStep then( + AsyncTransactionFunction function, Executor executor); + } + + /** + * {@link ApiFuture} that returns the commit {@link Timestamp} of a Cloud Spanner transaction that + * is executed using an {@link AsyncTransactionManager}. This future is returned by the call to + * {@link AsyncTransactionStep#commitAsync()} of the last step in the transaction. + */ + public interface CommitTimestampFuture extends ApiFuture { + /** + * Returns the commit timestamp of the transaction. Getting this value should always be done in + * order to ensure that the transaction succeeded. If any of the steps in the transaction fails + * with an uncaught exception, this method will automatically stop the transaction at that point + * and the exception will be returned as the cause of the {@link ExecutionException} that is + * thrown by this method. + * + * @throws AbortedException if the transaction was aborted by Cloud Spanner and needs to be + * retried. + */ + @Override + Timestamp get() throws AbortedException, InterruptedException, ExecutionException; + + /** + * Same as {@link #get()}, but will throw a {@link TimeoutException} if the transaction does not + * finish within the timeout. + */ + @Override + Timestamp get(long timeout, TimeUnit unit) + throws AbortedException, InterruptedException, ExecutionException, TimeoutException; + } + + /** + * {@link AsyncTransactionStep} is returned by {@link + * TransactionContextFuture#then(AsyncTransactionFunction)} and {@link + * AsyncTransactionStep#then(AsyncTransactionFunction)} and allows transaction steps that should + * be executed serially to be chained together. Each step can contain one or more statements that + * may execute in parallel. + * + *

Example usage: + * + *

{@code
+   * TransactionContextFuture txnFuture = manager.beginAsync();
+   * final String column = "FirstName";
+   * txnFuture.then(
+   *         new AsyncTransactionFunction() {
+   *           @Override
+   *           public ApiFuture apply(TransactionContext txn, Void input)
+   *               throws Exception {
+   *             return txn.readRowAsync(
+   *                 "Singers", Key.of(singerId), Collections.singleton(column));
+   *           }
+   *         })
+   *     .then(
+   *         new AsyncTransactionFunction() {
+   *           @Override
+   *           public ApiFuture apply(TransactionContext txn, Struct input)
+   *               throws Exception {
+   *             String name = input.getString(column);
+   *             txn.buffer(
+   *                 Mutation.newUpdateBuilder("Singers")
+   *                     .set(column)
+   *                     .to(name.toUpperCase())
+   *                     .build());
+   *             return ApiFutures.immediateFuture(null);
+   *           }
+   *         })
+   * }
+ */ + public interface AsyncTransactionStep extends ApiFuture { + /** + * Adds a step to the transaction chain that should be executed using the specified executor. + * This step is guaranteed to be executed only after the previous step executed successfully. + * {@link MoreExecutors#directExecutor()} can be be used for lightweight functions, but should + * be avoided for heavy or blocking operations. See also {@link + * ListenableFuture#addListener(Runnable, Executor)} for further information. + */ + AsyncTransactionStep then( + AsyncTransactionFunction next, Executor executor); + + /** + * Commits the transaction and returns a {@link CommitTimestampFuture} that will return the + * commit timestamp of the transaction, or throw the first uncaught exception in the transaction + * chain as an {@link ExecutionException}. + */ + CommitTimestampFuture commitAsync(); + } + + /** + * Each step in a transaction chain is defined by an {@link AsyncTransactionFunction}. It receives + * a {@link TransactionContext} and the output value of the previous transaction step as its input + * parameters. The method should return an {@link ApiFuture} that will return the result of this + * step. + */ + public interface AsyncTransactionFunction { + /** + * {@link #apply(TransactionContext, Object)} is called when this transaction step is executed. + * The input value is the result of the previous step, and this method will only be called if + * the previous step executed successfully. + * + * @param txn the {@link TransactionContext} that can be used to execute statements. + * @param input the result of the previous transaction step. + * @return an {@link ApiFuture} that will return the result of this step, and that will be the + * input of the next transaction step. This method should never return null. + * Instead, if the method does not have a return value, the method should return {@link + * ApiFutures#immediateFuture(null)}. + */ + ApiFuture apply(TransactionContext txn, I input) throws Exception; + } + + /** + * Creates a new read write transaction. This must be called before doing any other operation and + * can only be called once. To create a new transaction for subsequent retries, see {@link + * #resetForRetry()}. + */ + TransactionContextFuture beginAsync(); + + /** + * Rolls back the currently active transaction. In most cases there should be no need to call this + * explicitly since {@link #close()} would automatically roll back any active transaction. + */ + ApiFuture rollbackAsync(); + + /** + * Creates a new transaction for retry. This should only be called if the previous transaction + * failed with {@code ABORTED}. In all other cases, this will throw an {@link + * IllegalStateException}. Users should backoff before calling this method. Backoff delay is + * specified by {@link SpannerException#getRetryDelayInMillis()} on the {@code SpannerException} + * throw by the previous commit call. + */ + TransactionContextFuture resetForRetryAsync(); + + /** Returns the state of the transaction. */ + TransactionState getState(); + + /** + * Closes the manager. If there is an active transaction, it will be rolled back. Underlying + * session will be released back to the session pool. + */ + @Override + void close(); +} diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManagerImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManagerImpl.java new file mode 100644 index 0000000000..082fa827e7 --- /dev/null +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManagerImpl.java @@ -0,0 +1,167 @@ +/* + * Copyright 2017 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.spanner; + +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutureCallback; +import com.google.api.core.ApiFutures; +import com.google.api.core.SettableApiFuture; +import com.google.cloud.Timestamp; +import com.google.cloud.spanner.SessionImpl.SessionTransaction; +import com.google.cloud.spanner.TransactionContextFutureImpl.CommittableAsyncTransactionManager; +import com.google.cloud.spanner.TransactionManager.TransactionState; +import com.google.common.base.Preconditions; +import com.google.common.util.concurrent.MoreExecutors; +import io.opencensus.trace.Span; +import io.opencensus.trace.Tracer; +import io.opencensus.trace.Tracing; + +/** Implementation of {@link AsyncTransactionManager}. */ +final class AsyncTransactionManagerImpl + implements CommittableAsyncTransactionManager, SessionTransaction { + private static final Tracer tracer = Tracing.getTracer(); + + private final SessionImpl session; + private Span span; + + private TransactionRunnerImpl.TransactionContextImpl txn; + private TransactionState txnState; + private final SettableApiFuture commitTimestamp = SettableApiFuture.create(); + + AsyncTransactionManagerImpl(SessionImpl session, Span span) { + this.session = session; + this.span = span; + } + + @Override + public void setSpan(Span span) { + this.span = span; + } + + @Override + public void close() { + txn.close(); + } + + @Override + public TransactionContextFutureImpl beginAsync() { + Preconditions.checkState(txn == null, "begin can only be called once"); + TransactionContextFutureImpl begin = + new TransactionContextFutureImpl(this, internalBeginAsync(true)); + return begin; + } + + private ApiFuture internalBeginAsync(boolean setActive) { + txnState = TransactionState.STARTED; + txn = session.newTransaction(); + if (setActive) { + session.setActive(this); + } + final SettableApiFuture res = SettableApiFuture.create(); + final ApiFuture fut = txn.ensureTxnAsync(); + ApiFutures.addCallback( + fut, + new ApiFutureCallback() { + @Override + public void onFailure(Throwable t) { + res.setException(SpannerExceptionFactory.newSpannerException(t)); + } + + @Override + public void onSuccess(Void result) { + res.set(txn); + } + }, + MoreExecutors.directExecutor()); + return res; + } + + @Override + public void onError(Throwable t) { + if (t instanceof AbortedException) { + txnState = TransactionState.ABORTED; + } + } + + @Override + public ApiFuture commitAsync() { + Preconditions.checkState( + txnState == TransactionState.STARTED, + "commit can only be invoked if the transaction is in progress. Current state: " + txnState); + if (txn.isAborted()) { + txnState = TransactionState.ABORTED; + return ApiFutures.immediateFailedFuture( + SpannerExceptionFactory.newSpannerException( + ErrorCode.ABORTED, "Transaction already aborted")); + } + ApiFuture res = txn.commitAsync(); + txnState = TransactionState.COMMITTED; + ApiFutures.addCallback( + res, + new ApiFutureCallback() { + @Override + public void onFailure(Throwable t) { + if (t instanceof AbortedException) { + txnState = TransactionState.ABORTED; + } else { + txnState = TransactionState.COMMIT_FAILED; + commitTimestamp.setException(t); + } + } + + @Override + public void onSuccess(Timestamp result) { + commitTimestamp.set(result); + } + }, + MoreExecutors.directExecutor()); + return res; + } + + @Override + public ApiFuture rollbackAsync() { + Preconditions.checkState( + txnState == TransactionState.STARTED, + "rollback can only be called if the transaction is in progress"); + try { + return txn.rollbackAsync(); + } finally { + txnState = TransactionState.ROLLED_BACK; + } + } + + @Override + public TransactionContextFuture resetForRetryAsync() { + if (txn == null || !txn.isAborted() && txnState != TransactionState.ABORTED) { + throw new IllegalStateException( + "resetForRetry can only be called if the previous attempt aborted"); + } + return new TransactionContextFutureImpl(this, internalBeginAsync(false)); + } + + @Override + public TransactionState getState() { + return txnState; + } + + @Override + public void invalidate() { + if (txnState == TransactionState.STARTED || txnState == null) { + txnState = TransactionState.ROLLED_BACK; + } + } +} diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/BatchClientImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/BatchClientImpl.java index 43de2be092..c84bef77cf 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/BatchClientImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/BatchClientImpl.java @@ -30,6 +30,7 @@ import com.google.spanner.v1.PartitionReadRequest; import com.google.spanner.v1.PartitionResponse; import com.google.spanner.v1.TransactionSelector; +import io.opencensus.trace.Tracing; import java.util.List; import java.util.Map; @@ -51,6 +52,7 @@ public BatchReadOnlyTransaction batchReadOnlyTransaction(TimestampBound bound) { .setTimestampBound(bound) .setDefaultQueryOptions( sessionClient.getSpanner().getDefaultQueryOptions(sessionClient.getDatabaseId())) + .setExecutorProvider(sessionClient.getSpanner().getAsyncExecutorProvider()) .setDefaultPrefetchChunks(sessionClient.getSpanner().getDefaultPrefetchChunks()), checkNotNull(bound)); } @@ -67,6 +69,7 @@ public BatchReadOnlyTransaction batchReadOnlyTransaction(BatchTransactionId batc .setTimestamp(batchTransactionId.getTimestamp()) .setDefaultQueryOptions( sessionClient.getSpanner().getDefaultQueryOptions(sessionClient.getDatabaseId())) + .setExecutorProvider(sessionClient.getSpanner().getAsyncExecutorProvider()) .setDefaultPrefetchChunks(sessionClient.getSpanner().getDefaultPrefetchChunks()), batchTransactionId); } @@ -81,6 +84,7 @@ private static class BatchReadOnlyTransactionImpl extends MultiUseReadOnlyTransa super(builder.setTimestampBound(bound)); this.sessionName = session.getName(); this.options = session.getOptions(); + setSpan(Tracing.getTracer().getCurrentSpan()); initTransaction(); } @@ -89,6 +93,7 @@ private static class BatchReadOnlyTransactionImpl extends MultiUseReadOnlyTransa super(builder.setTransactionId(batchTransactionId.getTransactionId())); this.sessionName = session.getName(); this.options = session.getOptions(); + setSpan(Tracing.getTracer().getCurrentSpan()); } @Override diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClient.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClient.java index ac29ba2b37..d52d1d892e 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClient.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClient.java @@ -278,6 +278,127 @@ public interface DatabaseClient { */ TransactionManager transactionManager(); + /** + * Returns an asynchronous transaction runner for executing a single logical transaction with + * retries. The returned runner can only be used once. + * + *

Example of a read write transaction. + * + *

 
+   * Executor executor = Executors.newSingleThreadExecutor();
+   * final long singerId = my_singer_id;
+   * AsyncRunner runner = client.runAsync();
+   * ApiFuture rowCount =
+   *     runner.runAsync(
+   *         new AsyncWork() {
+   *           @Override
+   *           public ApiFuture doWorkAsync(TransactionContext txn) {
+   *             String column = "FirstName";
+   *             Struct row =
+   *                 txn.readRow("Singers", Key.of(singerId), Collections.singleton("Name"));
+   *             String name = row.getString("Name");
+   *             return txn.executeUpdateAsync(
+   *                 Statement.newBuilder("UPDATE Singers SET Name=@name WHERE SingerId=@id")
+   *                     .bind("id")
+   *                     .to(singerId)
+   *                     .bind("name")
+   *                     .to(name.toUpperCase())
+   *                     .build());
+   *           }
+   *         },
+   *         executor);
+   * 
+ */ + AsyncRunner runAsync(); + + /** + * Returns an asynchronous transaction manager which allows manual management of transaction + * lifecycle. This API is meant for advanced users. Most users should instead use the {@link + * #runAsync()} API instead. + * + *

Example of using {@link AsyncTransactionManager} with lambda expressions (Java 8 and + * higher). + * + *

{@code
+   * long singerId = 1L;
+   * try (AsyncTransactionManager manager = client.transactionManagerAsync()) {
+   *   TransactionContextFuture txnFut = manager.beginAsync();
+   *   while (true) {
+   *     String column = "FirstName";
+   *     CommitTimestampFuture commitTimestamp =
+   *         txnFut
+   *             .then(
+   *                 (txn, __) ->
+   *                     txn.readRowAsync(
+   *                         "Singers", Key.of(singerId), Collections.singleton(column)))
+   *             .then(
+   *                 (txn, row) -> {
+   *                   String name = row.getString(column);
+   *                   txn.buffer(
+   *                       Mutation.newUpdateBuilder("Singers")
+   *                           .set(column)
+   *                           .to(name.toUpperCase())
+   *                           .build());
+   *                   return ApiFutures.immediateFuture(null);
+   *                 })
+   *             .commitAsync();
+   *     try {
+   *       commitTimestamp.get();
+   *       break;
+   *     } catch (AbortedException e) {
+   *       Thread.sleep(e.getRetryDelayInMillis() / 1000);
+   *       txnFut = manager.resetForRetryAsync();
+   *     }
+   *   }
+   * }
+   * }
+ * + *

Example of using {@link AsyncTransactionManager} (Java 7). + * + *

{@code
+   * final long singerId = 1L;
+   * try (AsyncTransactionManager manager = client().transactionManagerAsync()) {
+   *   TransactionContextFuture txn = manager.beginAsync();
+   *   while (true) {
+   *     final String column = "FirstName";
+   *     CommitTimestampFuture commitTimestamp =
+   *         txn.then(
+   *                 new AsyncTransactionFunction() {
+   *                   @Override
+   *                   public ApiFuture apply(TransactionContext txn, Void input)
+   *                       throws Exception {
+   *                     return txn.readRowAsync(
+   *                         "Singers", Key.of(singerId), Collections.singleton(column));
+   *                   }
+   *                 })
+   *             .then(
+   *                 new AsyncTransactionFunction() {
+   *                   @Override
+   *                   public ApiFuture apply(TransactionContext txn, Struct input)
+   *                       throws Exception {
+   *                     String name = input.getString(column);
+   *                     txn.buffer(
+   *                         Mutation.newUpdateBuilder("Singers")
+   *                             .set(column)
+   *                             .to(name.toUpperCase())
+   *                             .build());
+   *                     return ApiFutures.immediateFuture(null);
+   *                   }
+   *                 })
+   *             .commitAsync();
+   *     try {
+   *       commitTimestamp.get();
+   *       break;
+   *     } catch (AbortedException e) {
+   *       Thread.sleep(e.getRetryDelayInMillis() / 1000);
+   *       txn = manager.resetForRetryAsync();
+   *     }
+   *   }
+   * }
+   * }
+ */ + AsyncTransactionManager transactionManagerAsync(); + /** * Returns the lower bound of rows modified by this DML statement. * 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 ec83d06335..4dd10001c7 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 @@ -17,7 +17,7 @@ package com.google.cloud.spanner; import com.google.cloud.Timestamp; -import com.google.cloud.spanner.SessionPool.PooledSession; +import com.google.cloud.spanner.SessionPool.PooledSessionFuture; import com.google.cloud.spanner.SpannerImpl.ClosedException; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Function; @@ -52,12 +52,12 @@ private enum SessionMode { } @VisibleForTesting - PooledSession getReadSession() { + PooledSessionFuture getReadSession() { return pool.getReadSession(); } @VisibleForTesting - PooledSession getReadWriteSession() { + PooledSessionFuture getReadWriteSession() { return pool.getReadWriteSession(); } @@ -191,6 +191,28 @@ public TransactionManager transactionManager() { } } + @Override + public AsyncRunner runAsync() { + Span span = tracer.spanBuilder(READ_WRITE_TRANSACTION).startSpan(); + try (Scope s = tracer.withSpan(span)) { + return getReadWriteSession().runAsync(); + } catch (RuntimeException e) { + TraceUtil.endSpanWithFailure(span, e); + throw e; + } + } + + @Override + public AsyncTransactionManager transactionManagerAsync() { + Span span = tracer.spanBuilder(READ_WRITE_TRANSACTION).startSpan(); + try (Scope s = tracer.withSpan(span)) { + return getReadWriteSession().transactionManagerAsync(); + } catch (RuntimeException e) { + TraceUtil.endSpanWithFailure(span, e); + throw e; + } + } + @Override public long executePartitionedUpdate(final Statement stmt) { Span span = tracer.spanBuilder(PARTITION_DML_TRANSACTION).startSpan(); @@ -212,7 +234,7 @@ public Long apply(Session session) { } private T runWithSessionRetry(SessionMode mode, Function callable) { - PooledSession session = + PooledSessionFuture session = mode == SessionMode.READ_WRITE ? getReadWriteSession() : getReadSession(); while (true) { try { diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseId.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseId.java index d2c732750e..dd13df65e8 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseId.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseId.java @@ -81,7 +81,7 @@ public String toString() { * projects/PROJECT_ID/instances/INSTANCE_ID/databases/DATABASE_ID} * @throws IllegalArgumentException if {@code name} does not conform to the expected pattern */ - static DatabaseId of(String name) { + public static DatabaseId of(String name) { Preconditions.checkNotNull(name); Map parts = NAME_TEMPLATE.match(name); Preconditions.checkArgument( diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ForwardingAsyncResultSet.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ForwardingAsyncResultSet.java new file mode 100644 index 0000000000..78e3505998 --- /dev/null +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ForwardingAsyncResultSet.java @@ -0,0 +1,65 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.spanner; + +import com.google.api.core.ApiFuture; +import com.google.common.base.Function; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import java.util.concurrent.Executor; + +/** Forwarding implementation of {@link AsyncResultSet} that forwards all calls to a delegate. */ +public class ForwardingAsyncResultSet extends ForwardingResultSet implements AsyncResultSet { + final AsyncResultSet delegate; + + public ForwardingAsyncResultSet(AsyncResultSet delegate) { + super(Preconditions.checkNotNull(delegate)); + this.delegate = delegate; + } + + @Override + public CursorState tryNext() throws SpannerException { + return delegate.tryNext(); + } + + @Override + public ApiFuture setCallback(Executor exec, ReadyCallback cb) { + return delegate.setCallback(exec, cb); + } + + @Override + public void cancel() { + delegate.cancel(); + } + + @Override + public void resume() { + delegate.resume(); + } + + @Override + public ApiFuture> toListAsync( + Function transformer, Executor executor) { + return delegate.toListAsync(transformer, executor); + } + + @Override + public ImmutableList toList(Function transformer) + throws SpannerException { + return delegate.toList(transformer); + } +} diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ForwardingResultSet.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ForwardingResultSet.java index 753c3f6f39..4cc0ab9b9e 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ForwardingResultSet.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ForwardingResultSet.java @@ -17,16 +17,23 @@ package com.google.cloud.spanner; import com.google.common.base.Preconditions; +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; import com.google.spanner.v1.ResultSetStats; /** Forwarding implementation of ResultSet that forwards all calls to a delegate. */ public class ForwardingResultSet extends ForwardingStructReader implements ResultSet { - private ResultSet delegate; + private Supplier delegate; public ForwardingResultSet(ResultSet delegate) { super(delegate); - this.delegate = Preconditions.checkNotNull(delegate); + this.delegate = Suppliers.ofInstance(Preconditions.checkNotNull(delegate)); + } + + public ForwardingResultSet(Supplier supplier) { + super(supplier); + this.delegate = supplier; } /** @@ -39,26 +46,26 @@ public ForwardingResultSet(ResultSet delegate) { void replaceDelegate(ResultSet newDelegate) { Preconditions.checkNotNull(newDelegate); super.replaceDelegate(newDelegate); - this.delegate = newDelegate; + this.delegate = Suppliers.ofInstance(Preconditions.checkNotNull(newDelegate)); } @Override public boolean next() throws SpannerException { - return delegate.next(); + return delegate.get().next(); } @Override public Struct getCurrentRowAsStruct() { - return delegate.getCurrentRowAsStruct(); + return delegate.get().getCurrentRowAsStruct(); } @Override public void close() { - delegate.close(); + delegate.get().close(); } @Override public ResultSetStats getStats() { - return delegate.getStats(); + return delegate.get().getStats(); } } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ForwardingStructReader.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ForwardingStructReader.java index 9b30b89985..67e546ad5a 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ForwardingStructReader.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ForwardingStructReader.java @@ -20,14 +20,20 @@ import com.google.cloud.Date; import com.google.cloud.Timestamp; import com.google.common.base.Preconditions; +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; import java.util.List; /** Forwarding implements of StructReader */ public class ForwardingStructReader implements StructReader { - private StructReader delegate; + private Supplier delegate; public ForwardingStructReader(StructReader delegate) { + this.delegate = Suppliers.ofInstance(Preconditions.checkNotNull(delegate)); + } + + public ForwardingStructReader(Supplier delegate) { this.delegate = Preconditions.checkNotNull(delegate); } @@ -39,221 +45,271 @@ public ForwardingStructReader(StructReader delegate) { * returned to the user. */ void replaceDelegate(StructReader newDelegate) { - this.delegate = Preconditions.checkNotNull(newDelegate); + this.delegate = Suppliers.ofInstance(Preconditions.checkNotNull(newDelegate)); } + /** + * Called before each forwarding call to allow sub classes to do additional state checking. Sub + * classes should throw an {@link Exception} if the current state is not valid for reading data + * from this {@link ForwardingStructReader}. The default implementation does nothing. + */ + protected void checkValidState() {} + @Override public Type getType() { - return delegate.getType(); + checkValidState(); + return delegate.get().getType(); } @Override public int getColumnCount() { - return delegate.getColumnCount(); + checkValidState(); + return delegate.get().getColumnCount(); } @Override public int getColumnIndex(String columnName) { - return delegate.getColumnIndex(columnName); + checkValidState(); + return delegate.get().getColumnIndex(columnName); } @Override public Type getColumnType(int columnIndex) { - return delegate.getColumnType(columnIndex); + checkValidState(); + return delegate.get().getColumnType(columnIndex); } @Override public Type getColumnType(String columnName) { - return delegate.getColumnType(columnName); + checkValidState(); + return delegate.get().getColumnType(columnName); } @Override public boolean isNull(int columnIndex) { - return delegate.isNull(columnIndex); + checkValidState(); + return delegate.get().isNull(columnIndex); } @Override public boolean isNull(String columnName) { - return delegate.isNull(columnName); + checkValidState(); + return delegate.get().isNull(columnName); } @Override public boolean getBoolean(int columnIndex) { - return delegate.getBoolean(columnIndex); + checkValidState(); + return delegate.get().getBoolean(columnIndex); } @Override public boolean getBoolean(String columnName) { - return delegate.getBoolean(columnName); + checkValidState(); + return delegate.get().getBoolean(columnName); } @Override public long getLong(int columnIndex) { - return delegate.getLong(columnIndex); + checkValidState(); + return delegate.get().getLong(columnIndex); } @Override public long getLong(String columnName) { - return delegate.getLong(columnName); + checkValidState(); + return delegate.get().getLong(columnName); } @Override public double getDouble(int columnIndex) { - return delegate.getDouble(columnIndex); + checkValidState(); + return delegate.get().getDouble(columnIndex); } @Override public double getDouble(String columnName) { - return delegate.getDouble(columnName); + checkValidState(); + return delegate.get().getDouble(columnName); } @Override public String getString(int columnIndex) { - return delegate.getString(columnIndex); + checkValidState(); + return delegate.get().getString(columnIndex); } @Override public String getString(String columnName) { - return delegate.getString(columnName); + checkValidState(); + return delegate.get().getString(columnName); } @Override public ByteArray getBytes(int columnIndex) { - return delegate.getBytes(columnIndex); + checkValidState(); + return delegate.get().getBytes(columnIndex); } @Override public ByteArray getBytes(String columnName) { - return delegate.getBytes(columnName); + checkValidState(); + return delegate.get().getBytes(columnName); } @Override public Timestamp getTimestamp(int columnIndex) { - return delegate.getTimestamp(columnIndex); + checkValidState(); + return delegate.get().getTimestamp(columnIndex); } @Override public Timestamp getTimestamp(String columnName) { - return delegate.getTimestamp(columnName); + checkValidState(); + return delegate.get().getTimestamp(columnName); } @Override public Date getDate(int columnIndex) { - return delegate.getDate(columnIndex); + checkValidState(); + return delegate.get().getDate(columnIndex); } @Override public Date getDate(String columnName) { - return delegate.getDate(columnName); + checkValidState(); + return delegate.get().getDate(columnName); } @Override public boolean[] getBooleanArray(int columnIndex) { - return delegate.getBooleanArray(columnIndex); + checkValidState(); + return delegate.get().getBooleanArray(columnIndex); } @Override public boolean[] getBooleanArray(String columnName) { - return delegate.getBooleanArray(columnName); + checkValidState(); + return delegate.get().getBooleanArray(columnName); } @Override public List getBooleanList(int columnIndex) { - return delegate.getBooleanList(columnIndex); + checkValidState(); + return delegate.get().getBooleanList(columnIndex); } @Override public List getBooleanList(String columnName) { - return delegate.getBooleanList(columnName); + checkValidState(); + return delegate.get().getBooleanList(columnName); } @Override public long[] getLongArray(int columnIndex) { - return delegate.getLongArray(columnIndex); + checkValidState(); + return delegate.get().getLongArray(columnIndex); } @Override public long[] getLongArray(String columnName) { - return delegate.getLongArray(columnName); + checkValidState(); + return delegate.get().getLongArray(columnName); } @Override public List getLongList(int columnIndex) { - return delegate.getLongList(columnIndex); + checkValidState(); + return delegate.get().getLongList(columnIndex); } @Override public List getLongList(String columnName) { - return delegate.getLongList(columnName); + checkValidState(); + return delegate.get().getLongList(columnName); } @Override public double[] getDoubleArray(int columnIndex) { - return delegate.getDoubleArray(columnIndex); + checkValidState(); + return delegate.get().getDoubleArray(columnIndex); } @Override public double[] getDoubleArray(String columnName) { - return delegate.getDoubleArray(columnName); + checkValidState(); + return delegate.get().getDoubleArray(columnName); } @Override public List getDoubleList(int columnIndex) { - return delegate.getDoubleList(columnIndex); + checkValidState(); + return delegate.get().getDoubleList(columnIndex); } @Override public List getDoubleList(String columnName) { - return delegate.getDoubleList(columnName); + checkValidState(); + return delegate.get().getDoubleList(columnName); } @Override public List getStringList(int columnIndex) { - return delegate.getStringList(columnIndex); + checkValidState(); + return delegate.get().getStringList(columnIndex); } @Override public List getStringList(String columnName) { - return delegate.getStringList(columnName); + checkValidState(); + return delegate.get().getStringList(columnName); } @Override public List getBytesList(int columnIndex) { - return delegate.getBytesList(columnIndex); + checkValidState(); + return delegate.get().getBytesList(columnIndex); } @Override public List getBytesList(String columnName) { - return delegate.getBytesList(columnName); + checkValidState(); + return delegate.get().getBytesList(columnName); } @Override public List getTimestampList(int columnIndex) { - return delegate.getTimestampList(columnIndex); + checkValidState(); + return delegate.get().getTimestampList(columnIndex); } @Override public List getTimestampList(String columnName) { - return delegate.getTimestampList(columnName); + checkValidState(); + return delegate.get().getTimestampList(columnName); } @Override public List getDateList(int columnIndex) { - return delegate.getDateList(columnIndex); + checkValidState(); + return delegate.get().getDateList(columnIndex); } @Override public List getDateList(String columnName) { - return delegate.getDateList(columnName); + checkValidState(); + return delegate.get().getDateList(columnName); } @Override public List getStructList(int columnIndex) { - return delegate.getStructList(columnIndex); + checkValidState(); + return delegate.get().getStructList(columnIndex); } @Override public List getStructList(String columnName) { - return delegate.getStructList(columnName); + checkValidState(); + return delegate.get().getStructList(columnName); } } 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 d193ad1c75..879b632d17 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 @@ -59,6 +59,11 @@ public static ReadAndQueryOption prefetchChunks(int prefetchChunks) { return new FlowControlOption(prefetchChunks); } + public static ReadAndQueryOption bufferRows(int bufferRows) { + Preconditions.checkArgument(bufferRows > 0, "bufferRows should be greater than 0"); + return new BufferRowsOption(bufferRows); + } + /** * Specifying this will cause the list operations to fetch at most this many records in a page. */ @@ -115,8 +120,22 @@ void appendToOptions(Options options) { } } + static final class BufferRowsOption extends InternalOption implements ReadAndQueryOption { + final int bufferRows; + + BufferRowsOption(int bufferRows) { + this.bufferRows = bufferRows; + } + + @Override + void appendToOptions(Options options) { + options.bufferRows = bufferRows; + } + } + private Long limit; private Integer prefetchChunks; + private Integer bufferRows; private Integer pageSize; private String pageToken; private String filter; @@ -140,6 +159,14 @@ int prefetchChunks() { return prefetchChunks; } + boolean hasBufferRows() { + return bufferRows != null; + } + + int bufferRows() { + return bufferRows; + } + boolean hasPageSize() { return pageSize != null; } @@ -203,6 +230,10 @@ public boolean equals(Object o) { || hasPrefetchChunks() && that.hasPrefetchChunks() && Objects.equals(prefetchChunks(), that.prefetchChunks())) + && (!hasBufferRows() && !that.hasBufferRows() + || hasBufferRows() + && that.hasBufferRows() + && Objects.equals(bufferRows(), that.bufferRows())) && (!hasPageSize() && !that.hasPageSize() || hasPageSize() && that.hasPageSize() && Objects.equals(pageSize(), that.pageSize())) && Objects.equals(pageToken(), that.pageToken()) @@ -218,6 +249,9 @@ public int hashCode() { if (prefetchChunks != null) { result = 31 * result + prefetchChunks.hashCode(); } + if (bufferRows != null) { + result = 31 * result + bufferRows.hashCode(); + } if (pageSize != null) { result = 31 * result + pageSize.hashCode(); } 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 fdde68989f..96ae390dd6 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 @@ -18,20 +18,33 @@ import static com.google.common.base.Preconditions.checkState; +import com.google.api.gax.grpc.GrpcStatusCode; +import com.google.api.gax.rpc.ServerStream; +import com.google.api.gax.rpc.UnavailableException; import com.google.cloud.spanner.SessionImpl.SessionTransaction; import com.google.cloud.spanner.spi.v1.SpannerRpc; +import com.google.common.base.Stopwatch; import com.google.protobuf.ByteString; import com.google.spanner.v1.BeginTransactionRequest; import com.google.spanner.v1.ExecuteSqlRequest; import com.google.spanner.v1.ExecuteSqlRequest.QueryMode; +import com.google.spanner.v1.PartialResultSet; import com.google.spanner.v1.Transaction; import com.google.spanner.v1.TransactionOptions; import com.google.spanner.v1.TransactionSelector; +import io.grpc.Status.Code; +import io.opencensus.trace.Span; import java.util.Map; -import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.threeten.bp.Duration; +import org.threeten.bp.temporal.ChronoUnit; /** Partitioned DML transaction for bulk updates and deletes. */ class PartitionedDMLTransaction implements SessionTransaction { + private static final Logger log = Logger.getLogger(PartitionedDMLTransaction.class.getName()); + private final SessionImpl session; private final SpannerRpc rpc; private volatile boolean isValid = true; @@ -60,45 +73,96 @@ private ByteString initTransaction() { /** * Executes the {@link Statement} using a partitioned dml transaction with automatic retry if the - * transaction was aborted. + * transaction was aborted. The update method uses the ExecuteStreamingSql RPC to execute the + * statement, and will retry the stream if an {@link UnavailableException} is thrown, using the + * last seen resume token if the server returns any. */ - long executePartitionedUpdate(final Statement statement) { + long executeStreamingPartitionedUpdate(final Statement statement, Duration timeout) { checkState(isValid, "Partitioned DML has been invalidated by a new operation on the session"); - Callable callable = - new Callable() { - @Override - public com.google.spanner.v1.ResultSet call() throws Exception { - ByteString transactionId = initTransaction(); - final ExecuteSqlRequest.Builder builder = - ExecuteSqlRequest.newBuilder() - .setSql(statement.getSql()) - .setQueryMode(QueryMode.NORMAL) - .setSession(session.getName()) - .setTransaction(TransactionSelector.newBuilder().setId(transactionId).build()); - 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(), param.getValue().toProto()); - builder.putParamTypes(param.getKey(), param.getValue().getType().toProto()); + log.log(Level.FINER, "Starting PartitionedUpdate statement"); + boolean foundStats = false; + long updateCount = 0L; + Duration remainingTimeout = timeout; + Stopwatch stopWatch = Stopwatch.createStarted(); + try { + // Loop to catch AbortedExceptions. + while (true) { + ByteString resumeToken = ByteString.EMPTY; + try { + ByteString transactionId = initTransaction(); + final ExecuteSqlRequest.Builder builder = + ExecuteSqlRequest.newBuilder() + .setSql(statement.getSql()) + .setQueryMode(QueryMode.NORMAL) + .setSession(session.getName()) + .setTransaction(TransactionSelector.newBuilder().setId(transactionId).build()); + 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(), param.getValue().toProto()); + builder.putParamTypes(param.getKey(), param.getValue().getType().toProto()); + } + } + while (true) { + remainingTimeout = + remainingTimeout.minus(stopWatch.elapsed(TimeUnit.MILLISECONDS), ChronoUnit.MILLIS); + try { + builder.setResumeToken(resumeToken); + ServerStream stream = + rpc.executeStreamingPartitionedDml( + builder.build(), session.getOptions(), remainingTimeout); + for (PartialResultSet rs : stream) { + if (rs.getResumeToken() != null && !ByteString.EMPTY.equals(rs.getResumeToken())) { + resumeToken = rs.getResumeToken(); + } + if (rs.hasStats()) { + foundStats = true; + updateCount += rs.getStats().getRowCountLowerBound(); + } + } + break; + } catch (UnavailableException e) { + // Retry the stream in the same transaction if the stream breaks with + // UnavailableException and we have a resume token. Otherwise, we just retry the + // entire transaction. + if (!ByteString.EMPTY.equals(resumeToken)) { + log.log( + Level.FINER, + "Retrying PartitionedDml stream using resume token '" + + resumeToken.toStringUtf8() + + "' because of broken stream", + e); + } else { + throw new com.google.api.gax.rpc.AbortedException( + e, GrpcStatusCode.of(Code.ABORTED), true); } } - return rpc.executePartitionedDml(builder.build(), session.getOptions()); } - }; - com.google.spanner.v1.ResultSet resultSet = - SpannerRetryHelper.runTxWithRetriesOnAborted( - callable, rpc.getPartitionedDmlRetrySettings()); - if (!resultSet.hasStats()) { - throw new IllegalArgumentException( - "Partitioned DML response missing stats possibly due to non-DML statement as input"); + break; + } catch (com.google.api.gax.rpc.AbortedException e) { + // Retry using a new transaction but with the same session if the transaction is aborted. + log.log(Level.FINER, "Retrying PartitionedDml transaction after AbortedException", e); + } + } + if (!foundStats) { + throw SpannerExceptionFactory.newSpannerException( + ErrorCode.INVALID_ARGUMENT, + "Partitioned DML response missing stats possibly due to non-DML statement as input"); + } + log.log(Level.FINER, "Finished PartitionedUpdate statement"); + return updateCount; + } catch (Exception e) { + throw SpannerExceptionFactory.newSpannerException(e); } - // For partitioned DML, using the row count lower bound. - return resultSet.getStats().getRowCountLowerBound(); } @Override public void invalidate() { isValid = false; } + + // No-op method needed to implement SessionTransaction interface. + @Override + public void setSpan(Span span) {} } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ReadContext.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ReadContext.java index 16f40769fa..e87d40fb20 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ReadContext.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ReadContext.java @@ -16,6 +16,7 @@ package com.google.cloud.spanner; +import com.google.api.core.ApiFuture; import com.google.cloud.spanner.Options.QueryOption; import com.google.cloud.spanner.Options.ReadOption; import javax.annotation.Nullable; @@ -65,6 +66,13 @@ enum QueryAnalyzeMode { */ ResultSet read(String table, KeySet keys, Iterable columns, ReadOption... options); + /** + * Same as {@link #read(String, KeySet, Iterable, ReadOption...)}, but is guaranteed to be + * non-blocking and will return the results as an {@link AsyncResultSet}. + */ + AsyncResultSet readAsync( + String table, KeySet keys, Iterable columns, ReadOption... options); + /** * Reads zero or more rows from a database using an index. * @@ -93,6 +101,13 @@ enum QueryAnalyzeMode { ResultSet readUsingIndex( String table, String index, KeySet keys, Iterable columns, ReadOption... options); + /** + * Same as {@link #readUsingIndex(String, String, KeySet, Iterable, ReadOption...)}, but is + * guaranteed to be non-blocking and will return its results as an {@link AsyncResultSet}. + */ + AsyncResultSet readUsingIndexAsync( + String table, String index, KeySet keys, Iterable columns, ReadOption... options); + /** * Reads a single row from a database, returning {@code null} if the row does not exist. * @@ -112,6 +127,9 @@ ResultSet readUsingIndex( @Nullable Struct readRow(String table, Key key, Iterable columns); + /** Same as {@link #readRow(String, Key, Iterable)}, but is guaranteed to be non-blocking. */ + ApiFuture readRowAsync(String table, Key key, Iterable columns); + /** * Reads a single row from a database using an index, returning {@code null} if the row does not * exist. @@ -134,6 +152,13 @@ ResultSet readUsingIndex( @Nullable Struct readRowUsingIndex(String table, String index, Key key, Iterable columns); + /** + * Same as {@link #readRowUsingIndex(String, String, Key, Iterable)}, but is guaranteed to be + * non-blocking. + */ + ApiFuture readRowUsingIndexAsync( + String table, String index, Key key, Iterable columns); + /** * Executes a query against the database. * @@ -160,6 +185,12 @@ ResultSet readUsingIndex( */ ResultSet executeQuery(Statement statement, QueryOption... options); + /** + * Same as {@link #executeQuery(Statement, QueryOption...)}, but is guaranteed to be non-blocking + * and returns its results as an {@link AsyncResultSet}. + */ + AsyncResultSet executeQueryAsync(Statement statement, QueryOption... options); + /** * Analyzes a query and returns query plan and/or query execution statistics information. * diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResultSets.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResultSets.java index 29c3e52c6a..278b15d967 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResultSets.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResultSets.java @@ -16,6 +16,8 @@ package com.google.cloud.spanner; +import com.google.api.gax.core.ExecutorProvider; +import com.google.api.gax.core.InstantiatingExecutorProvider; import com.google.cloud.ByteArray; import com.google.cloud.Date; import com.google.cloud.Timestamp; @@ -23,6 +25,7 @@ import com.google.cloud.spanner.Type.StructField; import com.google.common.base.Preconditions; import com.google.common.collect.Lists; +import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.google.spanner.v1.ResultSetStats; import java.util.List; @@ -41,6 +44,30 @@ public static ResultSet forRows(Type type, Iterable rows) { return new PrePopulatedResultSet(type, rows); } + /** Converts the given {@link ResultSet} to an {@link AsyncResultSet}. */ + public static AsyncResultSet toAsyncResultSet(ResultSet delegate) { + return new AsyncResultSetImpl( + InstantiatingExecutorProvider.newBuilder() + .setExecutorThreadCount(1) + .setThreadFactory( + new ThreadFactoryBuilder() + .setDaemon(true) + .setNameFormat("test-async-resultset-%d") + .build()) + .build(), + delegate, + 100); + } + + /** + * Converts the given {@link ResultSet} to an {@link AsyncResultSet} using the given {@link + * ExecutorProvider}. + */ + public static AsyncResultSet toAsyncResultSet( + ResultSet delegate, ExecutorProvider executorProvider) { + return new AsyncResultSetImpl(executorProvider, delegate, 100); + } + private static class PrePopulatedResultSet implements ResultSet { private final List rows; private final Type type; 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 d1de6e204f..ce4d27e94e 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 @@ -20,6 +20,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.api.core.ApiFuture; +import com.google.api.core.SettableApiFuture; import com.google.cloud.Timestamp; import com.google.cloud.spanner.AbstractReadContext.MultiUseReadOnlyTransaction; import com.google.cloud.spanner.AbstractReadContext.SingleReadContext; @@ -28,6 +29,7 @@ import com.google.cloud.spanner.TransactionRunnerImpl.TransactionContextImpl; import com.google.cloud.spanner.spi.v1.SpannerRpc; import com.google.common.collect.Lists; +import com.google.common.util.concurrent.MoreExecutors; import com.google.protobuf.ByteString; import com.google.protobuf.Empty; import com.google.spanner.v1.BeginTransactionRequest; @@ -43,6 +45,7 @@ import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.concurrent.ExecutionException; import javax.annotation.Nullable; /** @@ -76,14 +79,17 @@ static void throwIfTransactionsPending() { static interface SessionTransaction { /** Invalidates the transaction, generally because a new one has been started on the session. */ void invalidate(); + /** Registers the current span on the transaction. */ + void setSpan(Span span); } private final SpannerImpl spanner; private final String name; private final DatabaseId databaseId; private SessionTransaction activeTransaction; - private ByteString readyTransactionId; + ByteString readyTransactionId; private final Map options; + private Span currentSpan; SessionImpl(SpannerImpl spanner, String name, Map options) { this.spanner = spanner; @@ -101,11 +107,16 @@ public String getName() { return options; } + void setCurrentSpan(Span span) { + currentSpan = span; + } + @Override public long executePartitionedUpdate(Statement stmt) { setActive(null); PartitionedDMLTransaction txn = new PartitionedDMLTransaction(this, spanner.getRpc()); - return txn.executePartitionedUpdate(stmt); + return txn.executeStreamingPartitionedUpdate( + stmt, spanner.getOptions().getPartitionedDmlTimeout()); } @Override @@ -169,6 +180,8 @@ public ReadContext singleUse(TimestampBound bound) { .setRpc(spanner.getRpc()) .setDefaultQueryOptions(spanner.getDefaultQueryOptions(databaseId)) .setDefaultPrefetchChunks(spanner.getDefaultPrefetchChunks()) + .setSpan(currentSpan) + .setExecutorProvider(spanner.getAsyncExecutorProvider()) .build()); } @@ -186,6 +199,8 @@ public ReadOnlyTransaction singleUseReadOnlyTransaction(TimestampBound bound) { .setRpc(spanner.getRpc()) .setDefaultQueryOptions(spanner.getDefaultQueryOptions(databaseId)) .setDefaultPrefetchChunks(spanner.getDefaultPrefetchChunks()) + .setSpan(currentSpan) + .setExecutorProvider(spanner.getAsyncExecutorProvider()) .buildSingleUseReadOnlyTransaction()); } @@ -203,6 +218,8 @@ public ReadOnlyTransaction readOnlyTransaction(TimestampBound bound) { .setRpc(spanner.getRpc()) .setDefaultQueryOptions(spanner.getDefaultQueryOptions(databaseId)) .setDefaultPrefetchChunks(spanner.getDefaultPrefetchChunks()) + .setSpan(currentSpan) + .setExecutorProvider(spanner.getAsyncExecutorProvider()) .build()); } @@ -212,6 +229,23 @@ public TransactionRunner readWriteTransaction() { new TransactionRunnerImpl(this, spanner.getRpc(), spanner.getDefaultPrefetchChunks())); } + @Override + public AsyncRunner runAsync() { + return new AsyncRunnerImpl( + setActive( + new TransactionRunnerImpl(this, spanner.getRpc(), spanner.getDefaultPrefetchChunks()))); + } + + @Override + public TransactionManager transactionManager() { + return new TransactionManagerImpl(this, currentSpan); + } + + @Override + public AsyncTransactionManagerImpl transactionManagerAsync() { + return new AsyncTransactionManagerImpl(this, currentSpan); + } + @Override public void prepareReadWriteTransaction() { setActive(null); @@ -237,27 +271,59 @@ public void close() { } ByteString beginTransaction() { - Span span = tracer.spanBuilder(SpannerImpl.BEGIN_TRANSACTION).startSpan(); - try (Scope s = tracer.withSpan(span)) { - final BeginTransactionRequest request = - BeginTransactionRequest.newBuilder() - .setSession(name) - .setOptions( - TransactionOptions.newBuilder() - .setReadWrite(TransactionOptions.ReadWrite.getDefaultInstance())) - .build(); - Transaction txn = spanner.getRpc().beginTransaction(request, options); - if (txn.getId().isEmpty()) { - throw newSpannerException(ErrorCode.INTERNAL, "Missing id in transaction\n" + getName()); - } - span.end(TraceUtil.END_SPAN_OPTIONS); - return txn.getId(); - } catch (RuntimeException e) { - TraceUtil.endSpanWithFailure(span, e); - throw e; + try { + return beginTransactionAsync().get(); + } catch (ExecutionException e) { + throw SpannerExceptionFactory.newSpannerException(e.getCause() == null ? e : e.getCause()); + } catch (InterruptedException e) { + throw SpannerExceptionFactory.propagateInterrupt(e); } } + ApiFuture beginTransactionAsync() { + final SettableApiFuture res = SettableApiFuture.create(); + final Span span = tracer.spanBuilder(SpannerImpl.BEGIN_TRANSACTION).startSpan(); + final BeginTransactionRequest request = + BeginTransactionRequest.newBuilder() + .setSession(name) + .setOptions( + TransactionOptions.newBuilder() + .setReadWrite(TransactionOptions.ReadWrite.getDefaultInstance())) + .build(); + final ApiFuture requestFuture = + spanner.getRpc().beginTransactionAsync(request, options); + requestFuture.addListener( + tracer.withSpan( + span, + new Runnable() { + @Override + public void run() { + try { + Transaction txn = requestFuture.get(); + if (txn.getId().isEmpty()) { + throw newSpannerException( + ErrorCode.INTERNAL, "Missing id in transaction\n" + getName()); + } + span.end(TraceUtil.END_SPAN_OPTIONS); + res.set(txn.getId()); + } catch (ExecutionException e) { + TraceUtil.endSpanWithFailure(span, e); + res.setException( + SpannerExceptionFactory.newSpannerException( + e.getCause() == null ? e : e.getCause())); + } catch (InterruptedException e) { + TraceUtil.endSpanWithFailure(span, e); + res.setException(SpannerExceptionFactory.propagateInterrupt(e)); + } catch (Exception e) { + TraceUtil.endSpanWithFailure(span, e); + res.setException(e); + } + } + }), + MoreExecutors.directExecutor()); + return res; + } + TransactionContextImpl newTransaction() { return TransactionContextImpl.newBuilder() .setSession(this) @@ -265,6 +331,8 @@ TransactionContextImpl newTransaction() { .setRpc(spanner.getRpc()) .setDefaultQueryOptions(spanner.getDefaultQueryOptions(databaseId)) .setDefaultPrefetchChunks(spanner.getDefaultPrefetchChunks()) + .setSpan(currentSpan) + .setExecutorProvider(spanner.getAsyncExecutorProvider()) .build(); } @@ -276,11 +344,13 @@ T setActive(@Nullable T ctx) { } activeTransaction = ctx; readyTransactionId = null; + if (activeTransaction != null) { + activeTransaction.setSpan(currentSpan); + } return ctx; } - @Override - public TransactionManager transactionManager() { - return new TransactionManagerImpl(this); + boolean hasReadyTransaction() { + return readyTransactionId != null; } } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPool.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPool.java index 776c3df1c9..90e399fad6 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPool.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPool.java @@ -40,6 +40,8 @@ import com.google.api.core.ApiFuture; import com.google.api.core.ApiFutures; +import com.google.api.core.SettableApiFuture; +import com.google.api.gax.core.ExecutorProvider; import com.google.cloud.Timestamp; import com.google.cloud.grpc.GrpcTransportOptions; import com.google.cloud.grpc.GrpcTransportOptions.ExecutorFactory; @@ -48,6 +50,7 @@ import com.google.cloud.spanner.SessionClient.SessionConsumer; import com.google.cloud.spanner.SpannerException.ResourceNotFoundException; import com.google.cloud.spanner.SpannerImpl.ClosedException; +import com.google.cloud.spanner.TransactionManager.TransactionState; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Function; import com.google.common.base.MoreObjects; @@ -56,11 +59,12 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import com.google.common.util.concurrent.ForwardingListenableFuture; +import com.google.common.util.concurrent.ForwardingListenableFuture.SimpleForwardingListenableFuture; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.SettableFuture; import com.google.common.util.concurrent.ThreadFactoryBuilder; -import com.google.common.util.concurrent.Uninterruptibles; import com.google.protobuf.Empty; import io.opencensus.common.Scope; import io.opencensus.common.ToLongFunction; @@ -85,12 +89,17 @@ import java.util.Queue; import java.util.Random; import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.SynchronousQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.logging.Level; import java.util.logging.Logger; @@ -132,53 +141,115 @@ Instant instant() { } } + private abstract static class CachedResultSetSupplier implements Supplier { + private ResultSet cached; + + abstract ResultSet load(); + + ResultSet reload() { + return cached = load(); + } + + @Override + public ResultSet get() { + if (cached == null) { + cached = load(); + } + return cached; + } + } + /** * Wrapper around {@code ReadContext} that releases the session to the pool once the call is * finished, if it is a single use context. */ private static class AutoClosingReadContext implements ReadContext { - private final Function readContextDelegateSupplier; + /** + * {@link AsyncResultSet} implementation that keeps track of the async operations that are still + * running for this {@link ReadContext} and that should finish before the {@link ReadContext} + * releases its session back into the pool. + */ + private class AutoClosingReadContextAsyncResultSetImpl extends AsyncResultSetImpl { + private AutoClosingReadContextAsyncResultSetImpl( + ExecutorProvider executorProvider, ResultSet delegate, int bufferRows) { + super(executorProvider, delegate, bufferRows); + } + + @Override + public ApiFuture setCallback(Executor exec, ReadyCallback cb) { + Runnable listener = + new Runnable() { + @Override + public void run() { + synchronized (lock) { + if (asyncOperationsCount.decrementAndGet() == 0 && closed) { + // All async operations for this read context have finished. + AutoClosingReadContext.this.close(); + } + } + } + }; + try { + asyncOperationsCount.incrementAndGet(); + addListener(listener); + return super.setCallback(exec, cb); + } catch (Throwable t) { + removeListener(listener); + asyncOperationsCount.decrementAndGet(); + throw t; + } + } + } + + private final Function readContextDelegateSupplier; private T readContextDelegate; private final SessionPool sessionPool; - private PooledSession session; private final boolean isSingleUse; - private boolean closed; + private final AtomicInteger asyncOperationsCount = new AtomicInteger(); + + private Object lock = new Object(); + + @GuardedBy("lock") private boolean sessionUsedForQuery = false; + @GuardedBy("lock") + private PooledSessionFuture session; + + @GuardedBy("lock") + private boolean closed; + + @GuardedBy("lock") + private boolean delegateClosed; + private AutoClosingReadContext( - Function delegateSupplier, + Function delegateSupplier, SessionPool sessionPool, - PooledSession session, + PooledSessionFuture session, boolean isSingleUse) { this.readContextDelegateSupplier = delegateSupplier; this.sessionPool = sessionPool; this.session = session; this.isSingleUse = isSingleUse; - while (true) { - try { - this.readContextDelegate = readContextDelegateSupplier.apply(this.session); - break; - } catch (SessionNotFoundException e) { - replaceSessionIfPossible(e); - } - } } T getReadContextDelegate() { + synchronized (lock) { + if (readContextDelegate == null) { + while (true) { + try { + this.readContextDelegate = readContextDelegateSupplier.apply(this.session); + break; + } catch (SessionNotFoundException e) { + replaceSessionIfPossible(e); + } + } + } + } return readContextDelegate; } - private ResultSet wrap(final Supplier resultSetSupplier) { - ResultSet res; - while (true) { - try { - res = resultSetSupplier.get(); - break; - } catch (SessionNotFoundException e) { - replaceSessionIfPossible(e); - } - } - return new ForwardingResultSet(res) { + private ResultSet wrap(final CachedResultSetSupplier resultSetSupplier) { + return new ForwardingResultSet(resultSetSupplier) { private boolean beforeFirst = true; @Override @@ -187,8 +258,18 @@ public boolean next() throws SpannerException { try { return internalNext(); } catch (SessionNotFoundException e) { - replaceSessionIfPossible(e); - replaceDelegate(resultSetSupplier.get()); + while (true) { + // Keep the replace-if-possible outside the try-block to let the exception bubble up + // if it's too late to replace the session. + replaceSessionIfPossible(e); + try { + replaceDelegate(resultSetSupplier.reload()); + break; + } catch (SessionNotFoundException snfe) { + e = snfe; + // retry on yet another session. + } + } } } } @@ -197,9 +278,11 @@ private boolean internalNext() { try { boolean ret = super.next(); if (beforeFirst) { - session.markUsed(); - beforeFirst = false; - sessionUsedForQuery = true; + synchronized (lock) { + session.get().markUsed(); + beforeFirst = false; + sessionUsedForQuery = true; + } } if (!ret && isSingleUse) { close(); @@ -208,9 +291,11 @@ private boolean internalNext() { } catch (SessionNotFoundException e) { throw e; } catch (SpannerException e) { - if (!closed && isSingleUse) { - session.lastException = e; - AutoClosingReadContext.this.close(); + synchronized (lock) { + if (!closed && isSingleUse) { + session.get().lastException = e; + AutoClosingReadContext.this.close(); + } } throw e; } @@ -218,22 +303,27 @@ private boolean internalNext() { @Override public void close() { - super.close(); - if (isSingleUse) { - AutoClosingReadContext.this.close(); + try { + super.close(); + } finally { + if (isSingleUse) { + AutoClosingReadContext.this.close(); + } } } }; } - private void replaceSessionIfPossible(SessionNotFoundException e) { - if (isSingleUse || !sessionUsedForQuery) { - // This class is only used by read-only transactions, so we know that we only need a - // read-only session. - session = sessionPool.replaceReadSession(e, session); - readContextDelegate = readContextDelegateSupplier.apply(session); - } else { - throw e; + private void replaceSessionIfPossible(SessionNotFoundException notFound) { + synchronized (lock) { + if (isSingleUse || !sessionUsedForQuery) { + // This class is only used by read-only transactions, so we know that we only need a + // read-only session. + session = sessionPool.replaceReadSession(notFound, session); + readContextDelegate = readContextDelegateSupplier.apply(session); + } else { + throw notFound; + } } } @@ -244,14 +334,37 @@ public ResultSet read( final Iterable columns, final ReadOption... options) { return wrap( - new Supplier() { + new CachedResultSetSupplier() { @Override - public ResultSet get() { - return readContextDelegate.read(table, keys, columns, options); + ResultSet load() { + return getReadContextDelegate().read(table, keys, columns, options); } }); } + @Override + public AsyncResultSet readAsync( + final String table, + final KeySet keys, + final Iterable columns, + final ReadOption... options) { + Options readOptions = Options.fromReadOptions(options); + final int bufferRows = + readOptions.hasBufferRows() + ? readOptions.bufferRows() + : AsyncResultSetImpl.DEFAULT_BUFFER_SIZE; + return new AutoClosingReadContextAsyncResultSetImpl( + sessionPool.sessionClient.getSpanner().getAsyncExecutorProvider(), + wrap( + new CachedResultSetSupplier() { + @Override + ResultSet load() { + return getReadContextDelegate().read(table, keys, columns, options); + } + }), + bufferRows); + } + @Override public ResultSet readUsingIndex( final String table, @@ -260,84 +373,159 @@ public ResultSet readUsingIndex( final Iterable columns, final ReadOption... options) { return wrap( - new Supplier() { + new CachedResultSetSupplier() { @Override - public ResultSet get() { - return readContextDelegate.readUsingIndex(table, index, keys, columns, options); + ResultSet load() { + return getReadContextDelegate().readUsingIndex(table, index, keys, columns, options); } }); } + @Override + public AsyncResultSet readUsingIndexAsync( + final String table, + final String index, + final KeySet keys, + final Iterable columns, + final ReadOption... options) { + Options readOptions = Options.fromReadOptions(options); + final int bufferRows = + readOptions.hasBufferRows() + ? readOptions.bufferRows() + : AsyncResultSetImpl.DEFAULT_BUFFER_SIZE; + return new AutoClosingReadContextAsyncResultSetImpl( + sessionPool.sessionClient.getSpanner().getAsyncExecutorProvider(), + wrap( + new CachedResultSetSupplier() { + @Override + ResultSet load() { + return getReadContextDelegate() + .readUsingIndex(table, index, keys, columns, options); + } + }), + bufferRows); + } + @Override @Nullable public Struct readRow(String table, Key key, Iterable columns) { try { while (true) { try { - session.markUsed(); - return readContextDelegate.readRow(table, key, columns); + synchronized (lock) { + session.get().markUsed(); + } + return getReadContextDelegate().readRow(table, key, columns); } catch (SessionNotFoundException e) { replaceSessionIfPossible(e); } } } finally { - sessionUsedForQuery = true; + synchronized (lock) { + sessionUsedForQuery = true; + } if (isSingleUse) { close(); } } } + @Override + public ApiFuture readRowAsync(String table, Key key, Iterable columns) { + try (AsyncResultSet rs = readAsync(table, KeySet.singleKey(key), columns)) { + return AbstractReadContext.consumeSingleRowAsync(rs); + } + } + @Override @Nullable public Struct readRowUsingIndex(String table, String index, Key key, Iterable columns) { try { while (true) { try { - session.markUsed(); - return readContextDelegate.readRowUsingIndex(table, index, key, columns); + synchronized (lock) { + session.get().markUsed(); + } + return getReadContextDelegate().readRowUsingIndex(table, index, key, columns); } catch (SessionNotFoundException e) { replaceSessionIfPossible(e); } } } finally { - sessionUsedForQuery = true; + synchronized (lock) { + sessionUsedForQuery = true; + } if (isSingleUse) { close(); } } } + @Override + public ApiFuture readRowUsingIndexAsync( + String table, String index, Key key, Iterable columns) { + try (AsyncResultSet rs = readUsingIndexAsync(table, index, KeySet.singleKey(key), columns)) { + return AbstractReadContext.consumeSingleRowAsync(rs); + } + } + @Override public ResultSet executeQuery(final Statement statement, final QueryOption... options) { return wrap( - new Supplier() { + new CachedResultSetSupplier() { @Override - public ResultSet get() { - return readContextDelegate.executeQuery(statement, options); + ResultSet load() { + return getReadContextDelegate().executeQuery(statement, options); } }); } + @Override + public AsyncResultSet executeQueryAsync( + final Statement statement, final QueryOption... options) { + Options queryOptions = Options.fromQueryOptions(options); + final int bufferRows = + queryOptions.hasBufferRows() + ? queryOptions.bufferRows() + : AsyncResultSetImpl.DEFAULT_BUFFER_SIZE; + return new AutoClosingReadContextAsyncResultSetImpl( + sessionPool.sessionClient.getSpanner().getAsyncExecutorProvider(), + wrap( + new CachedResultSetSupplier() { + @Override + ResultSet load() { + return getReadContextDelegate().executeQuery(statement, options); + } + }), + bufferRows); + } + @Override public ResultSet analyzeQuery(final Statement statement, final QueryAnalyzeMode queryMode) { return wrap( - new Supplier() { + new CachedResultSetSupplier() { @Override - public ResultSet get() { - return readContextDelegate.analyzeQuery(statement, queryMode); + ResultSet load() { + return getReadContextDelegate().analyzeQuery(statement, queryMode); } }); } @Override public void close() { - if (closed) { - return; + synchronized (lock) { + if (closed && delegateClosed) { + return; + } + closed = true; + if (asyncOperationsCount.get() == 0) { + if (readContextDelegate != null) { + readContextDelegate.close(); + } + session.close(); + delegateClosed = true; + } } - closed = true; - readContextDelegate.close(); - session.close(); } } @@ -345,9 +533,9 @@ private static class AutoClosingReadTransaction extends AutoClosingReadContext implements ReadOnlyTransaction { AutoClosingReadTransaction( - Function txnSupplier, + Function txnSupplier, SessionPool sessionPool, - PooledSession session, + PooledSessionFuture session, boolean isSingleUse) { super(txnSupplier, sessionPool, session, isSingleUse); } @@ -394,6 +582,13 @@ public ResultSet read( return new SessionPoolResultSet(delegate.read(table, keys, columns, options)); } + @Override + public AsyncResultSet readAsync( + String table, KeySet keys, Iterable columns, ReadOption... options) { + throw SpannerExceptionFactory.newSpannerException( + ErrorCode.UNIMPLEMENTED, "not yet implemented"); + } + @Override public ResultSet readUsingIndex( String table, @@ -405,6 +600,17 @@ public ResultSet readUsingIndex( delegate.readUsingIndex(table, index, keys, columns, options)); } + @Override + public AsyncResultSet readUsingIndexAsync( + String table, + String index, + KeySet keys, + Iterable columns, + ReadOption... options) { + throw SpannerExceptionFactory.newSpannerException( + ErrorCode.UNIMPLEMENTED, "not yet implemented"); + } + @Override public Struct readRow(String table, Key key, Iterable columns) { try { @@ -414,6 +620,13 @@ public Struct readRow(String table, Key key, Iterable columns) { } } + @Override + public ApiFuture readRowAsync(String table, Key key, Iterable columns) { + try (AsyncResultSet rs = readAsync(table, KeySet.singleKey(key), columns)) { + return AbstractReadContext.consumeSingleRowAsync(rs); + } + } + @Override public void buffer(Mutation mutation) { delegate.buffer(mutation); @@ -429,6 +642,15 @@ public Struct readRowUsingIndex( } } + @Override + public ApiFuture readRowUsingIndexAsync( + String table, String index, Key key, Iterable columns) { + try (AsyncResultSet rs = + readUsingIndexAsync(table, index, KeySet.singleKey(key), columns)) { + return AbstractReadContext.consumeSingleRowAsync(rs); + } + } + @Override public void buffer(Iterable mutations) { delegate.buffer(mutations); @@ -443,6 +665,15 @@ public long executeUpdate(Statement statement) { } } + @Override + public ApiFuture executeUpdateAsync(Statement statement) { + try { + return delegate.executeUpdateAsync(statement); + } catch (SessionNotFoundException e) { + throw handleSessionNotFound(e); + } + } + @Override public long[] batchUpdate(Iterable statements) { try { @@ -452,11 +683,29 @@ public long[] batchUpdate(Iterable statements) { } } + @Override + public ApiFuture batchUpdateAsync(Iterable statements) { + try { + return delegate.batchUpdateAsync(statements); + } catch (SessionNotFoundException e) { + throw handleSessionNotFound(e); + } + } + @Override public ResultSet executeQuery(Statement statement, QueryOption... options) { return new SessionPoolResultSet(delegate.executeQuery(statement, options)); } + @Override + public AsyncResultSet executeQueryAsync(Statement statement, QueryOption... options) { + try { + return delegate.executeQueryAsync(statement, options); + } catch (SessionNotFoundException e) { + throw handleSessionNotFound(e); + } + } + @Override public ResultSet analyzeQuery(Statement statement, QueryAnalyzeMode queryMode) { return new SessionPoolResultSet(delegate.analyzeQuery(statement, queryMode)); @@ -470,39 +719,40 @@ public void close() { private TransactionManager delegate; private final SessionPool sessionPool; - private PooledSession session; + private PooledSessionFuture session; private boolean closed; private boolean restartedAfterSessionNotFound; - AutoClosingTransactionManager(SessionPool sessionPool, PooledSession session) { + AutoClosingTransactionManager(SessionPool sessionPool, PooledSessionFuture session) { this.sessionPool = sessionPool; this.session = session; - this.delegate = session.delegate.transactionManager(); } @Override public TransactionContext begin() { + this.delegate = session.get().transactionManager(); while (true) { try { return internalBegin(); } catch (SessionNotFoundException e) { session = sessionPool.replaceReadWriteSession(e, session); - delegate = session.delegate.transactionManager(); + delegate = session.get().delegate.transactionManager(); } } } private TransactionContext internalBegin() { TransactionContext res = new SessionPoolTransactionContext(delegate.begin()); - session.markUsed(); + session.get().markUsed(); return res; } - private SpannerException handleSessionNotFound(SessionNotFoundException e) { - session = sessionPool.replaceReadWriteSession(e, session); - delegate = session.delegate.transactionManager(); + private SpannerException handleSessionNotFound(SessionNotFoundException notFound) { + session = sessionPool.replaceReadWriteSession(notFound, session); + delegate = session.get().delegate.transactionManager(); restartedAfterSessionNotFound = true; - return SpannerExceptionFactory.newSpannerException(ErrorCode.ABORTED, e.getMessage(), e); + return SpannerExceptionFactory.newSpannerException( + ErrorCode.ABORTED, notFound.getMessage(), notFound); } @Override @@ -540,7 +790,7 @@ public TransactionContext resetForRetry() { } } catch (SessionNotFoundException e) { session = sessionPool.replaceReadWriteSession(e, session); - delegate = session.delegate.transactionManager(); + delegate = session.get().delegate.transactionManager(); restartedAfterSessionNotFound = true; } } @@ -558,7 +808,9 @@ public void close() { } closed = true; try { - delegate.close(); + if (delegate != null) { + delegate.close(); + } } finally { session.close(); } @@ -569,7 +821,7 @@ public TransactionState getState() { if (restartedAfterSessionNotFound) { return TransactionState.ABORTED; } else { - return delegate.getState(); + return delegate == null ? null : delegate.getState(); } } } @@ -580,13 +832,19 @@ public TransactionState getState() { */ private static final class SessionPoolTransactionRunner implements TransactionRunner { private final SessionPool sessionPool; - private PooledSession session; + private PooledSessionFuture session; private TransactionRunner runner; - private SessionPoolTransactionRunner(SessionPool sessionPool, PooledSession session) { + private SessionPoolTransactionRunner(SessionPool sessionPool, PooledSessionFuture session) { this.sessionPool = sessionPool; this.session = session; - this.runner = session.delegate.readWriteTransaction(); + } + + private TransactionRunner getRunner() { + if (this.runner == null) { + this.runner = session.get().readWriteTransaction(); + } + return runner; } @Override @@ -596,17 +854,17 @@ public T run(TransactionCallable callable) { T result; while (true) { try { - result = runner.run(callable); + result = getRunner().run(callable); break; } catch (SessionNotFoundException e) { session = sessionPool.replaceReadWriteSession(e, session); - runner = session.delegate.readWriteTransaction(); + runner = session.get().delegate.readWriteTransaction(); } } - session.markUsed(); + session.get().markUsed(); return result; } catch (SpannerException e) { - throw session.lastException = e; + throw session.get().lastException = e; } finally { session.close(); } @@ -614,19 +872,86 @@ public T run(TransactionCallable callable) { @Override public Timestamp getCommitTimestamp() { - return runner.getCommitTimestamp(); + return getRunner().getCommitTimestamp(); } @Override public TransactionRunner allowNestedTransaction() { - runner.allowNestedTransaction(); + getRunner().allowNestedTransaction(); return this; } } + private static class SessionPoolAsyncRunner implements AsyncRunner { + private final SessionPool sessionPool; + private volatile PooledSessionFuture session; + private final SettableApiFuture commitTimestamp = SettableApiFuture.create(); + + private SessionPoolAsyncRunner(SessionPool sessionPool, PooledSessionFuture session) { + this.sessionPool = sessionPool; + this.session = session; + } + + @Override + public ApiFuture runAsync(final AsyncWork work, Executor executor) { + final SettableApiFuture res = SettableApiFuture.create(); + executor.execute( + new Runnable() { + @Override + public void run() { + SpannerException se = null; + R r = null; + AsyncRunner runner = null; + while (true) { + try { + runner = session.get().runAsync(); + r = runner.runAsync(work, MoreExecutors.directExecutor()).get(); + break; + } catch (ExecutionException e) { + se = SpannerExceptionFactory.newSpannerException(e.getCause()); + } catch (InterruptedException e) { + se = SpannerExceptionFactory.propagateInterrupt(e); + } catch (Throwable t) { + se = SpannerExceptionFactory.newSpannerException(t); + } finally { + if (se != null && se instanceof SessionNotFoundException) { + session = + sessionPool.replaceReadWriteSession((SessionNotFoundException) se, session); + } else { + break; + } + } + } + session.get().markUsed(); + session.close(); + setCommitTimestamp(runner); + if (se != null) { + res.setException(se); + } else { + res.set(r); + } + } + }); + return res; + } + + private void setCommitTimestamp(AsyncRunner delegate) { + try { + commitTimestamp.set(delegate.getCommitTimestamp().get()); + } catch (Throwable t) { + commitTimestamp.setException(t); + } + } + + @Override + public ApiFuture getCommitTimestamp() { + return commitTimestamp; + } + } + // Exception class used just to track the stack trace at the point when a session was handed out // from the pool. - private final class LeakedSessionException extends RuntimeException { + final class LeakedSessionException extends RuntimeException { private static final long serialVersionUID = 1451131180314064914L; private LeakedSessionException() { @@ -640,25 +965,124 @@ private enum SessionState { CLOSING, } - final class PooledSession implements Session { - @VisibleForTesting SessionImpl delegate; - private volatile Instant lastUseTime; - private volatile SpannerException lastException; - private volatile LeakedSessionException leakedException; - private volatile boolean allowReplacing = true; + /** + * Forwarding future that will return a {@link PooledSession}. If {@link #inProcessPrepare} has + * been set to true, the returned session will be prepared with a read/write session using the + * thread of the caller to {@link #get()}. This ensures that the executor that is responsible for + * background preparing of read/write transactions is not overwhelmed by requests in case of a + * large burst of write requests. Instead of filling up the queue of the background executor, the + * caller threads will be used for the BeginTransaction call. + */ + private final class ForwardingListenablePooledSessionFuture + extends SimpleForwardingListenableFuture { + private final boolean inProcessPrepare; + private final Span span; + private volatile boolean initialized = false; + private final Object prepareLock = new Object(); + private volatile PooledSession result; + private volatile SpannerException error; + + private ForwardingListenablePooledSessionFuture( + ListenableFuture delegate, boolean inProcessPrepare, Span span) { + super(delegate); + this.inProcessPrepare = inProcessPrepare; + this.span = span; + } - @GuardedBy("lock") - private SessionState state; + @Override + public PooledSession get() throws InterruptedException, ExecutionException { + try { + return initialize(super.get()); + } catch (ExecutionException e) { + throw SpannerExceptionFactory.newSpannerException(e.getCause()); + } catch (InterruptedException e) { + throw SpannerExceptionFactory.propagateInterrupt(e); + } + } - private PooledSession(SessionImpl delegate) { - this.delegate = delegate; - this.state = SessionState.AVAILABLE; - this.lastUseTime = clock.instant(); + @Override + public PooledSession get(long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException { + try { + return initialize(super.get(timeout, unit)); + } catch (ExecutionException e) { + throw SpannerExceptionFactory.newSpannerException(e.getCause()); + } catch (InterruptedException e) { + throw SpannerExceptionFactory.propagateInterrupt(e); + } catch (TimeoutException e) { + throw SpannerExceptionFactory.propagateTimeout(e); + } } - @VisibleForTesting - void setAllowReplacing(boolean allowReplacing) { - this.allowReplacing = allowReplacing; + private PooledSession initialize(PooledSession sess) { + if (!initialized) { + synchronized (prepareLock) { + if (!initialized) { + try { + result = prepare(sess); + } catch (Throwable t) { + error = SpannerExceptionFactory.newSpannerException(t); + } finally { + initialized = true; + } + } + } + } + if (error != null) { + throw error; + } + return result; + } + + private PooledSession prepare(PooledSession sess) { + if (inProcessPrepare && !sess.delegate.hasReadyTransaction()) { + while (true) { + try { + sess.prepareReadWriteTransaction(); + synchronized (lock) { + stopAutomaticPrepare = false; + } + break; + } catch (Throwable t) { + if (isClosed()) { + span.addAnnotation("Pool has been closed"); + throw new IllegalStateException("Pool has been closed"); + } + SpannerException e = newSpannerException(t); + WaiterFuture waiter = new WaiterFuture(); + synchronized (lock) { + handlePrepareSessionFailure(e, sess, false); + if (!isSessionNotFound(e)) { + throw e; + } + readWaiters.add(waiter); + } + sess = waiter.get(); + if (sess.delegate.hasReadyTransaction()) { + break; + } + } + } + } + return sess; + } + } + + private PooledSessionFuture createPooledSessionFuture( + ListenableFuture future, Span span) { + return new PooledSessionFuture(future, span); + } + + final class PooledSessionFuture extends SimpleForwardingListenableFuture + implements Session { + private volatile LeakedSessionException leakedException; + private volatile AtomicBoolean inUse = new AtomicBoolean(); + private volatile CountDownLatch initialized = new CountDownLatch(1); + private final Span span; + + private PooledSessionFuture(ListenableFuture delegate, Span span) { + super(delegate); + this.span = span; } @VisibleForTesting @@ -666,34 +1090,14 @@ void clearLeakedException() { this.leakedException = null; } - private void markBusy() { - this.state = SessionState.BUSY; + private void markCheckedOut() { this.leakedException = new LeakedSessionException(); } - private void markClosing() { - this.state = SessionState.CLOSING; - } - @Override public Timestamp write(Iterable mutations) throws SpannerException { try { - markUsed(); - return delegate.write(mutations); - } catch (SpannerException e) { - throw lastException = e; - } finally { - close(); - } - } - - @Override - public long executePartitionedUpdate(Statement stmt) throws SpannerException { - try { - markUsed(); - return delegate.executePartitionedUpdate(stmt); - } catch (SpannerException e) { - throw lastException = e; + return get().write(mutations); } finally { close(); } @@ -702,10 +1106,7 @@ public long executePartitionedUpdate(Statement stmt) throws SpannerException { @Override public Timestamp writeAtLeastOnce(Iterable mutations) throws SpannerException { try { - markUsed(); - return delegate.writeAtLeastOnce(mutations); - } catch (SpannerException e) { - throw lastException = e; + return get().writeAtLeastOnce(mutations); } finally { close(); } @@ -715,10 +1116,10 @@ public Timestamp writeAtLeastOnce(Iterable mutations) throws SpannerEx public ReadContext singleUse() { try { return new AutoClosingReadContext<>( - new Function() { + new Function() { @Override - public ReadContext apply(PooledSession session) { - return session.delegate.singleUse(); + public ReadContext apply(PooledSessionFuture session) { + return session.get().delegate.singleUse(); } }, SessionPool.this, @@ -734,10 +1135,10 @@ public ReadContext apply(PooledSession session) { public ReadContext singleUse(final TimestampBound bound) { try { return new AutoClosingReadContext<>( - new Function() { + new Function() { @Override - public ReadContext apply(PooledSession session) { - return session.delegate.singleUse(bound); + public ReadContext apply(PooledSessionFuture session) { + return session.get().delegate.singleUse(bound); } }, SessionPool.this, @@ -752,10 +1153,10 @@ public ReadContext apply(PooledSession session) { @Override public ReadOnlyTransaction singleUseReadOnlyTransaction() { return internalReadOnlyTransaction( - new Function() { + new Function() { @Override - public ReadOnlyTransaction apply(PooledSession session) { - return session.delegate.singleUseReadOnlyTransaction(); + public ReadOnlyTransaction apply(PooledSessionFuture session) { + return session.get().delegate.singleUseReadOnlyTransaction(); } }, true); @@ -764,10 +1165,10 @@ public ReadOnlyTransaction apply(PooledSession session) { @Override public ReadOnlyTransaction singleUseReadOnlyTransaction(final TimestampBound bound) { return internalReadOnlyTransaction( - new Function() { + new Function() { @Override - public ReadOnlyTransaction apply(PooledSession session) { - return session.delegate.singleUseReadOnlyTransaction(bound); + public ReadOnlyTransaction apply(PooledSessionFuture session) { + return session.get().delegate.singleUseReadOnlyTransaction(bound); } }, true); @@ -776,10 +1177,10 @@ public ReadOnlyTransaction apply(PooledSession session) { @Override public ReadOnlyTransaction readOnlyTransaction() { return internalReadOnlyTransaction( - new Function() { + new Function() { @Override - public ReadOnlyTransaction apply(PooledSession session) { - return session.delegate.readOnlyTransaction(); + public ReadOnlyTransaction apply(PooledSessionFuture session) { + return session.get().delegate.readOnlyTransaction(); } }, false); @@ -788,17 +1189,18 @@ public ReadOnlyTransaction apply(PooledSession session) { @Override public ReadOnlyTransaction readOnlyTransaction(final TimestampBound bound) { return internalReadOnlyTransaction( - new Function() { + new Function() { @Override - public ReadOnlyTransaction apply(PooledSession session) { - return session.delegate.readOnlyTransaction(bound); + public ReadOnlyTransaction apply(PooledSessionFuture session) { + return session.get().delegate.readOnlyTransaction(bound); } }, false); } private ReadOnlyTransaction internalReadOnlyTransaction( - Function transactionSupplier, boolean isSingleUse) { + Function transactionSupplier, + boolean isSingleUse) { try { return new AutoClosingReadTransaction( transactionSupplier, SessionPool.this, this, isSingleUse); @@ -813,6 +1215,188 @@ public TransactionRunner readWriteTransaction() { return new SessionPoolTransactionRunner(SessionPool.this, this); } + @Override + public TransactionManager transactionManager() { + return new AutoClosingTransactionManager(SessionPool.this, this); + } + + @Override + public AsyncRunner runAsync() { + return new SessionPoolAsyncRunner(SessionPool.this, this); + } + + @Override + public AsyncTransactionManager transactionManagerAsync() { + return new SessionPoolAsyncTransactionManager(this); + } + + @Override + public long executePartitionedUpdate(Statement stmt) { + try { + return get().executePartitionedUpdate(stmt); + } finally { + close(); + } + } + + @Override + public String getName() { + return get().getName(); + } + + @Override + public void prepareReadWriteTransaction() { + get().prepareReadWriteTransaction(); + } + + @Override + public void close() { + synchronized (lock) { + leakedException = null; + checkedOutSessions.remove(this); + } + get().close(); + } + + @Override + public ApiFuture asyncClose() { + synchronized (lock) { + leakedException = null; + checkedOutSessions.remove(this); + } + return get().asyncClose(); + } + + @Override + public PooledSession get() { + if (inUse.compareAndSet(false, true)) { + PooledSession res = null; + try { + res = super.get(); + } catch (Throwable e) { + // ignore the exception as it will be handled by the call to super.get() below. + } + if (res != null) { + res.markBusy(span); + span.addAnnotation(sessionAnnotation(res)); + synchronized (lock) { + incrementNumSessionsInUse(); + checkedOutSessions.add(this); + } + } + initialized.countDown(); + } + try { + initialized.await(); + return super.get(); + } catch (ExecutionException e) { + throw SpannerExceptionFactory.newSpannerException(e.getCause()); + } catch (InterruptedException e) { + throw SpannerExceptionFactory.propagateInterrupt(e); + } + } + } + + final class PooledSession implements Session { + @VisibleForTesting SessionImpl delegate; + private volatile Instant lastUseTime; + private volatile SpannerException lastException; + private volatile boolean allowReplacing = true; + + @GuardedBy("lock") + private SessionState state; + + private PooledSession(SessionImpl delegate) { + this.delegate = delegate; + this.state = SessionState.AVAILABLE; + this.lastUseTime = clock.instant(); + } + + @Override + public String toString() { + return getName(); + } + + @VisibleForTesting + void setAllowReplacing(boolean allowReplacing) { + this.allowReplacing = allowReplacing; + } + + @Override + public Timestamp write(Iterable mutations) throws SpannerException { + try { + markUsed(); + return delegate.write(mutations); + } catch (SpannerException e) { + throw lastException = e; + } + } + + @Override + public Timestamp writeAtLeastOnce(Iterable mutations) throws SpannerException { + try { + markUsed(); + return delegate.writeAtLeastOnce(mutations); + } catch (SpannerException e) { + throw lastException = e; + } + } + + @Override + public long executePartitionedUpdate(Statement stmt) throws SpannerException { + try { + markUsed(); + return delegate.executePartitionedUpdate(stmt); + } catch (SpannerException e) { + throw lastException = e; + } + } + + @Override + public ReadContext singleUse() { + return delegate.singleUse(); + } + + @Override + public ReadContext singleUse(TimestampBound bound) { + return delegate.singleUse(bound); + } + + @Override + public ReadOnlyTransaction singleUseReadOnlyTransaction() { + return delegate.singleUseReadOnlyTransaction(); + } + + @Override + public ReadOnlyTransaction singleUseReadOnlyTransaction(TimestampBound bound) { + return delegate.singleUseReadOnlyTransaction(bound); + } + + @Override + public ReadOnlyTransaction readOnlyTransaction() { + return delegate.readOnlyTransaction(); + } + + @Override + public ReadOnlyTransaction readOnlyTransaction(TimestampBound bound) { + return delegate.readOnlyTransaction(bound); + } + + @Override + public TransactionRunner readWriteTransaction() { + return delegate.readWriteTransaction(); + } + + @Override + public AsyncRunner runAsync() { + return delegate.runAsync(); + } + + @Override + public AsyncTransactionManagerImpl transactionManagerAsync() { + return delegate.transactionManagerAsync(); + } + @Override public ApiFuture asyncClose() { close(); @@ -825,7 +1409,6 @@ public void close() { numSessionsInUse--; numSessionsReleased++; } - leakedException = null; if (lastException != null && isSessionNotFound(lastException)) { invalidateSession(this); } else { @@ -868,59 +1451,56 @@ private void keepAlive() { } } + private void markBusy(Span span) { + this.delegate.setCurrentSpan(span); + this.state = SessionState.BUSY; + } + + private void markClosing() { + this.state = SessionState.CLOSING; + } + void markUsed() { lastUseTime = clock.instant(); } @Override public TransactionManager transactionManager() { - return new AutoClosingTransactionManager(SessionPool.this, this); + return delegate.transactionManager(); } } - private static final class SessionOrError { - private final PooledSession session; - private final SpannerException e; - - SessionOrError(PooledSession session) { - this.session = session; - this.e = null; - } + private final class WaiterFuture extends ForwardingListenableFuture { + private static final long MAX_SESSION_WAIT_TIMEOUT = 240_000L; + private final SettableFuture waiter = SettableFuture.create(); - SessionOrError(SpannerException e) { - this.session = null; - this.e = e; + @Override + protected ListenableFuture delegate() { + return waiter; } - } - - private final class Waiter { - private static final long MAX_SESSION_WAIT_TIMEOUT = 240_000L; - private final SynchronousQueue waiter = new SynchronousQueue<>(); private void put(PooledSession session) { - Uninterruptibles.putUninterruptibly(waiter, new SessionOrError(session)); + waiter.set(session); } private void put(SpannerException e) { - Uninterruptibles.putUninterruptibly(waiter, new SessionOrError(e)); + waiter.setException(e); } - private PooledSession take() throws SpannerException { + @Override + public PooledSession get() { long currentTimeout = options.getInitialWaitForSessionTimeoutMillis(); while (true) { Span span = tracer.spanBuilder(WAIT_FOR_SESSION).startSpan(); try (Scope waitScope = tracer.withSpan(span)) { - SessionOrError s = pollUninterruptiblyWithTimeout(currentTimeout); + PooledSession s = pollUninterruptiblyWithTimeout(currentTimeout); if (s == null) { // Set the status to DEADLINE_EXCEEDED and retry. numWaiterTimeouts.incrementAndGet(); tracer.getCurrentSpan().setStatus(Status.DEADLINE_EXCEEDED); currentTimeout = Math.min(currentTimeout * 2, MAX_SESSION_WAIT_TIMEOUT); } else { - if (s.e != null) { - throw newSpannerException(s.e); - } - return s.session; + return s; } } catch (Exception e) { TraceUtil.setWithFailure(span, e); @@ -931,14 +1511,18 @@ private PooledSession take() throws SpannerException { } } - private SessionOrError pollUninterruptiblyWithTimeout(long timeoutMillis) { + private PooledSession pollUninterruptiblyWithTimeout(long timeoutMillis) { boolean interrupted = false; try { while (true) { try { - return waiter.poll(timeoutMillis, TimeUnit.MILLISECONDS); + return waiter.get(timeoutMillis, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { interrupted = true; + } catch (TimeoutException e) { + return null; + } catch (ExecutionException e) { + throw SpannerExceptionFactory.newSpannerException(e.getCause()); } } } finally { @@ -1118,6 +1702,7 @@ private static enum Position { private final ScheduledExecutorService executor; private final ExecutorFactory executorFactory; private final ScheduledExecutorService prepareExecutor; + private final int prepareThreadPoolSize; final PoolMaintainer poolMaintainer; private final Clock clock; @@ -1146,10 +1731,10 @@ private static enum Position { private final LinkedList writePreparedSessions = new LinkedList<>(); @GuardedBy("lock") - private final Queue readWaiters = new LinkedList<>(); + private final Queue readWaiters = new LinkedList<>(); @GuardedBy("lock") - private final Queue readWriteWaiters = new LinkedList<>(); + private final Queue readWriteWaiters = new LinkedList<>(); @GuardedBy("lock") private int numSessionsBeingPrepared = 0; @@ -1183,6 +1768,9 @@ private static enum Position { @GuardedBy("lock") private final Set allSessions = new HashSet<>(); + @GuardedBy("lock") + private final Set checkedOutSessions = new HashSet<>(); + private final SessionConsumer sessionConsumer = new SessionConsumerImpl(); @VisibleForTesting Function idleSessionRemovedListener; @@ -1275,6 +1863,12 @@ private SessionPool( } @VisibleForTesting + int getNumberOfSessionsInUse() { + synchronized (lock) { + return numSessionsInUse; + } + } + long getNumberOfSessionsInProcessPrepared() { synchronized (lock) { return numSessionsInProcessPrepared; @@ -1297,9 +1891,9 @@ void removeFromPool(PooledSession session) { session.markClosing(); allSessions.remove(session); numIdleSessionsRemoved++; - if (idleSessionRemovedListener != null) { - idleSessionRemovedListener.apply(session); - } + } + if (idleSessionRemovedListener != null) { + idleSessionRemovedListener.apply(session); } } @@ -1323,6 +1917,13 @@ int getNumberOfSessionsInPool() { } } + @VisibleForTesting + int getNumberOfWriteSessionsInPool() { + synchronized (lock) { + return writePreparedSessions.size() + numSessionsBeingPrepared; + } + } + @VisibleForTesting int getNumberOfSessionsBeingCreated() { synchronized (lock) { @@ -1430,10 +2031,10 @@ boolean isValid() { * session being returned to the pool or a new session being created. * */ - PooledSession getReadSession() throws SpannerException { + PooledSessionFuture getReadSession() throws SpannerException { Span span = Tracing.getTracer().getCurrentSpan(); span.addAnnotation("Acquiring session"); - Waiter waiter = null; + WaiterFuture waiter = null; PooledSession sess = null; synchronized (lock) { if (closureFuture != null) { @@ -1455,7 +2056,7 @@ PooledSession getReadSession() throws SpannerException { if (sess == null) { span.addAnnotation("No session available"); maybeCreateSession(); - waiter = new Waiter(); + waiter = new WaiterFuture(); readWaiters.add(waiter); } else { span.addAnnotation("Acquired read write session"); @@ -1463,18 +2064,8 @@ PooledSession getReadSession() throws SpannerException { } else { span.addAnnotation("Acquired read only session"); } + return checkoutSession(span, sess, waiter, false, false); } - if (waiter != null) { - logger.log( - Level.FINE, - "No session available in the pool. Blocking for one to become available/created"); - span.addAnnotation("Waiting for read only session to be available"); - sess = waiter.take(); - } - sess.markBusy(); - incrementNumSessionsInUse(); - span.addAnnotation(sessionAnnotation(sess)); - return sess; } /** @@ -1495,129 +2086,123 @@ PooledSession getReadSession() throws SpannerException { * to the pool which is then write prepared. * */ - PooledSession getReadWriteSession() { + PooledSessionFuture getReadWriteSession() { Span span = Tracing.getTracer().getCurrentSpan(); span.addAnnotation("Acquiring read write session"); PooledSession sess = null; - // Loop to retry SessionNotFoundExceptions that might occur during in-process prepare of a - // session. - while (true) { - Waiter waiter = null; - boolean inProcessPrepare = stopAutomaticPrepare; - synchronized (lock) { - if (closureFuture != null) { - span.addAnnotation("Pool has been closed"); - throw new IllegalStateException("Pool has been closed", closedException); - } - if (resourceNotFoundException != null) { - span.addAnnotation("Database has been deleted"); - throw SpannerExceptionFactory.newSpannerException( - ErrorCode.NOT_FOUND, - String.format( - "The session pool has been invalidated because a previous RPC returned 'Database not found': %s", - resourceNotFoundException.getMessage()), - resourceNotFoundException); - } - sess = writePreparedSessions.poll(); - if (sess == null) { - if (!inProcessPrepare && numSessionsBeingPrepared <= prepareThreadPoolSize) { - if (numSessionsBeingPrepared <= readWriteWaiters.size()) { - PooledSession readSession = readSessions.poll(); - if (readSession != null) { - span.addAnnotation( - "Acquired read only session. Preparing for read write transaction"); - prepareSession(readSession); - } else { - span.addAnnotation("No session available"); - maybeCreateSession(); - } - } - } else { - inProcessPrepare = true; - numSessionsInProcessPrepared++; + WaiterFuture waiter = null; + boolean inProcessPrepare = stopAutomaticPrepare; + synchronized (lock) { + if (closureFuture != null) { + span.addAnnotation("Pool has been closed"); + throw new IllegalStateException("Pool has been closed", closedException); + } + if (resourceNotFoundException != null) { + span.addAnnotation("Database has been deleted"); + throw SpannerExceptionFactory.newSpannerException( + ErrorCode.NOT_FOUND, + String.format( + "The session pool has been invalidated because a previous RPC returned 'Database not found': %s", + resourceNotFoundException.getMessage()), + resourceNotFoundException); + } + sess = writePreparedSessions.poll(); + if (sess == null) { + if (!inProcessPrepare && numSessionsBeingPrepared <= prepareThreadPoolSize) { + if (numSessionsBeingPrepared <= readWriteWaiters.size()) { PooledSession readSession = readSessions.poll(); if (readSession != null) { - // Create a read/write transaction in-process if there is already a queue for prepared - // sessions. This is more efficient than doing it asynchronously, as it scales with - // the number of user threads. The thread pool for asynchronously preparing sessions - // is fixed. span.addAnnotation( - "Acquired read only session. Preparing in-process for read write transaction"); - sess = readSession; + "Acquired read only session. Preparing for read write transaction"); + prepareSession(readSession); } else { span.addAnnotation("No session available"); maybeCreateSession(); } } - if (sess == null) { - waiter = new Waiter(); - if (inProcessPrepare) { - // inProcessPrepare=true means that we have already determined that the queue for - // preparing read/write sessions is larger than the number of threads in the prepare - // thread pool, and that it's more efficient to do the prepare in-process. We will - // therefore create a waiter for a read-only session, even though a read/write session - // has been requested. - readWaiters.add(waiter); - } else { - readWriteWaiters.add(waiter); - } - } } else { - span.addAnnotation("Acquired read write session"); - } - } - if (waiter != null) { - logger.log( - Level.FINE, - "No session available in the pool. Blocking for one to become available/created"); - span.addAnnotation("Waiting for read write session to be available"); - sess = waiter.take(); - } - if (inProcessPrepare) { - try { - sess.prepareReadWriteTransaction(); - // Session prepare succeeded, restart automatic prepare if it had been stopped. - synchronized (lock) { - stopAutomaticPrepare = false; - } - } catch (Throwable t) { - SpannerException e = newSpannerException(t); - if (!isClosed()) { - handlePrepareSessionFailure(e, sess, false); + inProcessPrepare = true; + numSessionsInProcessPrepared++; + PooledSession readSession = readSessions.poll(); + if (readSession != null) { + // Create a read/write transaction in-process if there is already a queue for prepared + // sessions. This is more efficient than doing it asynchronously, as it scales with + // the number of user threads. The thread pool for asynchronously preparing sessions + // is fixed. + span.addAnnotation( + "Acquired read only session. Preparing in-process for read write transaction"); + sess = readSession; + } else { + span.addAnnotation("No session available"); + maybeCreateSession(); } - sess = null; - if (!isSessionNotFound(e)) { - throw e; + } + if (sess == null) { + waiter = new WaiterFuture(); + if (inProcessPrepare) { + // inProcessPrepare=true means that we have already determined that the queue for + // preparing read/write sessions is larger than the number of threads in the prepare + // thread pool, and that it's more efficient to do the prepare in-process. We will + // therefore create a waiter for a read-only session, even though a read/write session + // has been requested. + readWaiters.add(waiter); + } else { + readWriteWaiters.add(waiter); } } + } else { + span.addAnnotation("Acquired read write session"); } - if (sess != null) { - break; - } + return checkoutSession(span, sess, waiter, true, inProcessPrepare); } - sess.markBusy(); - incrementNumSessionsInUse(); - span.addAnnotation(sessionAnnotation(sess)); - return sess; } - PooledSession replaceReadSession(SessionNotFoundException e, PooledSession session) { + private PooledSessionFuture checkoutSession( + final Span span, + final PooledSession readySession, + WaiterFuture waiter, + boolean write, + final boolean inProcessPrepare) { + ListenableFuture sessionFuture; + if (waiter != null) { + logger.log( + Level.FINE, + "No session available in the pool. Blocking for one to become available/created"); + span.addAnnotation( + String.format( + "Waiting for %s session to be available", write ? "read write" : "read only")); + sessionFuture = waiter; + } else { + SettableFuture fut = SettableFuture.create(); + fut.set(readySession); + sessionFuture = fut; + } + ForwardingListenablePooledSessionFuture forwardingFuture = + new ForwardingListenablePooledSessionFuture(sessionFuture, inProcessPrepare, span); + PooledSessionFuture res = createPooledSessionFuture(forwardingFuture, span); + res.markCheckedOut(); + return res; + } + + PooledSessionFuture replaceReadSession(SessionNotFoundException e, PooledSessionFuture session) { return replaceSession(e, session, false); } - PooledSession replaceReadWriteSession(SessionNotFoundException e, PooledSession session) { + PooledSessionFuture replaceReadWriteSession( + SessionNotFoundException e, PooledSessionFuture session) { return replaceSession(e, session, true); } - private PooledSession replaceSession( - SessionNotFoundException e, PooledSession session, boolean write) { - if (!options.isFailIfSessionNotFound() && session.allowReplacing) { + private PooledSessionFuture replaceSession( + SessionNotFoundException e, PooledSessionFuture session, boolean write) { + if (!options.isFailIfSessionNotFound() && session.get().allowReplacing) { synchronized (lock) { numSessionsInUse--; numSessionsReleased++; + checkedOutSessions.remove(session); } session.leakedException = null; - invalidateSession(session); + invalidateSession(session.get()); return write ? getReadWriteSession() : getReadSession(); } else { throw e; @@ -1780,7 +2365,7 @@ ListenableFuture closeAsync(ClosedException closedException) { } this.closedException = closedException; // Fail all pending waiters. - Waiter waiter = readWaiters.poll(); + WaiterFuture waiter = readWaiters.poll(); while (waiter != null) { waiter.put(newSpannerException(ErrorCode.INTERNAL, "Client has been closed")); waiter = readWaiters.poll(); @@ -1814,10 +2399,16 @@ public void run() { } } }); - for (final PooledSession session : ImmutableList.copyOf(allSessions)) { + for (PooledSessionFuture session : checkedOutSessions) { if (session.leakedException != null) { - logger.log(Level.WARNING, "Leaked session", session.leakedException); + if (options.isFailOnSessionLeak()) { + throw session.leakedException; + } else { + logger.log(Level.WARNING, "Leaked session", session.leakedException); + } } + } + for (final PooledSession session : ImmutableList.copyOf(allSessions)) { if (session.state != SessionState.CLOSING) { closeSessionAsync(session); } @@ -1887,7 +2478,7 @@ public void run() { } } }, - executor); + MoreExecutors.directExecutor()); return res; } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPoolAsyncTransactionManager.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPoolAsyncTransactionManager.java new file mode 100644 index 0000000000..55b6102a27 --- /dev/null +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPoolAsyncTransactionManager.java @@ -0,0 +1,216 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.spanner; + +import com.google.api.core.ApiAsyncFunction; +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutureCallback; +import com.google.api.core.ApiFutures; +import com.google.api.core.SettableApiFuture; +import com.google.cloud.Timestamp; +import com.google.cloud.spanner.AsyncTransactionManager.TransactionContextFuture; +import com.google.cloud.spanner.SessionPool.PooledSessionFuture; +import com.google.cloud.spanner.TransactionContextFutureImpl.CommittableAsyncTransactionManager; +import com.google.cloud.spanner.TransactionManager.TransactionState; +import com.google.common.base.Preconditions; +import com.google.common.util.concurrent.MoreExecutors; +import javax.annotation.concurrent.GuardedBy; + +class SessionPoolAsyncTransactionManager implements CommittableAsyncTransactionManager { + private final Object lock = new Object(); + + @GuardedBy("lock") + private TransactionState txnState; + + private volatile PooledSessionFuture session; + private final SettableApiFuture delegate = + SettableApiFuture.create(); + + SessionPoolAsyncTransactionManager(PooledSessionFuture session) { + this.session = session; + this.session.addListener( + new Runnable() { + @Override + public void run() { + try { + delegate.set( + SessionPoolAsyncTransactionManager.this.session.get().transactionManagerAsync()); + } catch (Throwable t) { + delegate.setException(t); + } + } + }, + MoreExecutors.directExecutor()); + } + + @Override + public void close() { + delegate.addListener( + new Runnable() { + @Override + public void run() { + session.close(); + } + }, + MoreExecutors.directExecutor()); + } + + @Override + public TransactionContextFuture beginAsync() { + synchronized (lock) { + Preconditions.checkState(txnState == null, "begin can only be called once"); + txnState = TransactionState.STARTED; + } + final SettableApiFuture delegateTxnFuture = SettableApiFuture.create(); + ApiFutures.addCallback( + delegate, + new ApiFutureCallback() { + @Override + public void onFailure(Throwable t) { + delegateTxnFuture.setException(t); + } + + @Override + public void onSuccess(AsyncTransactionManagerImpl result) { + ApiFutures.addCallback( + result.beginAsync(), + new ApiFutureCallback() { + @Override + public void onFailure(Throwable t) { + delegateTxnFuture.setException(t); + } + + @Override + public void onSuccess(TransactionContext result) { + delegateTxnFuture.set(result); + } + }, + MoreExecutors.directExecutor()); + } + }, + MoreExecutors.directExecutor()); + return new TransactionContextFutureImpl(this, delegateTxnFuture); + } + + @Override + public void onError(Throwable t) { + if (t instanceof AbortedException) { + synchronized (lock) { + txnState = TransactionState.ABORTED; + } + } + } + + @Override + public ApiFuture commitAsync() { + synchronized (lock) { + Preconditions.checkState( + txnState == TransactionState.STARTED, + "commit can only be invoked if the transaction is in progress. Current state: " + + txnState); + txnState = TransactionState.COMMITTED; + } + return ApiFutures.transformAsync( + delegate, + new ApiAsyncFunction() { + @Override + public ApiFuture apply(AsyncTransactionManagerImpl input) throws Exception { + final SettableApiFuture res = SettableApiFuture.create(); + ApiFutures.addCallback( + input.commitAsync(), + new ApiFutureCallback() { + @Override + public void onFailure(Throwable t) { + synchronized (lock) { + if (t instanceof AbortedException) { + txnState = TransactionState.ABORTED; + } else { + txnState = TransactionState.COMMIT_FAILED; + } + } + res.setException(t); + } + + @Override + public void onSuccess(Timestamp result) { + res.set(result); + } + }, + MoreExecutors.directExecutor()); + return res; + } + }, + MoreExecutors.directExecutor()); + } + + @Override + public ApiFuture rollbackAsync() { + synchronized (lock) { + Preconditions.checkState( + txnState == TransactionState.STARTED, + "rollback can only be called if the transaction is in progress"); + txnState = TransactionState.ROLLED_BACK; + } + return ApiFutures.transformAsync( + delegate, + new ApiAsyncFunction() { + @Override + public ApiFuture apply(AsyncTransactionManagerImpl input) throws Exception { + ApiFuture res = input.rollbackAsync(); + res.addListener( + new Runnable() { + @Override + public void run() { + session.close(); + } + }, + MoreExecutors.directExecutor()); + return res; + } + }, + MoreExecutors.directExecutor()); + } + + @Override + public TransactionContextFuture resetForRetryAsync() { + synchronized (lock) { + Preconditions.checkState( + txnState == TransactionState.ABORTED, + "resetForRetry can only be called after the transaction aborted."); + txnState = TransactionState.STARTED; + } + return new TransactionContextFutureImpl( + this, + ApiFutures.transformAsync( + delegate, + new ApiAsyncFunction() { + @Override + public ApiFuture apply(AsyncTransactionManagerImpl input) + throws Exception { + return input.resetForRetryAsync(); + } + }, + MoreExecutors.directExecutor())); + } + + @Override + public TransactionState getState() { + synchronized (lock) { + return txnState; + } + } +} 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 17295a38ab..57dbd4debd 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 @@ -37,6 +37,7 @@ public class SessionPoolOptions { private final int keepAliveIntervalMinutes; private final Duration removeInactiveSessionAfter; private final ActionOnSessionNotFound actionOnSessionNotFound; + private final ActionOnSessionLeak actionOnSessionLeak; private final long initialWaitForSessionTimeoutMillis; private SessionPoolOptions(Builder builder) { @@ -50,6 +51,7 @@ private SessionPoolOptions(Builder builder) { this.writeSessionsFraction = builder.writeSessionsFraction; this.actionOnExhaustion = builder.actionOnExhaustion; this.actionOnSessionNotFound = builder.actionOnSessionNotFound; + this.actionOnSessionLeak = builder.actionOnSessionLeak; this.initialWaitForSessionTimeoutMillis = builder.initialWaitForSessionTimeoutMillis; this.loopFrequency = builder.loopFrequency; this.keepAliveIntervalMinutes = builder.keepAliveIntervalMinutes; @@ -106,6 +108,11 @@ boolean isFailIfSessionNotFound() { return actionOnSessionNotFound == ActionOnSessionNotFound.FAIL; } + @VisibleForTesting + boolean isFailOnSessionLeak() { + return actionOnSessionLeak == ActionOnSessionLeak.FAIL; + } + public static Builder newBuilder() { return new Builder(); } @@ -120,6 +127,11 @@ private static enum ActionOnSessionNotFound { FAIL; } + private static enum ActionOnSessionLeak { + WARN, + FAIL; + } + /** Builder for creating SessionPoolOptions. */ public static class Builder { private boolean minSessionsSet = false; @@ -131,6 +143,7 @@ public static class Builder { private ActionOnExhaustion actionOnExhaustion = DEFAULT_ACTION; private long initialWaitForSessionTimeoutMillis = 30_000L; private ActionOnSessionNotFound actionOnSessionNotFound = ActionOnSessionNotFound.RETRY; + private ActionOnSessionLeak actionOnSessionLeak = ActionOnSessionLeak.WARN; private long loopFrequency = 10 * 1000L; private int keepAliveIntervalMinutes = 30; private Duration removeInactiveSessionAfter = Duration.ofMinutes(55L); @@ -240,6 +253,12 @@ Builder setFailIfSessionNotFound() { return this; } + @VisibleForTesting + Builder setFailOnSessionLeak() { + this.actionOnSessionLeak = ActionOnSessionLeak.FAIL; + return this; + } + /** * Fraction of sessions to be kept prepared for write transactions. This is an optimisation to * avoid the cost of sending a BeginTransaction() rpc. If all such sessions are in use and a diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Spanner.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Spanner.java index 0c6bec4ea8..52c35cb713 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Spanner.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Spanner.java @@ -16,6 +16,7 @@ package com.google.cloud.spanner; +import com.google.api.gax.core.ExecutorProvider; import com.google.cloud.Service; /** @@ -108,4 +109,7 @@ public interface Spanner extends Service, AutoCloseable { /** @return true if this {@link Spanner} object is closed. */ boolean isClosed(); + + /** @return the {@link ExecutorProvider} that is used for asynchronous queries and operations. */ + ExecutorProvider getAsyncExecutorProvider(); } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerExceptionFactory.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerExceptionFactory.java index e9ca06e233..a86d5463c9 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerExceptionFactory.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerExceptionFactory.java @@ -18,6 +18,7 @@ import com.google.api.gax.grpc.GrpcStatusCode; import com.google.api.gax.rpc.ApiException; +import com.google.api.gax.rpc.WatchdogTimeoutException; import com.google.cloud.spanner.SpannerException.DoNotConstructDirectly; import com.google.common.base.MoreObjects; import com.google.common.base.Predicate; @@ -212,7 +213,14 @@ private static SpannerException newSpannerExceptionPreformatted( } private static SpannerException fromApiException(ApiException exception) { - Status.Code code = ((GrpcStatusCode) exception.getStatusCode()).getTransportCode(); + Status.Code code; + if (exception.getStatusCode() instanceof GrpcStatusCode) { + code = ((GrpcStatusCode) exception.getStatusCode()).getTransportCode(); + } else if (exception instanceof WatchdogTimeoutException) { + code = Status.Code.DEADLINE_EXCEEDED; + } else { + code = Status.Code.UNKNOWN; + } ErrorCode errorCode = ErrorCode.fromGrpcStatus(Status.fromCode(code)); if (exception.getCause() != null) { return SpannerExceptionFactory.newSpannerException( 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 4e937459cf..2d034eda88 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java @@ -16,6 +16,7 @@ package com.google.cloud.spanner; +import com.google.api.gax.core.ExecutorProvider; import com.google.api.gax.core.GaxProperties; import com.google.api.gax.paging.Page; import com.google.cloud.BaseService; @@ -23,9 +24,11 @@ import com.google.cloud.PageImpl.NextPageFetcher; import com.google.cloud.grpc.GrpcTransportOptions; import com.google.cloud.spanner.SessionClient.SessionId; +import com.google.cloud.spanner.SpannerOptions.CloseableExecutorProvider; import com.google.cloud.spanner.spi.v1.SpannerRpc; import com.google.cloud.spanner.spi.v1.SpannerRpc.Paginated; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.MoreObjects; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; @@ -86,6 +89,8 @@ private static String nextDatabaseClientId(DatabaseId databaseId) { @GuardedBy("this") private final Map dbClients = new HashMap<>(); + private final CloseableExecutorProvider asyncExecutorProvider; + @GuardedBy("this") private final List invalidatedDbClients = new ArrayList<>(); @@ -116,6 +121,10 @@ static final class ClosedException extends RuntimeException { SpannerImpl(SpannerRpc gapicRpc, SpannerOptions options) { super(options); this.gapicRpc = gapicRpc; + this.asyncExecutorProvider = + MoreObjects.firstNonNull( + options.getAsyncExecutorProvider(), + SpannerOptions.createDefaultAsyncExecutorProvider()); this.dbAdminClient = new DatabaseAdminClientImpl(options.getProjectId(), gapicRpc); this.instanceClient = new InstanceAdminClientImpl(options.getProjectId(), gapicRpc, dbAdminClient); @@ -140,6 +149,13 @@ QueryOptions getDefaultQueryOptions(DatabaseId databaseId) { return getOptions().getDefaultQueryOptions(databaseId); } + /** + * Returns the {@link ExecutorProvider} to use for async methods that need a background executor. + */ + public ExecutorProvider getAsyncExecutorProvider() { + return asyncExecutorProvider; + } + SessionImpl sessionWithId(String name) { Preconditions.checkArgument(!Strings.isNullOrEmpty(name), "name is null or empty"); SessionId id = SessionId.of(name); @@ -251,6 +267,7 @@ void close(long timeout, TimeUnit unit) { sessionClient.close(); } sessionClients.clear(); + asyncExecutorProvider.close(); try { gapicRpc.shutdown(); } catch (RuntimeException e) { 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 7e895524dc..35a288530f 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 @@ -17,6 +17,7 @@ package com.google.cloud.spanner; import com.google.api.core.ApiFunction; +import com.google.api.gax.core.ExecutorProvider; import com.google.api.gax.grpc.GrpcInterceptorProvider; import com.google.api.gax.longrunning.OperationSnapshot; import com.google.api.gax.longrunning.OperationTimedPollAlgorithm; @@ -40,15 +41,19 @@ import com.google.cloud.spanner.spi.v1.SpannerRpc; import com.google.cloud.spanner.v1.SpannerSettings; import com.google.cloud.spanner.v1.stub.SpannerStubSettings; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.MoreObjects; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.google.spanner.admin.database.v1.CreateBackupRequest; import com.google.spanner.admin.database.v1.CreateDatabaseRequest; import com.google.spanner.admin.database.v1.RestoreDatabaseRequest; import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions; import io.grpc.CallCredentials; +import io.grpc.CompressorRegistry; +import io.grpc.ExperimentalApi; import io.grpc.ManagedChannelBuilder; import java.io.IOException; import java.net.MalformedURLException; @@ -57,7 +62,13 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import org.threeten.bp.Duration; /** Options for the Cloud Spanner service. */ @@ -104,6 +115,8 @@ public class SpannerOptions extends ServiceOptions { private final Map mergedQueryOptions; private final CallCredentialsProvider callCredentialsProvider; + private final CloseableExecutorProvider asyncExecutorProvider; + private final String compressorName; /** * Interface that can be used to provide {@link CallCredentials} instead of {@link Credentials} to @@ -134,6 +147,67 @@ public ServiceRpc create(SpannerOptions options) { } } + private static final AtomicInteger DEFAULT_POOL_COUNT = new AtomicInteger(); + + /** {@link ExecutorProvider} that is used for {@link AsyncResultSet}. */ + interface CloseableExecutorProvider extends ExecutorProvider, AutoCloseable { + /** Overridden to suppress the throws declaration of the super interface. */ + @Override + public void close(); + } + + static class FixedCloseableExecutorProvider implements CloseableExecutorProvider { + private final ScheduledExecutorService executor; + + private FixedCloseableExecutorProvider(ScheduledExecutorService executor) { + this.executor = Preconditions.checkNotNull(executor); + } + + @Override + public void close() { + executor.shutdown(); + } + + @Override + public ScheduledExecutorService getExecutor() { + return executor; + } + + @Override + public boolean shouldAutoClose() { + return false; + } + + /** Creates a FixedCloseableExecutorProvider. */ + static FixedCloseableExecutorProvider create(ScheduledExecutorService executor) { + return new FixedCloseableExecutorProvider(executor); + } + } + + /** + * Default {@link ExecutorProvider} for high-level async calls that need an executor. The default + * uses a cached thread pool containing a max of 8 threads. The pool is lazily initialized and + * will not create any threads if the user application does not use any async methods. It will + * also scale down the thread usage if the async load allows for that. + */ + @VisibleForTesting + static CloseableExecutorProvider createDefaultAsyncExecutorProvider() { + return createAsyncExecutorProvider(8, 60L, TimeUnit.SECONDS); + } + + @VisibleForTesting + static CloseableExecutorProvider createAsyncExecutorProvider( + int poolSize, long keepAliveTime, TimeUnit unit) { + String format = + String.format("spanner-async-pool-%d-thread-%%d", DEFAULT_POOL_COUNT.incrementAndGet()); + ThreadFactory threadFactory = + new ThreadFactoryBuilder().setDaemon(true).setNameFormat(format).build(); + ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(poolSize, threadFactory); + executor.setKeepAliveTime(keepAliveTime, unit); + executor.allowCoreThreadTimeOut(true); + return FixedCloseableExecutorProvider.create(executor); + } + private SpannerOptions(Builder builder) { super(SpannerFactory.class, SpannerRpcFactory.class, builder, new SpannerDefaults()); numChannels = builder.numChannels; @@ -174,6 +248,8 @@ private SpannerOptions(Builder builder) { this.mergedQueryOptions = ImmutableMap.copyOf(merged); } callCredentialsProvider = builder.callCredentialsProvider; + asyncExecutorProvider = builder.asyncExecutorProvider; + compressorName = builder.compressorName; } /** @@ -238,6 +314,8 @@ public static class Builder private boolean autoThrottleAdministrativeRequests = false; private Map defaultQueryOptions = new HashMap<>(); private CallCredentialsProvider callCredentialsProvider; + private CloseableExecutorProvider asyncExecutorProvider; + private String compressorName; private String emulatorHost = System.getenv("SPANNER_EMULATOR_HOST"); private Builder() { @@ -298,6 +376,11 @@ private Builder() { Builder(SpannerOptions options) { super(options); + if (options.getHost() != null + && this.emulatorHost != null + && !options.getHost().equals(this.emulatorHost)) { + this.emulatorHost = null; + } this.numChannels = options.numChannels; this.sessionPoolOptions = options.sessionPoolOptions; this.prefetchChunks = options.prefetchChunks; @@ -309,6 +392,8 @@ private Builder() { this.autoThrottleAdministrativeRequests = options.autoThrottleAdministrativeRequests; this.defaultQueryOptions = options.defaultQueryOptions; this.callCredentialsProvider = options.callCredentialsProvider; + this.asyncExecutorProvider = options.asyncExecutorProvider; + this.compressorName = options.compressorName; this.channelProvider = options.channelProvider; this.channelConfigurator = options.channelConfigurator; this.interceptorProvider = options.interceptorProvider; @@ -558,6 +643,28 @@ public Builder setCallCredentialsProvider(CallCredentialsProvider callCredential return this; } + /** + * Sets the compression to use for all gRPC calls. The compressor must be a valid name known in + * the {@link CompressorRegistry}. + * + *

Supported values are: + * + *

    + *
  • gzip: Enable gzip compression + *
  • identity: Disable compression + *
  • null: Use default compression + *
+ */ + @ExperimentalApi("https://github.com/grpc/grpc-java/issues/1704") + public Builder setCompressorName(@Nullable String compressorName) { + Preconditions.checkArgument( + compressorName == null + || CompressorRegistry.getDefaultInstance().lookupCompressor(compressorName) != null, + String.format("%s is not a known compressor", compressorName)); + this.compressorName = compressorName; + return this; + } + /** * Specifying this will allow the client to prefetch up to {@code prefetchChunks} {@code * PartialResultSet} chunks for each read and query. The data size of each chunk depends on the @@ -690,6 +797,10 @@ public CallCredentialsProvider getCallCredentialsProvider() { return callCredentialsProvider; } + public String getCompressorName() { + return compressorName; + } + /** 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 @@ -703,6 +814,10 @@ public QueryOptions getDefaultQueryOptions(DatabaseId databaseId) { return options; } + CloseableExecutorProvider getAsyncExecutorProvider() { + return asyncExecutorProvider; + } + public int getPrefetchChunks() { return prefetchChunks; } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TraceUtil.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TraceUtil.java index c5488ac55d..0d429661ad 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TraceUtil.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TraceUtil.java @@ -40,7 +40,7 @@ static Map getTransactionAnnotations(Transaction t) { AttributeValue.stringAttributeValue(Timestamp.fromProto(t.getReadTimestamp()).toString())); } - static ImmutableMap getExceptionAnnotations(RuntimeException e) { + static ImmutableMap getExceptionAnnotations(Throwable e) { if (e instanceof SpannerException) { return ImmutableMap.of( "Status", diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionContext.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionContext.java index a529c4c492..0b4a92f989 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionContext.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionContext.java @@ -16,6 +16,8 @@ package com.google.cloud.spanner; +import com.google.api.core.ApiFuture; + /** * Context for a single attempt of a locking read-write transaction. This type of transaction is the * only way to write data into Cloud Spanner; {@link Session#write(Iterable)} and {@link @@ -102,6 +104,17 @@ public interface TransactionContext extends ReadContext { */ long executeUpdate(Statement statement); + /** + * Same as {@link #executeUpdate(Statement)}, but is guaranteed to be non-blocking. If multiple + * asynchronous update statements are submitted to the same read/write transaction, the statements + * are guaranteed to be submitted to Cloud Spanner in the order that they were submitted in the + * client. This does however not guarantee that an asynchronous update statement will see the + * results of all previously submitted statements, as the execution of the statements can be + * parallel. If you rely on the results of a previous statement, you should block until the result + * of that statement is known and has been returned to the client. + */ + ApiFuture executeUpdateAsync(Statement statement); + /** * Executes a list of DML statements in a single request. The statements will be executed in order * and the semantics is the same as if each statement is executed by {@code executeUpdate} in a @@ -118,4 +131,15 @@ public interface TransactionContext extends ReadContext { * statement. The 3rd statement will not run. */ long[] batchUpdate(Iterable statements); + + /** + * Same as {@link #batchUpdate(Iterable)}, but is guaranteed to be non-blocking. If multiple + * asynchronous update statements are submitted to the same read/write transaction, the statements + * are guaranteed to be submitted to Cloud Spanner in the order that they were submitted in the + * client. This does however not guarantee that an asynchronous update statement will see the + * results of all previously submitted statements, as the execution of the statements can be + * parallel. If you rely on the results of a previous statement, you should block until the result + * of that statement is known and has been returned to the client. + */ + ApiFuture batchUpdateAsync(Iterable statements); } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionContextFutureImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionContextFutureImpl.java new file mode 100644 index 0000000000..bc8262a535 --- /dev/null +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionContextFutureImpl.java @@ -0,0 +1,258 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.spanner; + +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutureCallback; +import com.google.api.core.ApiFutures; +import com.google.api.core.ForwardingApiFuture; +import com.google.api.core.InternalApi; +import com.google.api.core.SettableApiFuture; +import com.google.cloud.Timestamp; +import com.google.cloud.spanner.AsyncTransactionManager.AsyncTransactionFunction; +import com.google.cloud.spanner.AsyncTransactionManager.AsyncTransactionStep; +import com.google.cloud.spanner.AsyncTransactionManager.CommitTimestampFuture; +import com.google.cloud.spanner.AsyncTransactionManager.TransactionContextFuture; +import com.google.common.base.Preconditions; +import com.google.common.util.concurrent.MoreExecutors; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +class TransactionContextFutureImpl extends ForwardingApiFuture + implements TransactionContextFuture { + + @InternalApi + interface CommittableAsyncTransactionManager extends AsyncTransactionManager { + void onError(Throwable t); + + ApiFuture commitAsync(); + } + /** + * {@link ApiFuture} that returns a commit timestamp. Any {@link AbortedException} that is thrown + * by either the commit call or any other rpc during the transaction will be thrown by the {@link + * #get()} method of this future as an {@link AbortedException} and not as an {@link + * ExecutionException} with an {@link AbortedException} as its cause. + */ + static class CommitTimestampFutureImpl extends ForwardingApiFuture + implements CommitTimestampFuture { + CommitTimestampFutureImpl(ApiFuture delegate) { + super(Preconditions.checkNotNull(delegate)); + } + + @Override + public Timestamp get() throws AbortedException, ExecutionException, InterruptedException { + try { + return super.get(); + } catch (ExecutionException e) { + if (e.getCause() != null && e.getCause() instanceof AbortedException) { + throw (AbortedException) e.getCause(); + } + throw e; + } + } + + @Override + public Timestamp get(long timeout, TimeUnit unit) + throws AbortedException, ExecutionException, InterruptedException, TimeoutException { + try { + return super.get(timeout, unit); + } catch (ExecutionException e) { + if (e.getCause() != null && e.getCause() instanceof AbortedException) { + throw (AbortedException) e.getCause(); + } + throw e; + } + } + } + + class AsyncTransactionStatementImpl extends ForwardingApiFuture + implements AsyncTransactionStep { + final ApiFuture txnFuture; + final SettableApiFuture statementResult; + + AsyncTransactionStatementImpl( + final ApiFuture txnFuture, + ApiFuture input, + final AsyncTransactionFunction function, + Executor executor) { + this(SettableApiFuture.create(), txnFuture, input, function, executor); + } + + AsyncTransactionStatementImpl( + SettableApiFuture delegate, + final ApiFuture txnFuture, + ApiFuture input, + final AsyncTransactionFunction function, + final Executor executor) { + super(delegate); + this.statementResult = delegate; + this.txnFuture = txnFuture; + ApiFutures.addCallback( + input, + new ApiFutureCallback() { + @Override + public void onFailure(Throwable t) { + mgr.onError(t); + txnResult.setException(t); + } + + @Override + public void onSuccess(I result) { + try { + ApiFutures.addCallback( + runAsyncTransactionFunction(function, txnFuture.get(), result, executor), + new ApiFutureCallback() { + @Override + public void onFailure(Throwable t) { + mgr.onError(t); + txnResult.setException(t); + } + + @Override + public void onSuccess(O result) { + statementResult.set(result); + } + }, + MoreExecutors.directExecutor()); + } catch (Throwable t) { + mgr.onError(t); + txnResult.setException(t); + } + } + }, + MoreExecutors.directExecutor()); + } + + @Override + public AsyncTransactionStatementImpl then( + AsyncTransactionFunction next, Executor executor) { + return new AsyncTransactionStatementImpl<>(txnFuture, statementResult, next, executor); + } + + @Override + public CommitTimestampFuture commitAsync() { + ApiFutures.addCallback( + statementResult, + new ApiFutureCallback() { + @Override + public void onFailure(Throwable t) { + mgr.onError(t); + txnResult.setException(t); + } + + @Override + public void onSuccess(O result) { + ApiFutures.addCallback( + mgr.commitAsync(), + new ApiFutureCallback() { + @Override + public void onFailure(Throwable t) { + mgr.onError(t); + txnResult.setException(t); + } + + @Override + public void onSuccess(Timestamp result) { + txnResult.set(result); + } + }, + MoreExecutors.directExecutor()); + } + }, + MoreExecutors.directExecutor()); + return new CommitTimestampFutureImpl(txnResult); + } + } + + static ApiFuture runAsyncTransactionFunction( + final AsyncTransactionFunction function, + final TransactionContext txn, + final I input, + Executor executor) + throws Exception { + // Shortcut for common path. + if (executor == MoreExecutors.directExecutor()) { + return Preconditions.checkNotNull( + function.apply(txn, input), + "AsyncTransactionFunction returned . Did you mean to return ApiFutures.immediateFuture(null)?"); + } else { + final SettableApiFuture res = SettableApiFuture.create(); + executor.execute( + new Runnable() { + @Override + public void run() { + try { + ApiFuture functionResult = + Preconditions.checkNotNull( + function.apply(txn, input), + "AsyncTransactionFunction returned . Did you mean to return ApiFutures.immediateFuture(null)?"); + ApiFutures.addCallback( + functionResult, + new ApiFutureCallback() { + @Override + public void onFailure(Throwable t) { + res.setException(t); + } + + @Override + public void onSuccess(O result) { + res.set(result); + } + }, + MoreExecutors.directExecutor()); + } catch (Throwable t) { + res.setException(t); + } + } + }); + return res; + } + } + + final CommittableAsyncTransactionManager mgr; + final SettableApiFuture txnResult = SettableApiFuture.create(); + + TransactionContextFutureImpl( + CommittableAsyncTransactionManager mgr, ApiFuture txnFuture) { + super(txnFuture); + this.mgr = mgr; + } + + @Override + public AsyncTransactionStatementImpl then( + AsyncTransactionFunction function, Executor executor) { + final SettableApiFuture input = SettableApiFuture.create(); + ApiFutures.addCallback( + this, + new ApiFutureCallback() { + @Override + public void onFailure(Throwable t) { + mgr.onError(t); + input.setException(t); + } + + @Override + public void onSuccess(TransactionContext result) { + input.set(null); + } + }, + MoreExecutors.directExecutor()); + return new AsyncTransactionStatementImpl<>(this, input, function, executor); + } +} diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionManagerImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionManagerImpl.java index bdf7ec954f..8dbab88314 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionManagerImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionManagerImpl.java @@ -29,14 +29,23 @@ final class TransactionManagerImpl implements TransactionManager, SessionTransac private static final Tracer tracer = Tracing.getTracer(); private final SessionImpl session; - private final Span span; + private Span span; private TransactionRunnerImpl.TransactionContextImpl txn; private TransactionState txnState; - TransactionManagerImpl(SessionImpl session) { + TransactionManagerImpl(SessionImpl session, Span span) { this.session = session; - this.span = Tracing.getTracer().getCurrentSpan(); + this.span = span; + } + + Span getSpan() { + return span; + } + + @Override + public void setSpan(Span span) { + this.span = span; } @Override diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java index cfa8b73c4a..21812aa96a 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java @@ -21,18 +21,29 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; +import com.google.api.core.ApiAsyncFunction; +import com.google.api.core.ApiFunction; +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutures; +import com.google.api.core.SettableApiFuture; import com.google.cloud.Timestamp; +import com.google.cloud.spanner.Options.QueryOption; +import com.google.cloud.spanner.Options.ReadOption; import com.google.cloud.spanner.SessionImpl.SessionTransaction; import com.google.cloud.spanner.spi.v1.SpannerRpc; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableMap; +import com.google.common.util.concurrent.MoreExecutors; import com.google.protobuf.ByteString; +import com.google.protobuf.Empty; import com.google.rpc.Code; import com.google.spanner.v1.CommitRequest; import com.google.spanner.v1.CommitResponse; import com.google.spanner.v1.ExecuteBatchDmlRequest; +import com.google.spanner.v1.ExecuteBatchDmlResponse; import com.google.spanner.v1.ExecuteSqlRequest; import com.google.spanner.v1.ExecuteSqlRequest.QueryMode; +import com.google.spanner.v1.ResultSet; import com.google.spanner.v1.RollbackRequest; import com.google.spanner.v1.TransactionSelector; import io.opencensus.common.Scope; @@ -43,6 +54,8 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; @@ -76,6 +89,57 @@ static Builder newBuilder() { return new Builder(); } + /** + * {@link AsyncResultSet} implementation that keeps track of the async operations that are still + * running for this {@link TransactionContext} and that should finish before the {@link + * TransactionContext} can commit and release its session back into the pool. + */ + private class TransactionContextAsyncResultSetImpl extends ForwardingAsyncResultSet + implements ListenableAsyncResultSet { + private TransactionContextAsyncResultSetImpl(ListenableAsyncResultSet delegate) { + super(delegate); + } + + @Override + public ApiFuture setCallback(Executor exec, ReadyCallback cb) { + Runnable listener = + new Runnable() { + @Override + public void run() { + decreaseAsyncOperations(); + } + }; + try { + increaseAsynOperations(); + addListener(listener); + return super.setCallback(exec, cb); + } catch (Throwable t) { + removeListener(listener); + decreaseAsyncOperations(); + throw t; + } + } + + @Override + public void addListener(Runnable listener) { + ((ListenableAsyncResultSet) this.delegate).addListener(listener); + } + + @Override + public void removeListener(Runnable listener) { + ((ListenableAsyncResultSet) this.delegate).removeListener(listener); + } + } + + @GuardedBy("lock") + private volatile boolean committing; + + @GuardedBy("lock") + private volatile SettableApiFuture finishedAsyncOperations = SettableApiFuture.create(); + + @GuardedBy("lock") + private volatile int runningAsyncOperations; + @GuardedBy("lock") private List mutations = new ArrayList<>(); @@ -92,25 +156,70 @@ static Builder newBuilder() { private TransactionContextImpl(Builder builder) { super(builder); this.transactionId = builder.transactionId; + this.finishedAsyncOperations.set(null); + } + + private void increaseAsynOperations() { + synchronized (lock) { + if (runningAsyncOperations == 0) { + finishedAsyncOperations = SettableApiFuture.create(); + } + runningAsyncOperations++; + } + } + + private void decreaseAsyncOperations() { + synchronized (lock) { + runningAsyncOperations--; + if (runningAsyncOperations == 0) { + finishedAsyncOperations.set(null); + } + } } void ensureTxn() { + try { + ensureTxnAsync().get(); + } catch (ExecutionException e) { + throw SpannerExceptionFactory.newSpannerException(e.getCause() == null ? e : e.getCause()); + } catch (InterruptedException e) { + throw SpannerExceptionFactory.propagateInterrupt(e); + } + } + + ApiFuture ensureTxnAsync() { + final SettableApiFuture res = SettableApiFuture.create(); if (transactionId == null || isAborted()) { span.addAnnotation("Creating Transaction"); - try { - transactionId = session.beginTransaction(); - span.addAnnotation( - "Transaction Creation Done", - ImmutableMap.of( - "Id", AttributeValue.stringAttributeValue(transactionId.toStringUtf8()))); - txnLogger.log( - Level.FINER, - "Started transaction {0}", - txnLogger.isLoggable(Level.FINER) ? transactionId.asReadOnlyByteBuffer() : null); - } catch (SpannerException e) { - span.addAnnotation("Transaction Creation Failed", TraceUtil.getExceptionAnnotations(e)); - throw e; - } + final ApiFuture fut = session.beginTransactionAsync(); + fut.addListener( + new Runnable() { + @Override + public void run() { + try { + transactionId = fut.get(); + span.addAnnotation( + "Transaction Creation Done", + ImmutableMap.of( + "Id", AttributeValue.stringAttributeValue(transactionId.toStringUtf8()))); + txnLogger.log( + Level.FINER, + "Started transaction {0}", + txnLogger.isLoggable(Level.FINER) + ? transactionId.asReadOnlyByteBuffer() + : null); + res.set(null); + } catch (ExecutionException e) { + span.addAnnotation( + "Transaction Creation Failed", + TraceUtil.getExceptionAnnotations(e.getCause() == null ? e : e.getCause())); + res.setException(e.getCause() == null ? e : e.getCause()); + } catch (InterruptedException e) { + res.setException(SpannerExceptionFactory.propagateInterrupt(e)); + } + } + }, + MoreExecutors.directExecutor()); } else { span.addAnnotation( "Transaction Initialized", @@ -120,41 +229,102 @@ void ensureTxn() { Level.FINER, "Using prepared transaction {0}", txnLogger.isLoggable(Level.FINER) ? transactionId.asReadOnlyByteBuffer() : null); + res.set(null); } + return res; } void commit() { - span.addAnnotation("Starting Commit"); - CommitRequest.Builder builder = - CommitRequest.newBuilder().setSession(session.getName()).setTransactionId(transactionId); - synchronized (lock) { - if (!mutations.isEmpty()) { - List mutationsProto = new ArrayList<>(); - Mutation.toProto(mutations, mutationsProto); - builder.addAllMutations(mutationsProto); - } - // Ensure that no call to buffer mutations that would be lost can succeed. - mutations = null; + try { + commitTimestamp = commitAsync().get(); + } catch (InterruptedException e) { + throw SpannerExceptionFactory.propagateInterrupt(e); + } catch (ExecutionException e) { + throw SpannerExceptionFactory.newSpannerException(e.getCause() == null ? e : e.getCause()); } - final CommitRequest commitRequest = builder.build(); - Span opSpan = tracer.spanBuilderWithExplicitParent(SpannerImpl.COMMIT, span).startSpan(); - try (Scope s = tracer.withSpan(opSpan)) { - CommitResponse commitResponse = rpc.commit(commitRequest, session.getOptions()); - if (!commitResponse.hasCommitTimestamp()) { - throw newSpannerException( - ErrorCode.INTERNAL, "Missing commitTimestamp:\n" + session.getName()); - } - commitTimestamp = Timestamp.fromProto(commitResponse.getCommitTimestamp()); - opSpan.end(TraceUtil.END_SPAN_OPTIONS); - } catch (RuntimeException e) { - span.addAnnotation("Commit Failed", TraceUtil.getExceptionAnnotations(e)); - TraceUtil.endSpanWithFailure(opSpan, e); - if (e instanceof SpannerException) { - onError((SpannerException) e); - } - throw e; + } + + ApiFuture commitAsync() { + final SettableApiFuture res = SettableApiFuture.create(); + final SettableApiFuture latch; + synchronized (lock) { + latch = finishedAsyncOperations; } - span.addAnnotation("Commit Done"); + latch.addListener( + new Runnable() { + @Override + public void run() { + try { + latch.get(); + CommitRequest.Builder builder = + CommitRequest.newBuilder() + .setSession(session.getName()) + .setTransactionId(transactionId); + synchronized (lock) { + if (!mutations.isEmpty()) { + List mutationsProto = new ArrayList<>(); + Mutation.toProto(mutations, mutationsProto); + builder.addAllMutations(mutationsProto); + } + // Ensure that no call to buffer mutations that would be lost can succeed. + mutations = null; + } + final CommitRequest commitRequest = builder.build(); + span.addAnnotation("Starting Commit"); + final Span opSpan = + tracer.spanBuilderWithExplicitParent(SpannerImpl.COMMIT, span).startSpan(); + final ApiFuture commitFuture = + rpc.commitAsync(commitRequest, session.getOptions()); + commitFuture.addListener( + tracer.withSpan( + opSpan, + new Runnable() { + @Override + public void run() { + try { + CommitResponse commitResponse = commitFuture.get(); + if (!commitResponse.hasCommitTimestamp()) { + throw newSpannerException( + ErrorCode.INTERNAL, + "Missing commitTimestamp:\n" + session.getName()); + } + Timestamp ts = + Timestamp.fromProto(commitResponse.getCommitTimestamp()); + span.addAnnotation("Commit Done"); + opSpan.end(TraceUtil.END_SPAN_OPTIONS); + res.set(ts); + } catch (Throwable e) { + if (e instanceof ExecutionException) { + e = + SpannerExceptionFactory.newSpannerException( + e.getCause() == null ? e : e.getCause()); + } else if (e instanceof InterruptedException) { + e = + SpannerExceptionFactory.propagateInterrupt( + (InterruptedException) e); + } else { + e = SpannerExceptionFactory.newSpannerException(e); + } + span.addAnnotation( + "Commit Failed", TraceUtil.getExceptionAnnotations(e)); + TraceUtil.endSpanWithFailure(opSpan, e); + onError((SpannerException) e); + res.setException(e); + } + } + }), + MoreExecutors.directExecutor()); + } catch (InterruptedException e) { + res.setException(SpannerExceptionFactory.propagateInterrupt(e)); + } catch (ExecutionException e) { + res.setException( + SpannerExceptionFactory.newSpannerException( + e.getCause() == null ? e : e.getCause())); + } + } + }, + MoreExecutors.directExecutor()); + return res; } Timestamp commitTimestamp() { @@ -190,6 +360,25 @@ void rollback() { } } + ApiFuture rollbackAsync() { + span.addAnnotation("Starting Rollback"); + return ApiFutures.transformAsync( + rpc.rollbackAsync( + RollbackRequest.newBuilder() + .setSession(session.getName()) + .setTransactionId(transactionId) + .build(), + session.getOptions()), + new ApiAsyncFunction() { + @Override + public ApiFuture apply(Empty input) throws Exception { + span.addAnnotation("Rollback Done"); + return ApiFutures.immediateFuture(null); + } + }, + MoreExecutors.directExecutor()); + } + @Nullable @Override TransactionSelector getTransactionSelector() { @@ -252,6 +441,61 @@ public long executeUpdate(Statement statement) { } } + @Override + public ApiFuture executeUpdateAsync(Statement statement) { + beforeReadOrQuery(); + final ExecuteSqlRequest.Builder builder = + getExecuteSqlRequestBuilder(statement, QueryMode.NORMAL); + ApiFuture resultSet; + try { + // Register the update as an async operation that must finish before the transaction may + // commit. + increaseAsynOperations(); + resultSet = rpc.executeQueryAsync(builder.build(), session.getOptions()); + } catch (Throwable t) { + decreaseAsyncOperations(); + throw t; + } + ApiFuture updateCount = + ApiFutures.transform( + resultSet, + new ApiFunction() { + @Override + public Long apply(ResultSet input) { + if (!input.hasStats()) { + throw SpannerExceptionFactory.newSpannerException( + ErrorCode.INVALID_ARGUMENT, + "DML response missing stats possibly due to non-DML statement as input"); + } + // For standard DML, using the exact row count. + return input.getStats().getRowCountExact(); + } + }, + MoreExecutors.directExecutor()); + updateCount = + ApiFutures.catching( + updateCount, + Throwable.class, + new ApiFunction() { + @Override + public Long apply(Throwable input) { + SpannerException e = SpannerExceptionFactory.newSpannerException(input); + onError(e); + throw e; + } + }, + MoreExecutors.directExecutor()); + updateCount.addListener( + new Runnable() { + @Override + public void run() { + decreaseAsyncOperations(); + } + }, + MoreExecutors.directExecutor()); + return updateCount; + } + @Override public long[] batchUpdate(Iterable statements) { beforeReadOrQuery(); @@ -281,11 +525,91 @@ public long[] batchUpdate(Iterable statements) { throw e; } } + + @Override + public ApiFuture batchUpdateAsync(Iterable statements) { + beforeReadOrQuery(); + final ExecuteBatchDmlRequest.Builder builder = getExecuteBatchDmlRequestBuilder(statements); + ApiFuture response; + try { + // Register the update as an async operation that must finish before the transaction may + // commit. + increaseAsynOperations(); + response = rpc.executeBatchDmlAsync(builder.build(), session.getOptions()); + } catch (Throwable t) { + decreaseAsyncOperations(); + throw t; + } + final ApiFuture updateCounts = + ApiFutures.transform( + response, + new ApiFunction() { + @Override + public long[] apply(ExecuteBatchDmlResponse input) { + long[] results = new long[input.getResultSetsCount()]; + for (int i = 0; i < input.getResultSetsCount(); ++i) { + results[i] = input.getResultSets(i).getStats().getRowCountExact(); + } + // If one of the DML statements was aborted, we should throw an aborted exception. + // In all other cases, we should throw a BatchUpdateException. + if (input.getStatus().getCode() == Code.ABORTED_VALUE) { + throw newSpannerException( + ErrorCode.fromRpcStatus(input.getStatus()), input.getStatus().getMessage()); + } else if (input.getStatus().getCode() != 0) { + throw newSpannerBatchUpdateException( + ErrorCode.fromRpcStatus(input.getStatus()), + input.getStatus().getMessage(), + results); + } + return results; + } + }, + MoreExecutors.directExecutor()); + updateCounts.addListener( + new Runnable() { + @Override + public void run() { + try { + updateCounts.get(); + } catch (ExecutionException e) { + onError(SpannerExceptionFactory.newSpannerException(e.getCause())); + } catch (InterruptedException e) { + onError(SpannerExceptionFactory.propagateInterrupt(e)); + } finally { + decreaseAsyncOperations(); + } + } + }, + MoreExecutors.directExecutor()); + return updateCounts; + } + + private ListenableAsyncResultSet wrap(ListenableAsyncResultSet delegate) { + return new TransactionContextAsyncResultSetImpl(delegate); + } + + @Override + public ListenableAsyncResultSet readAsync( + String table, KeySet keys, Iterable columns, ReadOption... options) { + return wrap(super.readAsync(table, keys, columns, options)); + } + + @Override + public ListenableAsyncResultSet readUsingIndexAsync( + String table, String index, KeySet keys, Iterable columns, ReadOption... options) { + return wrap(super.readUsingIndexAsync(table, index, keys, columns, options)); + } + + @Override + public ListenableAsyncResultSet executeQueryAsync( + final Statement statement, final QueryOption... options) { + return wrap(super.executeQueryAsync(statement, options)); + } } private boolean blockNestedTxn = true; private final SessionImpl session; - private final Span span; + private Span span; private TransactionContextImpl txn; private volatile boolean isValid = true; @@ -297,10 +621,14 @@ public TransactionRunner allowNestedTransaction() { TransactionRunnerImpl(SessionImpl session, SpannerRpc rpc, int defaultPrefetchChunks) { this.session = session; - this.span = Tracing.getTracer().getCurrentSpan(); this.txn = session.newTransaction(); } + @Override + public void setSpan(Span span) { + this.span = span; + } + @Nullable @Override public T run(TransactionCallable callable) { @@ -326,7 +654,7 @@ private T runInternal(final TransactionCallable txCallable) { new Callable() { @Override public T call() { - if (txn.isAborted()) { + if (attempt.get() > 0) { txn = session.newTransaction(); } checkState( diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java index 63b9a3a135..c32b5fde81 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Value.java @@ -71,7 +71,7 @@ public abstract class Value implements Serializable { */ public static final Timestamp COMMIT_TIMESTAMP = Timestamp.ofTimeMicroseconds(0L); - private static final int MAX_DEBUG_STRING_LENGTH = 32; + private static final int MAX_DEBUG_STRING_LENGTH = 36; private static final String ELLIPSIS = "..."; private static final String NULL_STRING = "NULL"; private static final char LIST_SEPERATOR = ','; diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/admin/database/v1/DatabaseAdminClient.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/admin/database/v1/DatabaseAdminClient.java index 112b43e2ef..27dd9c515a 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/admin/database/v1/DatabaseAdminClient.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/admin/database/v1/DatabaseAdminClient.java @@ -210,138 +210,6 @@ public final OperationsClient getOperationsClient() { return operationsClient; } - // AUTO-GENERATED DOCUMENTATION AND METHOD - /** - * Lists Cloud Spanner databases. - * - *

Sample code: - * - *


-   * try (DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create()) {
-   *   InstanceName parent = InstanceName.of("[PROJECT]", "[INSTANCE]");
-   *   for (Database element : databaseAdminClient.listDatabases(parent).iterateAll()) {
-   *     // doThingsWith(element);
-   *   }
-   * }
-   * 
- * - * @param parent Required. The instance whose databases should be listed. Values are of the form - * `projects/<project>/instances/<instance>`. - * @throws com.google.api.gax.rpc.ApiException if the remote call fails - */ - public final ListDatabasesPagedResponse listDatabases(InstanceName parent) { - ListDatabasesRequest request = - ListDatabasesRequest.newBuilder() - .setParent(parent == null ? null : parent.toString()) - .build(); - return listDatabases(request); - } - - // AUTO-GENERATED DOCUMENTATION AND METHOD - /** - * Lists Cloud Spanner databases. - * - *

Sample code: - * - *


-   * try (DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create()) {
-   *   InstanceName parent = InstanceName.of("[PROJECT]", "[INSTANCE]");
-   *   for (Database element : databaseAdminClient.listDatabases(parent.toString()).iterateAll()) {
-   *     // doThingsWith(element);
-   *   }
-   * }
-   * 
- * - * @param parent Required. The instance whose databases should be listed. Values are of the form - * `projects/<project>/instances/<instance>`. - * @throws com.google.api.gax.rpc.ApiException if the remote call fails - */ - public final ListDatabasesPagedResponse listDatabases(String parent) { - ListDatabasesRequest request = ListDatabasesRequest.newBuilder().setParent(parent).build(); - return listDatabases(request); - } - - // AUTO-GENERATED DOCUMENTATION AND METHOD - /** - * Lists Cloud Spanner databases. - * - *

Sample code: - * - *


-   * try (DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create()) {
-   *   InstanceName parent = InstanceName.of("[PROJECT]", "[INSTANCE]");
-   *   ListDatabasesRequest request = ListDatabasesRequest.newBuilder()
-   *     .setParent(parent.toString())
-   *     .build();
-   *   for (Database element : databaseAdminClient.listDatabases(request).iterateAll()) {
-   *     // doThingsWith(element);
-   *   }
-   * }
-   * 
- * - * @param request The request object containing all of the parameters for the API call. - * @throws com.google.api.gax.rpc.ApiException if the remote call fails - */ - public final ListDatabasesPagedResponse listDatabases(ListDatabasesRequest request) { - return listDatabasesPagedCallable().call(request); - } - - // AUTO-GENERATED DOCUMENTATION AND METHOD - /** - * Lists Cloud Spanner databases. - * - *

Sample code: - * - *


-   * try (DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create()) {
-   *   InstanceName parent = InstanceName.of("[PROJECT]", "[INSTANCE]");
-   *   ListDatabasesRequest request = ListDatabasesRequest.newBuilder()
-   *     .setParent(parent.toString())
-   *     .build();
-   *   ApiFuture<ListDatabasesPagedResponse> future = databaseAdminClient.listDatabasesPagedCallable().futureCall(request);
-   *   // Do something
-   *   for (Database element : future.get().iterateAll()) {
-   *     // doThingsWith(element);
-   *   }
-   * }
-   * 
- */ - public final UnaryCallable - listDatabasesPagedCallable() { - return stub.listDatabasesPagedCallable(); - } - - // AUTO-GENERATED DOCUMENTATION AND METHOD - /** - * Lists Cloud Spanner databases. - * - *

Sample code: - * - *


-   * try (DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create()) {
-   *   InstanceName parent = InstanceName.of("[PROJECT]", "[INSTANCE]");
-   *   ListDatabasesRequest request = ListDatabasesRequest.newBuilder()
-   *     .setParent(parent.toString())
-   *     .build();
-   *   while (true) {
-   *     ListDatabasesResponse response = databaseAdminClient.listDatabasesCallable().call(request);
-   *     for (Database element : response.getDatabasesList()) {
-   *       // doThingsWith(element);
-   *     }
-   *     String nextPageToken = response.getNextPageToken();
-   *     if (!Strings.isNullOrEmpty(nextPageToken)) {
-   *       request = request.toBuilder().setPageToken(nextPageToken).build();
-   *     } else {
-   *       break;
-   *     }
-   *   }
-   * }
-   * 
- */ - public final UnaryCallable listDatabasesCallable() { - return stub.listDatabasesCallable(); - } - // AUTO-GENERATED DOCUMENTATION AND METHOD /** * Creates a new Cloud Spanner database and starts to prepare it for serving. The returned @@ -520,96 +388,6 @@ public final UnaryCallable createDatabaseCalla return stub.createDatabaseCallable(); } - // AUTO-GENERATED DOCUMENTATION AND METHOD - /** - * Gets the state of a Cloud Spanner database. - * - *

Sample code: - * - *


-   * try (DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create()) {
-   *   DatabaseName name = DatabaseName.of("[PROJECT]", "[INSTANCE]", "[DATABASE]");
-   *   Database response = databaseAdminClient.getDatabase(name);
-   * }
-   * 
- * - * @param name Required. The name of the requested database. Values are of the form - * `projects/<project>/instances/<instance>/databases/<database>`. - * @throws com.google.api.gax.rpc.ApiException if the remote call fails - */ - public final Database getDatabase(DatabaseName name) { - GetDatabaseRequest request = - GetDatabaseRequest.newBuilder().setName(name == null ? null : name.toString()).build(); - return getDatabase(request); - } - - // AUTO-GENERATED DOCUMENTATION AND METHOD - /** - * Gets the state of a Cloud Spanner database. - * - *

Sample code: - * - *


-   * try (DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create()) {
-   *   DatabaseName name = DatabaseName.of("[PROJECT]", "[INSTANCE]", "[DATABASE]");
-   *   Database response = databaseAdminClient.getDatabase(name.toString());
-   * }
-   * 
- * - * @param name Required. The name of the requested database. Values are of the form - * `projects/<project>/instances/<instance>/databases/<database>`. - * @throws com.google.api.gax.rpc.ApiException if the remote call fails - */ - public final Database getDatabase(String name) { - GetDatabaseRequest request = GetDatabaseRequest.newBuilder().setName(name).build(); - return getDatabase(request); - } - - // AUTO-GENERATED DOCUMENTATION AND METHOD - /** - * Gets the state of a Cloud Spanner database. - * - *

Sample code: - * - *


-   * try (DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create()) {
-   *   DatabaseName name = DatabaseName.of("[PROJECT]", "[INSTANCE]", "[DATABASE]");
-   *   GetDatabaseRequest request = GetDatabaseRequest.newBuilder()
-   *     .setName(name.toString())
-   *     .build();
-   *   Database response = databaseAdminClient.getDatabase(request);
-   * }
-   * 
- * - * @param request The request object containing all of the parameters for the API call. - * @throws com.google.api.gax.rpc.ApiException if the remote call fails - */ - public final Database getDatabase(GetDatabaseRequest request) { - return getDatabaseCallable().call(request); - } - - // AUTO-GENERATED DOCUMENTATION AND METHOD - /** - * Gets the state of a Cloud Spanner database. - * - *

Sample code: - * - *


-   * try (DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create()) {
-   *   DatabaseName name = DatabaseName.of("[PROJECT]", "[INSTANCE]", "[DATABASE]");
-   *   GetDatabaseRequest request = GetDatabaseRequest.newBuilder()
-   *     .setName(name.toString())
-   *     .build();
-   *   ApiFuture<Database> future = databaseAdminClient.getDatabaseCallable().futureCall(request);
-   *   // Do something
-   *   Database response = future.get();
-   * }
-   * 
- */ - public final UnaryCallable getDatabaseCallable() { - return stub.getDatabaseCallable(); - } - // AUTO-GENERATED DOCUMENTATION AND METHOD /** * Updates the schema of a Cloud Spanner database by creating/altering/dropping tables, columns, @@ -736,46 +514,696 @@ public final OperationFuture updateDatabaseDdl * .setDatabase(database.toString()) * .addAllStatements(statements) * .build(); - * OperationFuture<Empty, UpdateDatabaseDdlMetadata> future = databaseAdminClient.updateDatabaseDdlOperationCallable().futureCall(request); - * // Do something - * future.get(); + * OperationFuture<Empty, UpdateDatabaseDdlMetadata> future = databaseAdminClient.updateDatabaseDdlOperationCallable().futureCall(request); + * // Do something + * future.get(); + * } + * + */ + @BetaApi("The surface for use by generated code is not stable yet and may change in the future.") + public final OperationCallable + updateDatabaseDdlOperationCallable() { + return stub.updateDatabaseDdlOperationCallable(); + } + + // AUTO-GENERATED DOCUMENTATION AND METHOD + /** + * Updates the schema of a Cloud Spanner database by creating/altering/dropping tables, columns, + * indexes, etc. The returned [long-running operation][google.longrunning.Operation] will have a + * name of the format `<database_name>/operations/<operation_id>` and can be used to + * track execution of the schema change(s). The [metadata][google.longrunning.Operation.metadata] + * field type is + * [UpdateDatabaseDdlMetadata][google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata]. The + * operation has no response. + * + *

Sample code: + * + *


+   * try (DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create()) {
+   *   DatabaseName database = DatabaseName.of("[PROJECT]", "[INSTANCE]", "[DATABASE]");
+   *   List<String> statements = new ArrayList<>();
+   *   UpdateDatabaseDdlRequest request = UpdateDatabaseDdlRequest.newBuilder()
+   *     .setDatabase(database.toString())
+   *     .addAllStatements(statements)
+   *     .build();
+   *   ApiFuture<Operation> future = databaseAdminClient.updateDatabaseDdlCallable().futureCall(request);
+   *   // Do something
+   *   future.get();
+   * }
+   * 
+ */ + public final UnaryCallable updateDatabaseDdlCallable() { + return stub.updateDatabaseDdlCallable(); + } + + // AUTO-GENERATED DOCUMENTATION AND METHOD + /** + * Starts creating a new Cloud Spanner Backup. The returned backup [long-running + * operation][google.longrunning.Operation] will have a name of the format + * `projects/<project>/instances/<instance>/backups/<backup>/operations/<operation_id>` + * and can be used to track creation of the backup. The + * [metadata][google.longrunning.Operation.metadata] field type is + * [CreateBackupMetadata][google.spanner.admin.database.v1.CreateBackupMetadata]. The + * [response][google.longrunning.Operation.response] field type is + * [Backup][google.spanner.admin.database.v1.Backup], if successful. Cancelling the returned + * operation will stop the creation and delete the backup. There can be only one pending backup + * creation per database. Backup creation of different databases can run concurrently. + * + *

Sample code: + * + *


+   * try (DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create()) {
+   *   InstanceName parent = InstanceName.of("[PROJECT]", "[INSTANCE]");
+   *   Backup backup = Backup.newBuilder().build();
+   *   String backupId = "";
+   *   Backup response = databaseAdminClient.createBackupAsync(parent, backup, backupId).get();
+   * }
+   * 
+ * + * @param parent Required. The name of the instance in which the backup will be created. This must + * be the same instance that contains the database the backup will be created from. The backup + * will be stored in the location(s) specified in the instance configuration of this instance. + * Values are of the form `projects/<project>/instances/<instance>`. + * @param backup Required. The backup to create. + * @param backupId Required. The id of the backup to be created. The `backup_id` appended to + * `parent` forms the full backup name of the form + * `projects/<project>/instances/<instance>/backups/<backup_id>`. + * @throws com.google.api.gax.rpc.ApiException if the remote call fails + */ + @BetaApi( + "The surface for long-running operations is not stable yet and may change in the future.") + public final OperationFuture createBackupAsync( + InstanceName parent, Backup backup, String backupId) { + CreateBackupRequest request = + CreateBackupRequest.newBuilder() + .setParent(parent == null ? null : parent.toString()) + .setBackup(backup) + .setBackupId(backupId) + .build(); + return createBackupAsync(request); + } + + // AUTO-GENERATED DOCUMENTATION AND METHOD + /** + * Starts creating a new Cloud Spanner Backup. The returned backup [long-running + * operation][google.longrunning.Operation] will have a name of the format + * `projects/<project>/instances/<instance>/backups/<backup>/operations/<operation_id>` + * and can be used to track creation of the backup. The + * [metadata][google.longrunning.Operation.metadata] field type is + * [CreateBackupMetadata][google.spanner.admin.database.v1.CreateBackupMetadata]. The + * [response][google.longrunning.Operation.response] field type is + * [Backup][google.spanner.admin.database.v1.Backup], if successful. Cancelling the returned + * operation will stop the creation and delete the backup. There can be only one pending backup + * creation per database. Backup creation of different databases can run concurrently. + * + *

Sample code: + * + *


+   * try (DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create()) {
+   *   InstanceName parent = InstanceName.of("[PROJECT]", "[INSTANCE]");
+   *   Backup backup = Backup.newBuilder().build();
+   *   String backupId = "";
+   *   Backup response = databaseAdminClient.createBackupAsync(parent.toString(), backup, backupId).get();
+   * }
+   * 
+ * + * @param parent Required. The name of the instance in which the backup will be created. This must + * be the same instance that contains the database the backup will be created from. The backup + * will be stored in the location(s) specified in the instance configuration of this instance. + * Values are of the form `projects/<project>/instances/<instance>`. + * @param backup Required. The backup to create. + * @param backupId Required. The id of the backup to be created. The `backup_id` appended to + * `parent` forms the full backup name of the form + * `projects/<project>/instances/<instance>/backups/<backup_id>`. + * @throws com.google.api.gax.rpc.ApiException if the remote call fails + */ + @BetaApi( + "The surface for long-running operations is not stable yet and may change in the future.") + public final OperationFuture createBackupAsync( + String parent, Backup backup, String backupId) { + CreateBackupRequest request = + CreateBackupRequest.newBuilder() + .setParent(parent) + .setBackup(backup) + .setBackupId(backupId) + .build(); + return createBackupAsync(request); + } + + // AUTO-GENERATED DOCUMENTATION AND METHOD + /** + * Starts creating a new Cloud Spanner Backup. The returned backup [long-running + * operation][google.longrunning.Operation] will have a name of the format + * `projects/<project>/instances/<instance>/backups/<backup>/operations/<operation_id>` + * and can be used to track creation of the backup. The + * [metadata][google.longrunning.Operation.metadata] field type is + * [CreateBackupMetadata][google.spanner.admin.database.v1.CreateBackupMetadata]. The + * [response][google.longrunning.Operation.response] field type is + * [Backup][google.spanner.admin.database.v1.Backup], if successful. Cancelling the returned + * operation will stop the creation and delete the backup. There can be only one pending backup + * creation per database. Backup creation of different databases can run concurrently. + * + *

Sample code: + * + *


+   * try (DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create()) {
+   *   InstanceName parent = InstanceName.of("[PROJECT]", "[INSTANCE]");
+   *   String backupId = "";
+   *   Backup backup = Backup.newBuilder().build();
+   *   CreateBackupRequest request = CreateBackupRequest.newBuilder()
+   *     .setParent(parent.toString())
+   *     .setBackupId(backupId)
+   *     .setBackup(backup)
+   *     .build();
+   *   Backup response = databaseAdminClient.createBackupAsync(request).get();
+   * }
+   * 
+ * + * @param request The request object containing all of the parameters for the API call. + * @throws com.google.api.gax.rpc.ApiException if the remote call fails + */ + @BetaApi( + "The surface for long-running operations is not stable yet and may change in the future.") + public final OperationFuture createBackupAsync( + CreateBackupRequest request) { + return createBackupOperationCallable().futureCall(request); + } + + // AUTO-GENERATED DOCUMENTATION AND METHOD + /** + * Starts creating a new Cloud Spanner Backup. The returned backup [long-running + * operation][google.longrunning.Operation] will have a name of the format + * `projects/<project>/instances/<instance>/backups/<backup>/operations/<operation_id>` + * and can be used to track creation of the backup. The + * [metadata][google.longrunning.Operation.metadata] field type is + * [CreateBackupMetadata][google.spanner.admin.database.v1.CreateBackupMetadata]. The + * [response][google.longrunning.Operation.response] field type is + * [Backup][google.spanner.admin.database.v1.Backup], if successful. Cancelling the returned + * operation will stop the creation and delete the backup. There can be only one pending backup + * creation per database. Backup creation of different databases can run concurrently. + * + *

Sample code: + * + *


+   * try (DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create()) {
+   *   InstanceName parent = InstanceName.of("[PROJECT]", "[INSTANCE]");
+   *   String backupId = "";
+   *   Backup backup = Backup.newBuilder().build();
+   *   CreateBackupRequest request = CreateBackupRequest.newBuilder()
+   *     .setParent(parent.toString())
+   *     .setBackupId(backupId)
+   *     .setBackup(backup)
+   *     .build();
+   *   OperationFuture<Backup, CreateBackupMetadata> future = databaseAdminClient.createBackupOperationCallable().futureCall(request);
+   *   // Do something
+   *   Backup response = future.get();
+   * }
+   * 
+ */ + @BetaApi("The surface for use by generated code is not stable yet and may change in the future.") + public final OperationCallable + createBackupOperationCallable() { + return stub.createBackupOperationCallable(); + } + + // AUTO-GENERATED DOCUMENTATION AND METHOD + /** + * Starts creating a new Cloud Spanner Backup. The returned backup [long-running + * operation][google.longrunning.Operation] will have a name of the format + * `projects/<project>/instances/<instance>/backups/<backup>/operations/<operation_id>` + * and can be used to track creation of the backup. The + * [metadata][google.longrunning.Operation.metadata] field type is + * [CreateBackupMetadata][google.spanner.admin.database.v1.CreateBackupMetadata]. The + * [response][google.longrunning.Operation.response] field type is + * [Backup][google.spanner.admin.database.v1.Backup], if successful. Cancelling the returned + * operation will stop the creation and delete the backup. There can be only one pending backup + * creation per database. Backup creation of different databases can run concurrently. + * + *

Sample code: + * + *


+   * try (DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create()) {
+   *   InstanceName parent = InstanceName.of("[PROJECT]", "[INSTANCE]");
+   *   String backupId = "";
+   *   Backup backup = Backup.newBuilder().build();
+   *   CreateBackupRequest request = CreateBackupRequest.newBuilder()
+   *     .setParent(parent.toString())
+   *     .setBackupId(backupId)
+   *     .setBackup(backup)
+   *     .build();
+   *   ApiFuture<Operation> future = databaseAdminClient.createBackupCallable().futureCall(request);
+   *   // Do something
+   *   Operation response = future.get();
+   * }
+   * 
+ */ + public final UnaryCallable createBackupCallable() { + return stub.createBackupCallable(); + } + + // AUTO-GENERATED DOCUMENTATION AND METHOD + /** + * Create a new database by restoring from a completed backup. The new database must be in the + * same project and in an instance with the same instance configuration as the instance containing + * the backup. The returned database [long-running operation][google.longrunning.Operation] has a + * name of the format + * `projects/<project>/instances/<instance>/databases/<database>/operations/<operation_id>`, + * and can be used to track the progress of the operation, and to cancel it. The + * [metadata][google.longrunning.Operation.metadata] field type is + * [RestoreDatabaseMetadata][google.spanner.admin.database.v1.RestoreDatabaseMetadata]. The + * [response][google.longrunning.Operation.response] type is + * [Database][google.spanner.admin.database.v1.Database], if successful. Cancelling the returned + * operation will stop the restore and delete the database. There can be only one database being + * restored into an instance at a time. Once the restore operation completes, a new restore + * operation can be initiated, without waiting for the optimize operation associated with the + * first restore to complete. + * + *

Sample code: + * + *


+   * try (DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create()) {
+   *   InstanceName parent = InstanceName.of("[PROJECT]", "[INSTANCE]");
+   *   String databaseId = "";
+   *   BackupName backup = BackupName.of("[PROJECT]", "[INSTANCE]", "[BACKUP]");
+   *   Database response = databaseAdminClient.restoreDatabaseAsync(parent, databaseId, backup).get();
+   * }
+   * 
+ * + * @param parent Required. The name of the instance in which to create the restored database. This + * instance must be in the same project and have the same instance configuration as the + * instance containing the source backup. Values are of the form + * `projects/<project>/instances/<instance>`. + * @param databaseId Required. The id of the database to create and restore to. This database must + * not already exist. The `database_id` appended to `parent` forms the full database name of + * the form + * `projects/<project>/instances/<instance>/databases/<database_id>`. + * @param backup Name of the backup from which to restore. Values are of the form + * `projects/<project>/instances/<instance>/backups/<backup>`. + * @throws com.google.api.gax.rpc.ApiException if the remote call fails + */ + @BetaApi( + "The surface for long-running operations is not stable yet and may change in the future.") + public final OperationFuture restoreDatabaseAsync( + InstanceName parent, String databaseId, BackupName backup) { + RestoreDatabaseRequest request = + RestoreDatabaseRequest.newBuilder() + .setParent(parent == null ? null : parent.toString()) + .setDatabaseId(databaseId) + .setBackup(backup == null ? null : backup.toString()) + .build(); + return restoreDatabaseAsync(request); + } + + // AUTO-GENERATED DOCUMENTATION AND METHOD + /** + * Create a new database by restoring from a completed backup. The new database must be in the + * same project and in an instance with the same instance configuration as the instance containing + * the backup. The returned database [long-running operation][google.longrunning.Operation] has a + * name of the format + * `projects/<project>/instances/<instance>/databases/<database>/operations/<operation_id>`, + * and can be used to track the progress of the operation, and to cancel it. The + * [metadata][google.longrunning.Operation.metadata] field type is + * [RestoreDatabaseMetadata][google.spanner.admin.database.v1.RestoreDatabaseMetadata]. The + * [response][google.longrunning.Operation.response] type is + * [Database][google.spanner.admin.database.v1.Database], if successful. Cancelling the returned + * operation will stop the restore and delete the database. There can be only one database being + * restored into an instance at a time. Once the restore operation completes, a new restore + * operation can be initiated, without waiting for the optimize operation associated with the + * first restore to complete. + * + *

Sample code: + * + *


+   * try (DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create()) {
+   *   InstanceName parent = InstanceName.of("[PROJECT]", "[INSTANCE]");
+   *   String databaseId = "";
+   *   BackupName backup = BackupName.of("[PROJECT]", "[INSTANCE]", "[BACKUP]");
+   *   Database response = databaseAdminClient.restoreDatabaseAsync(parent.toString(), databaseId, backup.toString()).get();
+   * }
+   * 
+ * + * @param parent Required. The name of the instance in which to create the restored database. This + * instance must be in the same project and have the same instance configuration as the + * instance containing the source backup. Values are of the form + * `projects/<project>/instances/<instance>`. + * @param databaseId Required. The id of the database to create and restore to. This database must + * not already exist. The `database_id` appended to `parent` forms the full database name of + * the form + * `projects/<project>/instances/<instance>/databases/<database_id>`. + * @param backup Name of the backup from which to restore. Values are of the form + * `projects/<project>/instances/<instance>/backups/<backup>`. + * @throws com.google.api.gax.rpc.ApiException if the remote call fails + */ + @BetaApi( + "The surface for long-running operations is not stable yet and may change in the future.") + public final OperationFuture restoreDatabaseAsync( + String parent, String databaseId, String backup) { + RestoreDatabaseRequest request = + RestoreDatabaseRequest.newBuilder() + .setParent(parent) + .setDatabaseId(databaseId) + .setBackup(backup) + .build(); + return restoreDatabaseAsync(request); + } + + // AUTO-GENERATED DOCUMENTATION AND METHOD + /** + * Create a new database by restoring from a completed backup. The new database must be in the + * same project and in an instance with the same instance configuration as the instance containing + * the backup. The returned database [long-running operation][google.longrunning.Operation] has a + * name of the format + * `projects/<project>/instances/<instance>/databases/<database>/operations/<operation_id>`, + * and can be used to track the progress of the operation, and to cancel it. The + * [metadata][google.longrunning.Operation.metadata] field type is + * [RestoreDatabaseMetadata][google.spanner.admin.database.v1.RestoreDatabaseMetadata]. The + * [response][google.longrunning.Operation.response] type is + * [Database][google.spanner.admin.database.v1.Database], if successful. Cancelling the returned + * operation will stop the restore and delete the database. There can be only one database being + * restored into an instance at a time. Once the restore operation completes, a new restore + * operation can be initiated, without waiting for the optimize operation associated with the + * first restore to complete. + * + *

Sample code: + * + *


+   * try (DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create()) {
+   *   InstanceName parent = InstanceName.of("[PROJECT]", "[INSTANCE]");
+   *   String databaseId = "";
+   *   RestoreDatabaseRequest request = RestoreDatabaseRequest.newBuilder()
+   *     .setParent(parent.toString())
+   *     .setDatabaseId(databaseId)
+   *     .build();
+   *   Database response = databaseAdminClient.restoreDatabaseAsync(request).get();
+   * }
+   * 
+ * + * @param request The request object containing all of the parameters for the API call. + * @throws com.google.api.gax.rpc.ApiException if the remote call fails + */ + @BetaApi( + "The surface for long-running operations is not stable yet and may change in the future.") + public final OperationFuture restoreDatabaseAsync( + RestoreDatabaseRequest request) { + return restoreDatabaseOperationCallable().futureCall(request); + } + + // AUTO-GENERATED DOCUMENTATION AND METHOD + /** + * Create a new database by restoring from a completed backup. The new database must be in the + * same project and in an instance with the same instance configuration as the instance containing + * the backup. The returned database [long-running operation][google.longrunning.Operation] has a + * name of the format + * `projects/<project>/instances/<instance>/databases/<database>/operations/<operation_id>`, + * and can be used to track the progress of the operation, and to cancel it. The + * [metadata][google.longrunning.Operation.metadata] field type is + * [RestoreDatabaseMetadata][google.spanner.admin.database.v1.RestoreDatabaseMetadata]. The + * [response][google.longrunning.Operation.response] type is + * [Database][google.spanner.admin.database.v1.Database], if successful. Cancelling the returned + * operation will stop the restore and delete the database. There can be only one database being + * restored into an instance at a time. Once the restore operation completes, a new restore + * operation can be initiated, without waiting for the optimize operation associated with the + * first restore to complete. + * + *

Sample code: + * + *


+   * try (DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create()) {
+   *   InstanceName parent = InstanceName.of("[PROJECT]", "[INSTANCE]");
+   *   String databaseId = "";
+   *   RestoreDatabaseRequest request = RestoreDatabaseRequest.newBuilder()
+   *     .setParent(parent.toString())
+   *     .setDatabaseId(databaseId)
+   *     .build();
+   *   OperationFuture<Database, RestoreDatabaseMetadata> future = databaseAdminClient.restoreDatabaseOperationCallable().futureCall(request);
+   *   // Do something
+   *   Database response = future.get();
+   * }
+   * 
+ */ + @BetaApi("The surface for use by generated code is not stable yet and may change in the future.") + public final OperationCallable + restoreDatabaseOperationCallable() { + return stub.restoreDatabaseOperationCallable(); + } + + // AUTO-GENERATED DOCUMENTATION AND METHOD + /** + * Create a new database by restoring from a completed backup. The new database must be in the + * same project and in an instance with the same instance configuration as the instance containing + * the backup. The returned database [long-running operation][google.longrunning.Operation] has a + * name of the format + * `projects/<project>/instances/<instance>/databases/<database>/operations/<operation_id>`, + * and can be used to track the progress of the operation, and to cancel it. The + * [metadata][google.longrunning.Operation.metadata] field type is + * [RestoreDatabaseMetadata][google.spanner.admin.database.v1.RestoreDatabaseMetadata]. The + * [response][google.longrunning.Operation.response] type is + * [Database][google.spanner.admin.database.v1.Database], if successful. Cancelling the returned + * operation will stop the restore and delete the database. There can be only one database being + * restored into an instance at a time. Once the restore operation completes, a new restore + * operation can be initiated, without waiting for the optimize operation associated with the + * first restore to complete. + * + *

Sample code: + * + *


+   * try (DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create()) {
+   *   InstanceName parent = InstanceName.of("[PROJECT]", "[INSTANCE]");
+   *   String databaseId = "";
+   *   RestoreDatabaseRequest request = RestoreDatabaseRequest.newBuilder()
+   *     .setParent(parent.toString())
+   *     .setDatabaseId(databaseId)
+   *     .build();
+   *   ApiFuture<Operation> future = databaseAdminClient.restoreDatabaseCallable().futureCall(request);
+   *   // Do something
+   *   Operation response = future.get();
+   * }
+   * 
+ */ + public final UnaryCallable restoreDatabaseCallable() { + return stub.restoreDatabaseCallable(); + } + + // AUTO-GENERATED DOCUMENTATION AND METHOD + /** + * Lists Cloud Spanner databases. + * + *

Sample code: + * + *


+   * try (DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create()) {
+   *   InstanceName parent = InstanceName.of("[PROJECT]", "[INSTANCE]");
+   *   for (Database element : databaseAdminClient.listDatabases(parent).iterateAll()) {
+   *     // doThingsWith(element);
+   *   }
+   * }
+   * 
+ * + * @param parent Required. The instance whose databases should be listed. Values are of the form + * `projects/<project>/instances/<instance>`. + * @throws com.google.api.gax.rpc.ApiException if the remote call fails + */ + public final ListDatabasesPagedResponse listDatabases(InstanceName parent) { + ListDatabasesRequest request = + ListDatabasesRequest.newBuilder() + .setParent(parent == null ? null : parent.toString()) + .build(); + return listDatabases(request); + } + + // AUTO-GENERATED DOCUMENTATION AND METHOD + /** + * Lists Cloud Spanner databases. + * + *

Sample code: + * + *


+   * try (DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create()) {
+   *   InstanceName parent = InstanceName.of("[PROJECT]", "[INSTANCE]");
+   *   for (Database element : databaseAdminClient.listDatabases(parent.toString()).iterateAll()) {
+   *     // doThingsWith(element);
+   *   }
+   * }
+   * 
+ * + * @param parent Required. The instance whose databases should be listed. Values are of the form + * `projects/<project>/instances/<instance>`. + * @throws com.google.api.gax.rpc.ApiException if the remote call fails + */ + public final ListDatabasesPagedResponse listDatabases(String parent) { + ListDatabasesRequest request = ListDatabasesRequest.newBuilder().setParent(parent).build(); + return listDatabases(request); + } + + // AUTO-GENERATED DOCUMENTATION AND METHOD + /** + * Lists Cloud Spanner databases. + * + *

Sample code: + * + *


+   * try (DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create()) {
+   *   InstanceName parent = InstanceName.of("[PROJECT]", "[INSTANCE]");
+   *   ListDatabasesRequest request = ListDatabasesRequest.newBuilder()
+   *     .setParent(parent.toString())
+   *     .build();
+   *   for (Database element : databaseAdminClient.listDatabases(request).iterateAll()) {
+   *     // doThingsWith(element);
+   *   }
+   * }
+   * 
+ * + * @param request The request object containing all of the parameters for the API call. + * @throws com.google.api.gax.rpc.ApiException if the remote call fails + */ + public final ListDatabasesPagedResponse listDatabases(ListDatabasesRequest request) { + return listDatabasesPagedCallable().call(request); + } + + // AUTO-GENERATED DOCUMENTATION AND METHOD + /** + * Lists Cloud Spanner databases. + * + *

Sample code: + * + *


+   * try (DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create()) {
+   *   InstanceName parent = InstanceName.of("[PROJECT]", "[INSTANCE]");
+   *   ListDatabasesRequest request = ListDatabasesRequest.newBuilder()
+   *     .setParent(parent.toString())
+   *     .build();
+   *   ApiFuture<ListDatabasesPagedResponse> future = databaseAdminClient.listDatabasesPagedCallable().futureCall(request);
+   *   // Do something
+   *   for (Database element : future.get().iterateAll()) {
+   *     // doThingsWith(element);
+   *   }
+   * }
+   * 
+ */ + public final UnaryCallable + listDatabasesPagedCallable() { + return stub.listDatabasesPagedCallable(); + } + + // AUTO-GENERATED DOCUMENTATION AND METHOD + /** + * Lists Cloud Spanner databases. + * + *

Sample code: + * + *


+   * try (DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create()) {
+   *   InstanceName parent = InstanceName.of("[PROJECT]", "[INSTANCE]");
+   *   ListDatabasesRequest request = ListDatabasesRequest.newBuilder()
+   *     .setParent(parent.toString())
+   *     .build();
+   *   while (true) {
+   *     ListDatabasesResponse response = databaseAdminClient.listDatabasesCallable().call(request);
+   *     for (Database element : response.getDatabasesList()) {
+   *       // doThingsWith(element);
+   *     }
+   *     String nextPageToken = response.getNextPageToken();
+   *     if (!Strings.isNullOrEmpty(nextPageToken)) {
+   *       request = request.toBuilder().setPageToken(nextPageToken).build();
+   *     } else {
+   *       break;
+   *     }
+   *   }
+   * }
+   * 
+ */ + public final UnaryCallable listDatabasesCallable() { + return stub.listDatabasesCallable(); + } + + // AUTO-GENERATED DOCUMENTATION AND METHOD + /** + * Gets the state of a Cloud Spanner database. + * + *

Sample code: + * + *


+   * try (DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create()) {
+   *   DatabaseName name = DatabaseName.of("[PROJECT]", "[INSTANCE]", "[DATABASE]");
+   *   Database response = databaseAdminClient.getDatabase(name);
+   * }
+   * 
+ * + * @param name Required. The name of the requested database. Values are of the form + * `projects/<project>/instances/<instance>/databases/<database>`. + * @throws com.google.api.gax.rpc.ApiException if the remote call fails + */ + public final Database getDatabase(DatabaseName name) { + GetDatabaseRequest request = + GetDatabaseRequest.newBuilder().setName(name == null ? null : name.toString()).build(); + return getDatabase(request); + } + + // AUTO-GENERATED DOCUMENTATION AND METHOD + /** + * Gets the state of a Cloud Spanner database. + * + *

Sample code: + * + *


+   * try (DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create()) {
+   *   DatabaseName name = DatabaseName.of("[PROJECT]", "[INSTANCE]", "[DATABASE]");
+   *   Database response = databaseAdminClient.getDatabase(name.toString());
+   * }
+   * 
+ * + * @param name Required. The name of the requested database. Values are of the form + * `projects/<project>/instances/<instance>/databases/<database>`. + * @throws com.google.api.gax.rpc.ApiException if the remote call fails + */ + public final Database getDatabase(String name) { + GetDatabaseRequest request = GetDatabaseRequest.newBuilder().setName(name).build(); + return getDatabase(request); + } + + // AUTO-GENERATED DOCUMENTATION AND METHOD + /** + * Gets the state of a Cloud Spanner database. + * + *

Sample code: + * + *


+   * try (DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create()) {
+   *   DatabaseName name = DatabaseName.of("[PROJECT]", "[INSTANCE]", "[DATABASE]");
+   *   GetDatabaseRequest request = GetDatabaseRequest.newBuilder()
+   *     .setName(name.toString())
+   *     .build();
+   *   Database response = databaseAdminClient.getDatabase(request);
    * }
    * 
+ * + * @param request The request object containing all of the parameters for the API call. + * @throws com.google.api.gax.rpc.ApiException if the remote call fails */ - @BetaApi("The surface for use by generated code is not stable yet and may change in the future.") - public final OperationCallable - updateDatabaseDdlOperationCallable() { - return stub.updateDatabaseDdlOperationCallable(); + public final Database getDatabase(GetDatabaseRequest request) { + return getDatabaseCallable().call(request); } // AUTO-GENERATED DOCUMENTATION AND METHOD /** - * Updates the schema of a Cloud Spanner database by creating/altering/dropping tables, columns, - * indexes, etc. The returned [long-running operation][google.longrunning.Operation] will have a - * name of the format `<database_name>/operations/<operation_id>` and can be used to - * track execution of the schema change(s). The [metadata][google.longrunning.Operation.metadata] - * field type is - * [UpdateDatabaseDdlMetadata][google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata]. The - * operation has no response. + * Gets the state of a Cloud Spanner database. * *

Sample code: * *


    * try (DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create()) {
-   *   DatabaseName database = DatabaseName.of("[PROJECT]", "[INSTANCE]", "[DATABASE]");
-   *   List<String> statements = new ArrayList<>();
-   *   UpdateDatabaseDdlRequest request = UpdateDatabaseDdlRequest.newBuilder()
-   *     .setDatabase(database.toString())
-   *     .addAllStatements(statements)
+   *   DatabaseName name = DatabaseName.of("[PROJECT]", "[INSTANCE]", "[DATABASE]");
+   *   GetDatabaseRequest request = GetDatabaseRequest.newBuilder()
+   *     .setName(name.toString())
    *     .build();
-   *   ApiFuture<Operation> future = databaseAdminClient.updateDatabaseDdlCallable().futureCall(request);
+   *   ApiFuture<Database> future = databaseAdminClient.getDatabaseCallable().futureCall(request);
    *   // Do something
-   *   future.get();
+   *   Database response = future.get();
    * }
    * 
*/ - public final UnaryCallable updateDatabaseDdlCallable() { - return stub.updateDatabaseDdlCallable(); + public final UnaryCallable getDatabaseCallable() { + return stub.getDatabaseCallable(); } // AUTO-GENERATED DOCUMENTATION AND METHOD @@ -1316,243 +1744,38 @@ public final TestIamPermissionsResponse testIamPermissions( * @throws com.google.api.gax.rpc.ApiException if the remote call fails */ public final TestIamPermissionsResponse testIamPermissions(TestIamPermissionsRequest request) { - return testIamPermissionsCallable().call(request); - } - - // AUTO-GENERATED DOCUMENTATION AND METHOD - /** - * Returns permissions that the caller has on the specified database or backup resource. - * - *

Attempting this RPC on a non-existent Cloud Spanner database will result in a NOT_FOUND - * error if the user has `spanner.databases.list` permission on the containing Cloud Spanner - * instance. Otherwise returns an empty set of permissions. Calling this method on a backup that - * does not exist will result in a NOT_FOUND error if the user has `spanner.backups.list` - * permission on the containing instance. - * - *

Sample code: - * - *


-   * try (DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create()) {
-   *   ResourceName resource = BackupName.of("[PROJECT]", "[INSTANCE]", "[BACKUP]");
-   *   List<String> permissions = new ArrayList<>();
-   *   TestIamPermissionsRequest request = TestIamPermissionsRequest.newBuilder()
-   *     .setResource(resource.toString())
-   *     .addAllPermissions(permissions)
-   *     .build();
-   *   ApiFuture<TestIamPermissionsResponse> future = databaseAdminClient.testIamPermissionsCallable().futureCall(request);
-   *   // Do something
-   *   TestIamPermissionsResponse response = future.get();
-   * }
-   * 
- */ - public final UnaryCallable - testIamPermissionsCallable() { - return stub.testIamPermissionsCallable(); - } - - // AUTO-GENERATED DOCUMENTATION AND METHOD - /** - * Starts creating a new Cloud Spanner Backup. The returned backup [long-running - * operation][google.longrunning.Operation] will have a name of the format - * `projects/<project>/instances/<instance>/backups/<backup>/operations/<operation_id>` - * and can be used to track creation of the backup. The - * [metadata][google.longrunning.Operation.metadata] field type is - * [CreateBackupMetadata][google.spanner.admin.database.v1.CreateBackupMetadata]. The - * [response][google.longrunning.Operation.response] field type is - * [Backup][google.spanner.admin.database.v1.Backup], if successful. Cancelling the returned - * operation will stop the creation and delete the backup. There can be only one pending backup - * creation per database. Backup creation of different databases can run concurrently. - * - *

Sample code: - * - *


-   * try (DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create()) {
-   *   InstanceName parent = InstanceName.of("[PROJECT]", "[INSTANCE]");
-   *   Backup backup = Backup.newBuilder().build();
-   *   String backupId = "";
-   *   Backup response = databaseAdminClient.createBackupAsync(parent, backup, backupId).get();
-   * }
-   * 
- * - * @param parent Required. The name of the instance in which the backup will be created. This must - * be the same instance that contains the database the backup will be created from. The backup - * will be stored in the location(s) specified in the instance configuration of this instance. - * Values are of the form `projects/<project>/instances/<instance>`. - * @param backup Required. The backup to create. - * @param backupId Required. The id of the backup to be created. The `backup_id` appended to - * `parent` forms the full backup name of the form - * `projects/<project>/instances/<instance>/backups/<backup_id>`. - * @throws com.google.api.gax.rpc.ApiException if the remote call fails - */ - @BetaApi( - "The surface for long-running operations is not stable yet and may change in the future.") - public final OperationFuture createBackupAsync( - InstanceName parent, Backup backup, String backupId) { - CreateBackupRequest request = - CreateBackupRequest.newBuilder() - .setParent(parent == null ? null : parent.toString()) - .setBackup(backup) - .setBackupId(backupId) - .build(); - return createBackupAsync(request); - } - - // AUTO-GENERATED DOCUMENTATION AND METHOD - /** - * Starts creating a new Cloud Spanner Backup. The returned backup [long-running - * operation][google.longrunning.Operation] will have a name of the format - * `projects/<project>/instances/<instance>/backups/<backup>/operations/<operation_id>` - * and can be used to track creation of the backup. The - * [metadata][google.longrunning.Operation.metadata] field type is - * [CreateBackupMetadata][google.spanner.admin.database.v1.CreateBackupMetadata]. The - * [response][google.longrunning.Operation.response] field type is - * [Backup][google.spanner.admin.database.v1.Backup], if successful. Cancelling the returned - * operation will stop the creation and delete the backup. There can be only one pending backup - * creation per database. Backup creation of different databases can run concurrently. - * - *

Sample code: - * - *


-   * try (DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create()) {
-   *   InstanceName parent = InstanceName.of("[PROJECT]", "[INSTANCE]");
-   *   Backup backup = Backup.newBuilder().build();
-   *   String backupId = "";
-   *   Backup response = databaseAdminClient.createBackupAsync(parent.toString(), backup, backupId).get();
-   * }
-   * 
- * - * @param parent Required. The name of the instance in which the backup will be created. This must - * be the same instance that contains the database the backup will be created from. The backup - * will be stored in the location(s) specified in the instance configuration of this instance. - * Values are of the form `projects/<project>/instances/<instance>`. - * @param backup Required. The backup to create. - * @param backupId Required. The id of the backup to be created. The `backup_id` appended to - * `parent` forms the full backup name of the form - * `projects/<project>/instances/<instance>/backups/<backup_id>`. - * @throws com.google.api.gax.rpc.ApiException if the remote call fails - */ - @BetaApi( - "The surface for long-running operations is not stable yet and may change in the future.") - public final OperationFuture createBackupAsync( - String parent, Backup backup, String backupId) { - CreateBackupRequest request = - CreateBackupRequest.newBuilder() - .setParent(parent) - .setBackup(backup) - .setBackupId(backupId) - .build(); - return createBackupAsync(request); - } - - // AUTO-GENERATED DOCUMENTATION AND METHOD - /** - * Starts creating a new Cloud Spanner Backup. The returned backup [long-running - * operation][google.longrunning.Operation] will have a name of the format - * `projects/<project>/instances/<instance>/backups/<backup>/operations/<operation_id>` - * and can be used to track creation of the backup. The - * [metadata][google.longrunning.Operation.metadata] field type is - * [CreateBackupMetadata][google.spanner.admin.database.v1.CreateBackupMetadata]. The - * [response][google.longrunning.Operation.response] field type is - * [Backup][google.spanner.admin.database.v1.Backup], if successful. Cancelling the returned - * operation will stop the creation and delete the backup. There can be only one pending backup - * creation per database. Backup creation of different databases can run concurrently. - * - *

Sample code: - * - *


-   * try (DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create()) {
-   *   InstanceName parent = InstanceName.of("[PROJECT]", "[INSTANCE]");
-   *   String backupId = "";
-   *   Backup backup = Backup.newBuilder().build();
-   *   CreateBackupRequest request = CreateBackupRequest.newBuilder()
-   *     .setParent(parent.toString())
-   *     .setBackupId(backupId)
-   *     .setBackup(backup)
-   *     .build();
-   *   Backup response = databaseAdminClient.createBackupAsync(request).get();
-   * }
-   * 
- * - * @param request The request object containing all of the parameters for the API call. - * @throws com.google.api.gax.rpc.ApiException if the remote call fails - */ - @BetaApi( - "The surface for long-running operations is not stable yet and may change in the future.") - public final OperationFuture createBackupAsync( - CreateBackupRequest request) { - return createBackupOperationCallable().futureCall(request); - } - - // AUTO-GENERATED DOCUMENTATION AND METHOD - /** - * Starts creating a new Cloud Spanner Backup. The returned backup [long-running - * operation][google.longrunning.Operation] will have a name of the format - * `projects/<project>/instances/<instance>/backups/<backup>/operations/<operation_id>` - * and can be used to track creation of the backup. The - * [metadata][google.longrunning.Operation.metadata] field type is - * [CreateBackupMetadata][google.spanner.admin.database.v1.CreateBackupMetadata]. The - * [response][google.longrunning.Operation.response] field type is - * [Backup][google.spanner.admin.database.v1.Backup], if successful. Cancelling the returned - * operation will stop the creation and delete the backup. There can be only one pending backup - * creation per database. Backup creation of different databases can run concurrently. - * - *

Sample code: - * - *


-   * try (DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create()) {
-   *   InstanceName parent = InstanceName.of("[PROJECT]", "[INSTANCE]");
-   *   String backupId = "";
-   *   Backup backup = Backup.newBuilder().build();
-   *   CreateBackupRequest request = CreateBackupRequest.newBuilder()
-   *     .setParent(parent.toString())
-   *     .setBackupId(backupId)
-   *     .setBackup(backup)
-   *     .build();
-   *   OperationFuture<Backup, CreateBackupMetadata> future = databaseAdminClient.createBackupOperationCallable().futureCall(request);
-   *   // Do something
-   *   Backup response = future.get();
-   * }
-   * 
- */ - @BetaApi("The surface for use by generated code is not stable yet and may change in the future.") - public final OperationCallable - createBackupOperationCallable() { - return stub.createBackupOperationCallable(); - } - - // AUTO-GENERATED DOCUMENTATION AND METHOD - /** - * Starts creating a new Cloud Spanner Backup. The returned backup [long-running - * operation][google.longrunning.Operation] will have a name of the format - * `projects/<project>/instances/<instance>/backups/<backup>/operations/<operation_id>` - * and can be used to track creation of the backup. The - * [metadata][google.longrunning.Operation.metadata] field type is - * [CreateBackupMetadata][google.spanner.admin.database.v1.CreateBackupMetadata]. The - * [response][google.longrunning.Operation.response] field type is - * [Backup][google.spanner.admin.database.v1.Backup], if successful. Cancelling the returned - * operation will stop the creation and delete the backup. There can be only one pending backup - * creation per database. Backup creation of different databases can run concurrently. + return testIamPermissionsCallable().call(request); + } + + // AUTO-GENERATED DOCUMENTATION AND METHOD + /** + * Returns permissions that the caller has on the specified database or backup resource. + * + *

Attempting this RPC on a non-existent Cloud Spanner database will result in a NOT_FOUND + * error if the user has `spanner.databases.list` permission on the containing Cloud Spanner + * instance. Otherwise returns an empty set of permissions. Calling this method on a backup that + * does not exist will result in a NOT_FOUND error if the user has `spanner.backups.list` + * permission on the containing instance. * *

Sample code: * *


    * try (DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create()) {
-   *   InstanceName parent = InstanceName.of("[PROJECT]", "[INSTANCE]");
-   *   String backupId = "";
-   *   Backup backup = Backup.newBuilder().build();
-   *   CreateBackupRequest request = CreateBackupRequest.newBuilder()
-   *     .setParent(parent.toString())
-   *     .setBackupId(backupId)
-   *     .setBackup(backup)
+   *   ResourceName resource = BackupName.of("[PROJECT]", "[INSTANCE]", "[BACKUP]");
+   *   List<String> permissions = new ArrayList<>();
+   *   TestIamPermissionsRequest request = TestIamPermissionsRequest.newBuilder()
+   *     .setResource(resource.toString())
+   *     .addAllPermissions(permissions)
    *     .build();
-   *   ApiFuture<Operation> future = databaseAdminClient.createBackupCallable().futureCall(request);
+   *   ApiFuture<TestIamPermissionsResponse> future = databaseAdminClient.testIamPermissionsCallable().futureCall(request);
    *   // Do something
-   *   Operation response = future.get();
+   *   TestIamPermissionsResponse response = future.get();
    * }
    * 
*/ - public final UnaryCallable createBackupCallable() { - return stub.createBackupCallable(); + public final UnaryCallable + testIamPermissionsCallable() { + return stub.testIamPermissionsCallable(); } // AUTO-GENERATED DOCUMENTATION AND METHOD @@ -1950,229 +2173,6 @@ public final UnaryCallable listBackupsC return stub.listBackupsCallable(); } - // AUTO-GENERATED DOCUMENTATION AND METHOD - /** - * Create a new database by restoring from a completed backup. The new database must be in the - * same project and in an instance with the same instance configuration as the instance containing - * the backup. The returned database [long-running operation][google.longrunning.Operation] has a - * name of the format - * `projects/<project>/instances/<instance>/databases/<database>/operations/<operation_id>`, - * and can be used to track the progress of the operation, and to cancel it. The - * [metadata][google.longrunning.Operation.metadata] field type is - * [RestoreDatabaseMetadata][google.spanner.admin.database.v1.RestoreDatabaseMetadata]. The - * [response][google.longrunning.Operation.response] type is - * [Database][google.spanner.admin.database.v1.Database], if successful. Cancelling the returned - * operation will stop the restore and delete the database. There can be only one database being - * restored into an instance at a time. Once the restore operation completes, a new restore - * operation can be initiated, without waiting for the optimize operation associated with the - * first restore to complete. - * - *

Sample code: - * - *


-   * try (DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create()) {
-   *   InstanceName parent = InstanceName.of("[PROJECT]", "[INSTANCE]");
-   *   String databaseId = "";
-   *   BackupName backup = BackupName.of("[PROJECT]", "[INSTANCE]", "[BACKUP]");
-   *   Database response = databaseAdminClient.restoreDatabaseAsync(parent, databaseId, backup).get();
-   * }
-   * 
- * - * @param parent Required. The name of the instance in which to create the restored database. This - * instance must be in the same project and have the same instance configuration as the - * instance containing the source backup. Values are of the form - * `projects/<project>/instances/<instance>`. - * @param databaseId Required. The id of the database to create and restore to. This database must - * not already exist. The `database_id` appended to `parent` forms the full database name of - * the form - * `projects/<project>/instances/<instance>/databases/<database_id>`. - * @param backup Name of the backup from which to restore. Values are of the form - * `projects/<project>/instances/<instance>/backups/<backup>`. - * @throws com.google.api.gax.rpc.ApiException if the remote call fails - */ - @BetaApi( - "The surface for long-running operations is not stable yet and may change in the future.") - public final OperationFuture restoreDatabaseAsync( - InstanceName parent, String databaseId, BackupName backup) { - RestoreDatabaseRequest request = - RestoreDatabaseRequest.newBuilder() - .setParent(parent == null ? null : parent.toString()) - .setDatabaseId(databaseId) - .setBackup(backup == null ? null : backup.toString()) - .build(); - return restoreDatabaseAsync(request); - } - - // AUTO-GENERATED DOCUMENTATION AND METHOD - /** - * Create a new database by restoring from a completed backup. The new database must be in the - * same project and in an instance with the same instance configuration as the instance containing - * the backup. The returned database [long-running operation][google.longrunning.Operation] has a - * name of the format - * `projects/<project>/instances/<instance>/databases/<database>/operations/<operation_id>`, - * and can be used to track the progress of the operation, and to cancel it. The - * [metadata][google.longrunning.Operation.metadata] field type is - * [RestoreDatabaseMetadata][google.spanner.admin.database.v1.RestoreDatabaseMetadata]. The - * [response][google.longrunning.Operation.response] type is - * [Database][google.spanner.admin.database.v1.Database], if successful. Cancelling the returned - * operation will stop the restore and delete the database. There can be only one database being - * restored into an instance at a time. Once the restore operation completes, a new restore - * operation can be initiated, without waiting for the optimize operation associated with the - * first restore to complete. - * - *

Sample code: - * - *


-   * try (DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create()) {
-   *   InstanceName parent = InstanceName.of("[PROJECT]", "[INSTANCE]");
-   *   String databaseId = "";
-   *   BackupName backup = BackupName.of("[PROJECT]", "[INSTANCE]", "[BACKUP]");
-   *   Database response = databaseAdminClient.restoreDatabaseAsync(parent.toString(), databaseId, backup.toString()).get();
-   * }
-   * 
- * - * @param parent Required. The name of the instance in which to create the restored database. This - * instance must be in the same project and have the same instance configuration as the - * instance containing the source backup. Values are of the form - * `projects/<project>/instances/<instance>`. - * @param databaseId Required. The id of the database to create and restore to. This database must - * not already exist. The `database_id` appended to `parent` forms the full database name of - * the form - * `projects/<project>/instances/<instance>/databases/<database_id>`. - * @param backup Name of the backup from which to restore. Values are of the form - * `projects/<project>/instances/<instance>/backups/<backup>`. - * @throws com.google.api.gax.rpc.ApiException if the remote call fails - */ - @BetaApi( - "The surface for long-running operations is not stable yet and may change in the future.") - public final OperationFuture restoreDatabaseAsync( - String parent, String databaseId, String backup) { - RestoreDatabaseRequest request = - RestoreDatabaseRequest.newBuilder() - .setParent(parent) - .setDatabaseId(databaseId) - .setBackup(backup) - .build(); - return restoreDatabaseAsync(request); - } - - // AUTO-GENERATED DOCUMENTATION AND METHOD - /** - * Create a new database by restoring from a completed backup. The new database must be in the - * same project and in an instance with the same instance configuration as the instance containing - * the backup. The returned database [long-running operation][google.longrunning.Operation] has a - * name of the format - * `projects/<project>/instances/<instance>/databases/<database>/operations/<operation_id>`, - * and can be used to track the progress of the operation, and to cancel it. The - * [metadata][google.longrunning.Operation.metadata] field type is - * [RestoreDatabaseMetadata][google.spanner.admin.database.v1.RestoreDatabaseMetadata]. The - * [response][google.longrunning.Operation.response] type is - * [Database][google.spanner.admin.database.v1.Database], if successful. Cancelling the returned - * operation will stop the restore and delete the database. There can be only one database being - * restored into an instance at a time. Once the restore operation completes, a new restore - * operation can be initiated, without waiting for the optimize operation associated with the - * first restore to complete. - * - *

Sample code: - * - *


-   * try (DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create()) {
-   *   InstanceName parent = InstanceName.of("[PROJECT]", "[INSTANCE]");
-   *   String databaseId = "";
-   *   RestoreDatabaseRequest request = RestoreDatabaseRequest.newBuilder()
-   *     .setParent(parent.toString())
-   *     .setDatabaseId(databaseId)
-   *     .build();
-   *   Database response = databaseAdminClient.restoreDatabaseAsync(request).get();
-   * }
-   * 
- * - * @param request The request object containing all of the parameters for the API call. - * @throws com.google.api.gax.rpc.ApiException if the remote call fails - */ - @BetaApi( - "The surface for long-running operations is not stable yet and may change in the future.") - public final OperationFuture restoreDatabaseAsync( - RestoreDatabaseRequest request) { - return restoreDatabaseOperationCallable().futureCall(request); - } - - // AUTO-GENERATED DOCUMENTATION AND METHOD - /** - * Create a new database by restoring from a completed backup. The new database must be in the - * same project and in an instance with the same instance configuration as the instance containing - * the backup. The returned database [long-running operation][google.longrunning.Operation] has a - * name of the format - * `projects/<project>/instances/<instance>/databases/<database>/operations/<operation_id>`, - * and can be used to track the progress of the operation, and to cancel it. The - * [metadata][google.longrunning.Operation.metadata] field type is - * [RestoreDatabaseMetadata][google.spanner.admin.database.v1.RestoreDatabaseMetadata]. The - * [response][google.longrunning.Operation.response] type is - * [Database][google.spanner.admin.database.v1.Database], if successful. Cancelling the returned - * operation will stop the restore and delete the database. There can be only one database being - * restored into an instance at a time. Once the restore operation completes, a new restore - * operation can be initiated, without waiting for the optimize operation associated with the - * first restore to complete. - * - *

Sample code: - * - *


-   * try (DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create()) {
-   *   InstanceName parent = InstanceName.of("[PROJECT]", "[INSTANCE]");
-   *   String databaseId = "";
-   *   RestoreDatabaseRequest request = RestoreDatabaseRequest.newBuilder()
-   *     .setParent(parent.toString())
-   *     .setDatabaseId(databaseId)
-   *     .build();
-   *   OperationFuture<Database, RestoreDatabaseMetadata> future = databaseAdminClient.restoreDatabaseOperationCallable().futureCall(request);
-   *   // Do something
-   *   Database response = future.get();
-   * }
-   * 
- */ - @BetaApi("The surface for use by generated code is not stable yet and may change in the future.") - public final OperationCallable - restoreDatabaseOperationCallable() { - return stub.restoreDatabaseOperationCallable(); - } - - // AUTO-GENERATED DOCUMENTATION AND METHOD - /** - * Create a new database by restoring from a completed backup. The new database must be in the - * same project and in an instance with the same instance configuration as the instance containing - * the backup. The returned database [long-running operation][google.longrunning.Operation] has a - * name of the format - * `projects/<project>/instances/<instance>/databases/<database>/operations/<operation_id>`, - * and can be used to track the progress of the operation, and to cancel it. The - * [metadata][google.longrunning.Operation.metadata] field type is - * [RestoreDatabaseMetadata][google.spanner.admin.database.v1.RestoreDatabaseMetadata]. The - * [response][google.longrunning.Operation.response] type is - * [Database][google.spanner.admin.database.v1.Database], if successful. Cancelling the returned - * operation will stop the restore and delete the database. There can be only one database being - * restored into an instance at a time. Once the restore operation completes, a new restore - * operation can be initiated, without waiting for the optimize operation associated with the - * first restore to complete. - * - *

Sample code: - * - *


-   * try (DatabaseAdminClient databaseAdminClient = DatabaseAdminClient.create()) {
-   *   InstanceName parent = InstanceName.of("[PROJECT]", "[INSTANCE]");
-   *   String databaseId = "";
-   *   RestoreDatabaseRequest request = RestoreDatabaseRequest.newBuilder()
-   *     .setParent(parent.toString())
-   *     .setDatabaseId(databaseId)
-   *     .build();
-   *   ApiFuture<Operation> future = databaseAdminClient.restoreDatabaseCallable().futureCall(request);
-   *   // Do something
-   *   Operation response = future.get();
-   * }
-   * 
- */ - public final UnaryCallable restoreDatabaseCallable() { - return stub.restoreDatabaseCallable(); - } - // AUTO-GENERATED DOCUMENTATION AND METHOD /** * Lists database [longrunning-operations][google.longrunning.Operation]. A database operation has diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/admin/database/v1/DatabaseAdminSettings.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/admin/database/v1/DatabaseAdminSettings.java index 53edae1edf..8ae9ec5303 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/admin/database/v1/DatabaseAdminSettings.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/admin/database/v1/DatabaseAdminSettings.java @@ -103,12 +103,6 @@ @Generated("by gapic-generator") @BetaApi public class DatabaseAdminSettings extends ClientSettings { - /** Returns the object with the settings used for calls to listDatabases. */ - public PagedCallSettings - listDatabasesSettings() { - return ((DatabaseAdminStubSettings) getStubSettings()).listDatabasesSettings(); - } - /** Returns the object with the settings used for calls to createDatabase. */ public UnaryCallSettings createDatabaseSettings() { return ((DatabaseAdminStubSettings) getStubSettings()).createDatabaseSettings(); @@ -122,11 +116,6 @@ public UnaryCallSettings createDatabaseSetting return ((DatabaseAdminStubSettings) getStubSettings()).createDatabaseOperationSettings(); } - /** Returns the object with the settings used for calls to getDatabase. */ - public UnaryCallSettings getDatabaseSettings() { - return ((DatabaseAdminStubSettings) getStubSettings()).getDatabaseSettings(); - } - /** Returns the object with the settings used for calls to updateDatabaseDdl. */ public UnaryCallSettings updateDatabaseDdlSettings() { return ((DatabaseAdminStubSettings) getStubSettings()).updateDatabaseDdlSettings(); @@ -140,6 +129,43 @@ public UnaryCallSettings updateDatabaseDdlS return ((DatabaseAdminStubSettings) getStubSettings()).updateDatabaseDdlOperationSettings(); } + /** Returns the object with the settings used for calls to createBackup. */ + public UnaryCallSettings createBackupSettings() { + return ((DatabaseAdminStubSettings) getStubSettings()).createBackupSettings(); + } + + /** Returns the object with the settings used for calls to createBackup. */ + @BetaApi( + "The surface for long-running operations is not stable yet and may change in the future.") + public OperationCallSettings + createBackupOperationSettings() { + return ((DatabaseAdminStubSettings) getStubSettings()).createBackupOperationSettings(); + } + + /** Returns the object with the settings used for calls to restoreDatabase. */ + public UnaryCallSettings restoreDatabaseSettings() { + return ((DatabaseAdminStubSettings) getStubSettings()).restoreDatabaseSettings(); + } + + /** Returns the object with the settings used for calls to restoreDatabase. */ + @BetaApi( + "The surface for long-running operations is not stable yet and may change in the future.") + public OperationCallSettings + restoreDatabaseOperationSettings() { + return ((DatabaseAdminStubSettings) getStubSettings()).restoreDatabaseOperationSettings(); + } + + /** Returns the object with the settings used for calls to listDatabases. */ + public PagedCallSettings + listDatabasesSettings() { + return ((DatabaseAdminStubSettings) getStubSettings()).listDatabasesSettings(); + } + + /** Returns the object with the settings used for calls to getDatabase. */ + public UnaryCallSettings getDatabaseSettings() { + return ((DatabaseAdminStubSettings) getStubSettings()).getDatabaseSettings(); + } + /** Returns the object with the settings used for calls to dropDatabase. */ public UnaryCallSettings dropDatabaseSettings() { return ((DatabaseAdminStubSettings) getStubSettings()).dropDatabaseSettings(); @@ -166,19 +192,6 @@ public UnaryCallSettings getIamPolicySettings() { return ((DatabaseAdminStubSettings) getStubSettings()).testIamPermissionsSettings(); } - /** Returns the object with the settings used for calls to createBackup. */ - public UnaryCallSettings createBackupSettings() { - return ((DatabaseAdminStubSettings) getStubSettings()).createBackupSettings(); - } - - /** Returns the object with the settings used for calls to createBackup. */ - @BetaApi( - "The surface for long-running operations is not stable yet and may change in the future.") - public OperationCallSettings - createBackupOperationSettings() { - return ((DatabaseAdminStubSettings) getStubSettings()).createBackupOperationSettings(); - } - /** Returns the object with the settings used for calls to getBackup. */ public UnaryCallSettings getBackupSettings() { return ((DatabaseAdminStubSettings) getStubSettings()).getBackupSettings(); @@ -200,19 +213,6 @@ public UnaryCallSettings deleteBackupSettings() { return ((DatabaseAdminStubSettings) getStubSettings()).listBackupsSettings(); } - /** Returns the object with the settings used for calls to restoreDatabase. */ - public UnaryCallSettings restoreDatabaseSettings() { - return ((DatabaseAdminStubSettings) getStubSettings()).restoreDatabaseSettings(); - } - - /** Returns the object with the settings used for calls to restoreDatabase. */ - @BetaApi( - "The surface for long-running operations is not stable yet and may change in the future.") - public OperationCallSettings - restoreDatabaseOperationSettings() { - return ((DatabaseAdminStubSettings) getStubSettings()).restoreDatabaseOperationSettings(); - } - /** Returns the object with the settings used for calls to listDatabaseOperations. */ public PagedCallSettings< ListDatabaseOperationsRequest, @@ -328,13 +328,6 @@ public Builder applyToAllUnaryMethods( return this; } - /** Returns the builder for the settings used for calls to listDatabases. */ - public PagedCallSettings.Builder< - ListDatabasesRequest, ListDatabasesResponse, ListDatabasesPagedResponse> - listDatabasesSettings() { - return getStubSettingsBuilder().listDatabasesSettings(); - } - /** Returns the builder for the settings used for calls to createDatabase. */ public UnaryCallSettings.Builder createDatabaseSettings() { return getStubSettingsBuilder().createDatabaseSettings(); @@ -348,11 +341,6 @@ public UnaryCallSettings.Builder createDatabas return getStubSettingsBuilder().createDatabaseOperationSettings(); } - /** Returns the builder for the settings used for calls to getDatabase. */ - public UnaryCallSettings.Builder getDatabaseSettings() { - return getStubSettingsBuilder().getDatabaseSettings(); - } - /** Returns the builder for the settings used for calls to updateDatabaseDdl. */ public UnaryCallSettings.Builder updateDatabaseDdlSettings() { @@ -367,6 +355,44 @@ public UnaryCallSettings.Builder getDatabaseSettin return getStubSettingsBuilder().updateDatabaseDdlOperationSettings(); } + /** Returns the builder for the settings used for calls to createBackup. */ + public UnaryCallSettings.Builder createBackupSettings() { + return getStubSettingsBuilder().createBackupSettings(); + } + + /** Returns the builder for the settings used for calls to createBackup. */ + @BetaApi( + "The surface for long-running operations is not stable yet and may change in the future.") + public OperationCallSettings.Builder + createBackupOperationSettings() { + return getStubSettingsBuilder().createBackupOperationSettings(); + } + + /** Returns the builder for the settings used for calls to restoreDatabase. */ + public UnaryCallSettings.Builder restoreDatabaseSettings() { + return getStubSettingsBuilder().restoreDatabaseSettings(); + } + + /** Returns the builder for the settings used for calls to restoreDatabase. */ + @BetaApi( + "The surface for long-running operations is not stable yet and may change in the future.") + public OperationCallSettings.Builder + restoreDatabaseOperationSettings() { + return getStubSettingsBuilder().restoreDatabaseOperationSettings(); + } + + /** Returns the builder for the settings used for calls to listDatabases. */ + public PagedCallSettings.Builder< + ListDatabasesRequest, ListDatabasesResponse, ListDatabasesPagedResponse> + listDatabasesSettings() { + return getStubSettingsBuilder().listDatabasesSettings(); + } + + /** Returns the builder for the settings used for calls to getDatabase. */ + public UnaryCallSettings.Builder getDatabaseSettings() { + return getStubSettingsBuilder().getDatabaseSettings(); + } + /** Returns the builder for the settings used for calls to dropDatabase. */ public UnaryCallSettings.Builder dropDatabaseSettings() { return getStubSettingsBuilder().dropDatabaseSettings(); @@ -394,19 +420,6 @@ public UnaryCallSettings.Builder getIamPolicySettin return getStubSettingsBuilder().testIamPermissionsSettings(); } - /** Returns the builder for the settings used for calls to createBackup. */ - public UnaryCallSettings.Builder createBackupSettings() { - return getStubSettingsBuilder().createBackupSettings(); - } - - /** Returns the builder for the settings used for calls to createBackup. */ - @BetaApi( - "The surface for long-running operations is not stable yet and may change in the future.") - public OperationCallSettings.Builder - createBackupOperationSettings() { - return getStubSettingsBuilder().createBackupOperationSettings(); - } - /** Returns the builder for the settings used for calls to getBackup. */ public UnaryCallSettings.Builder getBackupSettings() { return getStubSettingsBuilder().getBackupSettings(); @@ -429,19 +442,6 @@ public UnaryCallSettings.Builder deleteBackupSetting return getStubSettingsBuilder().listBackupsSettings(); } - /** Returns the builder for the settings used for calls to restoreDatabase. */ - public UnaryCallSettings.Builder restoreDatabaseSettings() { - return getStubSettingsBuilder().restoreDatabaseSettings(); - } - - /** Returns the builder for the settings used for calls to restoreDatabase. */ - @BetaApi( - "The surface for long-running operations is not stable yet and may change in the future.") - public OperationCallSettings.Builder - restoreDatabaseOperationSettings() { - return getStubSettingsBuilder().restoreDatabaseOperationSettings(); - } - /** Returns the builder for the settings used for calls to listDatabaseOperations. */ public PagedCallSettings.Builder< ListDatabaseOperationsRequest, diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/admin/database/v1/stub/DatabaseAdminStub.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/admin/database/v1/stub/DatabaseAdminStub.java index fd92157c55..9f060f971a 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/admin/database/v1/stub/DatabaseAdminStub.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/admin/database/v1/stub/DatabaseAdminStub.java @@ -74,15 +74,6 @@ public OperationsStub getOperationsStub() { throw new UnsupportedOperationException("Not implemented: getOperationsStub()"); } - public UnaryCallable - listDatabasesPagedCallable() { - throw new UnsupportedOperationException("Not implemented: listDatabasesPagedCallable()"); - } - - public UnaryCallable listDatabasesCallable() { - throw new UnsupportedOperationException("Not implemented: listDatabasesCallable()"); - } - @BetaApi("The surface for use by generated code is not stable yet and may change in the future.") public OperationCallable createDatabaseOperationCallable() { @@ -93,10 +84,6 @@ public UnaryCallable createDatabaseCallable() throw new UnsupportedOperationException("Not implemented: createDatabaseCallable()"); } - public UnaryCallable getDatabaseCallable() { - throw new UnsupportedOperationException("Not implemented: getDatabaseCallable()"); - } - @BetaApi("The surface for use by generated code is not stable yet and may change in the future.") public OperationCallable updateDatabaseDdlOperationCallable() { @@ -108,6 +95,39 @@ public UnaryCallable updateDatabaseDdlCalla throw new UnsupportedOperationException("Not implemented: updateDatabaseDdlCallable()"); } + @BetaApi("The surface for use by generated code is not stable yet and may change in the future.") + public OperationCallable + createBackupOperationCallable() { + throw new UnsupportedOperationException("Not implemented: createBackupOperationCallable()"); + } + + public UnaryCallable createBackupCallable() { + throw new UnsupportedOperationException("Not implemented: createBackupCallable()"); + } + + @BetaApi("The surface for use by generated code is not stable yet and may change in the future.") + public OperationCallable + restoreDatabaseOperationCallable() { + throw new UnsupportedOperationException("Not implemented: restoreDatabaseOperationCallable()"); + } + + public UnaryCallable restoreDatabaseCallable() { + throw new UnsupportedOperationException("Not implemented: restoreDatabaseCallable()"); + } + + public UnaryCallable + listDatabasesPagedCallable() { + throw new UnsupportedOperationException("Not implemented: listDatabasesPagedCallable()"); + } + + public UnaryCallable listDatabasesCallable() { + throw new UnsupportedOperationException("Not implemented: listDatabasesCallable()"); + } + + public UnaryCallable getDatabaseCallable() { + throw new UnsupportedOperationException("Not implemented: getDatabaseCallable()"); + } + public UnaryCallable dropDatabaseCallable() { throw new UnsupportedOperationException("Not implemented: dropDatabaseCallable()"); } @@ -129,16 +149,6 @@ public UnaryCallable getIamPolicyCallable() { throw new UnsupportedOperationException("Not implemented: testIamPermissionsCallable()"); } - @BetaApi("The surface for use by generated code is not stable yet and may change in the future.") - public OperationCallable - createBackupOperationCallable() { - throw new UnsupportedOperationException("Not implemented: createBackupOperationCallable()"); - } - - public UnaryCallable createBackupCallable() { - throw new UnsupportedOperationException("Not implemented: createBackupCallable()"); - } - public UnaryCallable getBackupCallable() { throw new UnsupportedOperationException("Not implemented: getBackupCallable()"); } @@ -159,16 +169,6 @@ public UnaryCallable listBackupsCallabl throw new UnsupportedOperationException("Not implemented: listBackupsCallable()"); } - @BetaApi("The surface for use by generated code is not stable yet and may change in the future.") - public OperationCallable - restoreDatabaseOperationCallable() { - throw new UnsupportedOperationException("Not implemented: restoreDatabaseOperationCallable()"); - } - - public UnaryCallable restoreDatabaseCallable() { - throw new UnsupportedOperationException("Not implemented: restoreDatabaseCallable()"); - } - public UnaryCallable listDatabaseOperationsPagedCallable() { throw new UnsupportedOperationException( diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/admin/database/v1/stub/DatabaseAdminStubSettings.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/admin/database/v1/stub/DatabaseAdminStubSettings.java index 53d5767a89..7799b21fc3 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/admin/database/v1/stub/DatabaseAdminStubSettings.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/admin/database/v1/stub/DatabaseAdminStubSettings.java @@ -128,16 +128,22 @@ public class DatabaseAdminStubSettings extends StubSettings - listDatabasesSettings; private final UnaryCallSettings createDatabaseSettings; private final OperationCallSettings createDatabaseOperationSettings; - private final UnaryCallSettings getDatabaseSettings; private final UnaryCallSettings updateDatabaseDdlSettings; private final OperationCallSettings updateDatabaseDdlOperationSettings; + private final UnaryCallSettings createBackupSettings; + private final OperationCallSettings + createBackupOperationSettings; + private final UnaryCallSettings restoreDatabaseSettings; + private final OperationCallSettings + restoreDatabaseOperationSettings; + private final PagedCallSettings< + ListDatabasesRequest, ListDatabasesResponse, ListDatabasesPagedResponse> + listDatabasesSettings; + private final UnaryCallSettings getDatabaseSettings; private final UnaryCallSettings dropDatabaseSettings; private final UnaryCallSettings getDatabaseDdlSettings; @@ -145,17 +151,11 @@ public class DatabaseAdminStubSettings extends StubSettings getIamPolicySettings; private final UnaryCallSettings testIamPermissionsSettings; - private final UnaryCallSettings createBackupSettings; - private final OperationCallSettings - createBackupOperationSettings; private final UnaryCallSettings getBackupSettings; private final UnaryCallSettings updateBackupSettings; private final UnaryCallSettings deleteBackupSettings; private final PagedCallSettings listBackupsSettings; - private final UnaryCallSettings restoreDatabaseSettings; - private final OperationCallSettings - restoreDatabaseOperationSettings; private final PagedCallSettings< ListDatabaseOperationsRequest, ListDatabaseOperationsResponse, @@ -167,12 +167,6 @@ public class DatabaseAdminStubSettings extends StubSettings listBackupOperationsSettings; - /** Returns the object with the settings used for calls to listDatabases. */ - public PagedCallSettings - listDatabasesSettings() { - return listDatabasesSettings; - } - /** Returns the object with the settings used for calls to createDatabase. */ public UnaryCallSettings createDatabaseSettings() { return createDatabaseSettings; @@ -185,11 +179,6 @@ public UnaryCallSettings createDatabaseSetting return createDatabaseOperationSettings; } - /** Returns the object with the settings used for calls to getDatabase. */ - public UnaryCallSettings getDatabaseSettings() { - return getDatabaseSettings; - } - /** Returns the object with the settings used for calls to updateDatabaseDdl. */ public UnaryCallSettings updateDatabaseDdlSettings() { return updateDatabaseDdlSettings; @@ -202,6 +191,41 @@ public UnaryCallSettings updateDatabaseDdlS return updateDatabaseDdlOperationSettings; } + /** Returns the object with the settings used for calls to createBackup. */ + public UnaryCallSettings createBackupSettings() { + return createBackupSettings; + } + + /** Returns the object with the settings used for calls to createBackup. */ + @BetaApi("The surface for use by generated code is not stable yet and may change in the future.") + public OperationCallSettings + createBackupOperationSettings() { + return createBackupOperationSettings; + } + + /** Returns the object with the settings used for calls to restoreDatabase. */ + public UnaryCallSettings restoreDatabaseSettings() { + return restoreDatabaseSettings; + } + + /** Returns the object with the settings used for calls to restoreDatabase. */ + @BetaApi("The surface for use by generated code is not stable yet and may change in the future.") + public OperationCallSettings + restoreDatabaseOperationSettings() { + return restoreDatabaseOperationSettings; + } + + /** Returns the object with the settings used for calls to listDatabases. */ + public PagedCallSettings + listDatabasesSettings() { + return listDatabasesSettings; + } + + /** Returns the object with the settings used for calls to getDatabase. */ + public UnaryCallSettings getDatabaseSettings() { + return getDatabaseSettings; + } + /** Returns the object with the settings used for calls to dropDatabase. */ public UnaryCallSettings dropDatabaseSettings() { return dropDatabaseSettings; @@ -228,18 +252,6 @@ public UnaryCallSettings getIamPolicySettings() { return testIamPermissionsSettings; } - /** Returns the object with the settings used for calls to createBackup. */ - public UnaryCallSettings createBackupSettings() { - return createBackupSettings; - } - - /** Returns the object with the settings used for calls to createBackup. */ - @BetaApi("The surface for use by generated code is not stable yet and may change in the future.") - public OperationCallSettings - createBackupOperationSettings() { - return createBackupOperationSettings; - } - /** Returns the object with the settings used for calls to getBackup. */ public UnaryCallSettings getBackupSettings() { return getBackupSettings; @@ -261,18 +273,6 @@ public UnaryCallSettings deleteBackupSettings() { return listBackupsSettings; } - /** Returns the object with the settings used for calls to restoreDatabase. */ - public UnaryCallSettings restoreDatabaseSettings() { - return restoreDatabaseSettings; - } - - /** Returns the object with the settings used for calls to restoreDatabase. */ - @BetaApi("The surface for use by generated code is not stable yet and may change in the future.") - public OperationCallSettings - restoreDatabaseOperationSettings() { - return restoreDatabaseOperationSettings; - } - /** Returns the object with the settings used for calls to listDatabaseOperations. */ public PagedCallSettings< ListDatabaseOperationsRequest, @@ -360,26 +360,26 @@ public Builder toBuilder() { protected DatabaseAdminStubSettings(Builder settingsBuilder) throws IOException { super(settingsBuilder); - listDatabasesSettings = settingsBuilder.listDatabasesSettings().build(); createDatabaseSettings = settingsBuilder.createDatabaseSettings().build(); createDatabaseOperationSettings = settingsBuilder.createDatabaseOperationSettings().build(); - getDatabaseSettings = settingsBuilder.getDatabaseSettings().build(); updateDatabaseDdlSettings = settingsBuilder.updateDatabaseDdlSettings().build(); updateDatabaseDdlOperationSettings = settingsBuilder.updateDatabaseDdlOperationSettings().build(); + createBackupSettings = settingsBuilder.createBackupSettings().build(); + createBackupOperationSettings = settingsBuilder.createBackupOperationSettings().build(); + restoreDatabaseSettings = settingsBuilder.restoreDatabaseSettings().build(); + restoreDatabaseOperationSettings = settingsBuilder.restoreDatabaseOperationSettings().build(); + listDatabasesSettings = settingsBuilder.listDatabasesSettings().build(); + getDatabaseSettings = settingsBuilder.getDatabaseSettings().build(); dropDatabaseSettings = settingsBuilder.dropDatabaseSettings().build(); getDatabaseDdlSettings = settingsBuilder.getDatabaseDdlSettings().build(); setIamPolicySettings = settingsBuilder.setIamPolicySettings().build(); getIamPolicySettings = settingsBuilder.getIamPolicySettings().build(); testIamPermissionsSettings = settingsBuilder.testIamPermissionsSettings().build(); - createBackupSettings = settingsBuilder.createBackupSettings().build(); - createBackupOperationSettings = settingsBuilder.createBackupOperationSettings().build(); getBackupSettings = settingsBuilder.getBackupSettings().build(); updateBackupSettings = settingsBuilder.updateBackupSettings().build(); deleteBackupSettings = settingsBuilder.deleteBackupSettings().build(); listBackupsSettings = settingsBuilder.listBackupsSettings().build(); - restoreDatabaseSettings = settingsBuilder.restoreDatabaseSettings().build(); - restoreDatabaseOperationSettings = settingsBuilder.restoreDatabaseOperationSettings().build(); listDatabaseOperationsSettings = settingsBuilder.listDatabaseOperationsSettings().build(); listBackupOperationsSettings = settingsBuilder.listBackupOperationsSettings().build(); } @@ -623,20 +623,28 @@ public ApiFuture getFuturePagedResponse( public static class Builder extends StubSettings.Builder { private final ImmutableList> unaryMethodSettingsBuilders; - private final PagedCallSettings.Builder< - ListDatabasesRequest, ListDatabasesResponse, ListDatabasesPagedResponse> - listDatabasesSettings; private final UnaryCallSettings.Builder createDatabaseSettings; private final OperationCallSettings.Builder< CreateDatabaseRequest, Database, CreateDatabaseMetadata> createDatabaseOperationSettings; - private final UnaryCallSettings.Builder getDatabaseSettings; private final UnaryCallSettings.Builder updateDatabaseDdlSettings; private final OperationCallSettings.Builder< UpdateDatabaseDdlRequest, Empty, UpdateDatabaseDdlMetadata> updateDatabaseDdlOperationSettings; + private final UnaryCallSettings.Builder createBackupSettings; + private final OperationCallSettings.Builder + createBackupOperationSettings; + private final UnaryCallSettings.Builder + restoreDatabaseSettings; + private final OperationCallSettings.Builder< + RestoreDatabaseRequest, Database, RestoreDatabaseMetadata> + restoreDatabaseOperationSettings; + private final PagedCallSettings.Builder< + ListDatabasesRequest, ListDatabasesResponse, ListDatabasesPagedResponse> + listDatabasesSettings; + private final UnaryCallSettings.Builder getDatabaseSettings; private final UnaryCallSettings.Builder dropDatabaseSettings; private final UnaryCallSettings.Builder getDatabaseDdlSettings; @@ -644,20 +652,12 @@ public static class Builder extends StubSettings.Builder getIamPolicySettings; private final UnaryCallSettings.Builder testIamPermissionsSettings; - private final UnaryCallSettings.Builder createBackupSettings; - private final OperationCallSettings.Builder - createBackupOperationSettings; private final UnaryCallSettings.Builder getBackupSettings; private final UnaryCallSettings.Builder updateBackupSettings; private final UnaryCallSettings.Builder deleteBackupSettings; private final PagedCallSettings.Builder< ListBackupsRequest, ListBackupsResponse, ListBackupsPagedResponse> listBackupsSettings; - private final UnaryCallSettings.Builder - restoreDatabaseSettings; - private final OperationCallSettings.Builder< - RestoreDatabaseRequest, Database, RestoreDatabaseMetadata> - restoreDatabaseOperationSettings; private final PagedCallSettings.Builder< ListDatabaseOperationsRequest, ListDatabaseOperationsResponse, @@ -676,11 +676,20 @@ public static class Builder extends StubSettings.Builder> definitions = ImmutableMap.builder(); definitions.put( - "idempotent", + "retry_policy_1_codes", + ImmutableSet.copyOf( + Lists.newArrayList( + StatusCode.Code.UNAVAILABLE, StatusCode.Code.DEADLINE_EXCEEDED))); + definitions.put( + "no_retry_2_codes", ImmutableSet.copyOf(Lists.newArrayList())); + definitions.put("no_retry_codes", ImmutableSet.copyOf(Lists.newArrayList())); + definitions.put( + "retry_policy_2_codes", ImmutableSet.copyOf( Lists.newArrayList( - StatusCode.Code.DEADLINE_EXCEEDED, StatusCode.Code.UNAVAILABLE))); - definitions.put("non_idempotent", ImmutableSet.copyOf(Lists.newArrayList())); + StatusCode.Code.UNAVAILABLE, StatusCode.Code.DEADLINE_EXCEEDED))); + definitions.put( + "no_retry_1_codes", ImmutableSet.copyOf(Lists.newArrayList())); RETRYABLE_CODE_DEFINITIONS = definitions.build(); } @@ -694,12 +703,41 @@ public static class Builder extends StubSettings.Builder>of( - listDatabasesSettings, createDatabaseSettings, - getDatabaseSettings, updateDatabaseDdlSettings, + createBackupSettings, + restoreDatabaseSettings, + listDatabasesSettings, + getDatabaseSettings, dropDatabaseSettings, getDatabaseDdlSettings, setIamPolicySettings, getIamPolicySettings, testIamPermissionsSettings, - createBackupSettings, getBackupSettings, updateBackupSettings, deleteBackupSettings, listBackupsSettings, - restoreDatabaseSettings, listDatabaseOperationsSettings, listBackupOperationsSettings); @@ -789,96 +827,96 @@ private static Builder createDefault() { private static Builder initDefaults(Builder builder) { builder - .listDatabasesSettings() - .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("idempotent")) - .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("default")); + .createDatabaseSettings() + .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("no_retry_1_codes")) + .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("no_retry_1_params")); builder - .createDatabaseSettings() - .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("non_idempotent")) - .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("default")); + .updateDatabaseDdlSettings() + .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("retry_policy_1_codes")) + .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("retry_policy_1_params")); builder - .getDatabaseSettings() - .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("idempotent")) - .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("default")); + .createBackupSettings() + .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("no_retry_1_codes")) + .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("no_retry_1_params")); builder - .updateDatabaseDdlSettings() - .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("idempotent")) - .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("default")); + .restoreDatabaseSettings() + .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("no_retry_1_codes")) + .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("no_retry_1_params")); + + builder + .listDatabasesSettings() + .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("retry_policy_1_codes")) + .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("retry_policy_1_params")); + + builder + .getDatabaseSettings() + .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("retry_policy_1_codes")) + .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("retry_policy_1_params")); builder .dropDatabaseSettings() - .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("idempotent")) - .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("default")); + .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("retry_policy_1_codes")) + .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("retry_policy_1_params")); builder .getDatabaseDdlSettings() - .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("idempotent")) - .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("default")); + .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("retry_policy_1_codes")) + .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("retry_policy_1_params")); builder .setIamPolicySettings() - .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("non_idempotent")) - .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("default")); + .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("no_retry_2_codes")) + .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("no_retry_2_params")); builder .getIamPolicySettings() - .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("idempotent")) - .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("default")); + .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("retry_policy_2_codes")) + .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("retry_policy_2_params")); builder .testIamPermissionsSettings() - .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("non_idempotent")) - .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("default")); - - builder - .createBackupSettings() - .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("non_idempotent")) - .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("default")); + .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("no_retry_2_codes")) + .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("no_retry_2_params")); builder .getBackupSettings() - .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("idempotent")) - .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("default")); + .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("retry_policy_1_codes")) + .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("retry_policy_1_params")); builder .updateBackupSettings() - .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("non_idempotent")) - .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("default")); + .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("no_retry_1_codes")) + .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("no_retry_1_params")); builder .deleteBackupSettings() - .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("idempotent")) - .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("default")); + .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("retry_policy_1_codes")) + .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("retry_policy_1_params")); builder .listBackupsSettings() - .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("idempotent")) - .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("default")); - - builder - .restoreDatabaseSettings() - .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("non_idempotent")) - .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("default")); + .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("retry_policy_1_codes")) + .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("retry_policy_1_params")); builder .listDatabaseOperationsSettings() - .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("idempotent")) - .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("default")); + .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("retry_policy_1_codes")) + .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("retry_policy_1_params")); builder .listBackupOperationsSettings() - .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("idempotent")) - .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("default")); + .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("retry_policy_1_codes")) + .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("retry_policy_1_params")); builder .createDatabaseOperationSettings() .setInitialCallSettings( UnaryCallSettings .newUnaryCallSettingsBuilder() - .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("non_idempotent")) - .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("default")) + .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("no_retry_1_codes")) + .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("no_retry_1_params")) .build()) .setResponseTransformer( ProtoOperationTransformers.ResponseTransformer.create(Database.class)) @@ -900,8 +938,8 @@ private static Builder initDefaults(Builder builder) { .setInitialCallSettings( UnaryCallSettings .newUnaryCallSettingsBuilder() - .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("idempotent")) - .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("default")) + .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("retry_policy_1_codes")) + .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("retry_policy_1_params")) .build()) .setResponseTransformer( ProtoOperationTransformers.ResponseTransformer.create(Empty.class)) @@ -924,8 +962,8 @@ private static Builder initDefaults(Builder builder) { .setInitialCallSettings( UnaryCallSettings .newUnaryCallSettingsBuilder() - .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("non_idempotent")) - .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("default")) + .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("no_retry_1_codes")) + .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("no_retry_1_params")) .build()) .setResponseTransformer( ProtoOperationTransformers.ResponseTransformer.create(Backup.class)) @@ -947,8 +985,8 @@ private static Builder initDefaults(Builder builder) { .setInitialCallSettings( UnaryCallSettings .newUnaryCallSettingsBuilder() - .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("non_idempotent")) - .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("default")) + .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("no_retry_1_codes")) + .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("no_retry_1_params")) .build()) .setResponseTransformer( ProtoOperationTransformers.ResponseTransformer.create(Database.class)) @@ -972,45 +1010,45 @@ private static Builder initDefaults(Builder builder) { protected Builder(DatabaseAdminStubSettings settings) { super(settings); - listDatabasesSettings = settings.listDatabasesSettings.toBuilder(); createDatabaseSettings = settings.createDatabaseSettings.toBuilder(); createDatabaseOperationSettings = settings.createDatabaseOperationSettings.toBuilder(); - getDatabaseSettings = settings.getDatabaseSettings.toBuilder(); updateDatabaseDdlSettings = settings.updateDatabaseDdlSettings.toBuilder(); updateDatabaseDdlOperationSettings = settings.updateDatabaseDdlOperationSettings.toBuilder(); + createBackupSettings = settings.createBackupSettings.toBuilder(); + createBackupOperationSettings = settings.createBackupOperationSettings.toBuilder(); + restoreDatabaseSettings = settings.restoreDatabaseSettings.toBuilder(); + restoreDatabaseOperationSettings = settings.restoreDatabaseOperationSettings.toBuilder(); + listDatabasesSettings = settings.listDatabasesSettings.toBuilder(); + getDatabaseSettings = settings.getDatabaseSettings.toBuilder(); dropDatabaseSettings = settings.dropDatabaseSettings.toBuilder(); getDatabaseDdlSettings = settings.getDatabaseDdlSettings.toBuilder(); setIamPolicySettings = settings.setIamPolicySettings.toBuilder(); getIamPolicySettings = settings.getIamPolicySettings.toBuilder(); testIamPermissionsSettings = settings.testIamPermissionsSettings.toBuilder(); - createBackupSettings = settings.createBackupSettings.toBuilder(); - createBackupOperationSettings = settings.createBackupOperationSettings.toBuilder(); getBackupSettings = settings.getBackupSettings.toBuilder(); updateBackupSettings = settings.updateBackupSettings.toBuilder(); deleteBackupSettings = settings.deleteBackupSettings.toBuilder(); listBackupsSettings = settings.listBackupsSettings.toBuilder(); - restoreDatabaseSettings = settings.restoreDatabaseSettings.toBuilder(); - restoreDatabaseOperationSettings = settings.restoreDatabaseOperationSettings.toBuilder(); listDatabaseOperationsSettings = settings.listDatabaseOperationsSettings.toBuilder(); listBackupOperationsSettings = settings.listBackupOperationsSettings.toBuilder(); unaryMethodSettingsBuilders = ImmutableList.>of( - listDatabasesSettings, createDatabaseSettings, - getDatabaseSettings, updateDatabaseDdlSettings, + createBackupSettings, + restoreDatabaseSettings, + listDatabasesSettings, + getDatabaseSettings, dropDatabaseSettings, getDatabaseDdlSettings, setIamPolicySettings, getIamPolicySettings, testIamPermissionsSettings, - createBackupSettings, getBackupSettings, updateBackupSettings, deleteBackupSettings, listBackupsSettings, - restoreDatabaseSettings, listDatabaseOperationsSettings, listBackupOperationsSettings); } @@ -1031,13 +1069,6 @@ public Builder applyToAllUnaryMethods( return unaryMethodSettingsBuilders; } - /** Returns the builder for the settings used for calls to listDatabases. */ - public PagedCallSettings.Builder< - ListDatabasesRequest, ListDatabasesResponse, ListDatabasesPagedResponse> - listDatabasesSettings() { - return listDatabasesSettings; - } - /** Returns the builder for the settings used for calls to createDatabase. */ public UnaryCallSettings.Builder createDatabaseSettings() { return createDatabaseSettings; @@ -1051,11 +1082,6 @@ public UnaryCallSettings.Builder createDatabas return createDatabaseOperationSettings; } - /** Returns the builder for the settings used for calls to getDatabase. */ - public UnaryCallSettings.Builder getDatabaseSettings() { - return getDatabaseSettings; - } - /** Returns the builder for the settings used for calls to updateDatabaseDdl. */ public UnaryCallSettings.Builder updateDatabaseDdlSettings() { @@ -1070,6 +1096,44 @@ public UnaryCallSettings.Builder getDatabaseSettin return updateDatabaseDdlOperationSettings; } + /** Returns the builder for the settings used for calls to createBackup. */ + public UnaryCallSettings.Builder createBackupSettings() { + return createBackupSettings; + } + + /** Returns the builder for the settings used for calls to createBackup. */ + @BetaApi( + "The surface for use by generated code is not stable yet and may change in the future.") + public OperationCallSettings.Builder + createBackupOperationSettings() { + return createBackupOperationSettings; + } + + /** Returns the builder for the settings used for calls to restoreDatabase. */ + public UnaryCallSettings.Builder restoreDatabaseSettings() { + return restoreDatabaseSettings; + } + + /** Returns the builder for the settings used for calls to restoreDatabase. */ + @BetaApi( + "The surface for use by generated code is not stable yet and may change in the future.") + public OperationCallSettings.Builder + restoreDatabaseOperationSettings() { + return restoreDatabaseOperationSettings; + } + + /** Returns the builder for the settings used for calls to listDatabases. */ + public PagedCallSettings.Builder< + ListDatabasesRequest, ListDatabasesResponse, ListDatabasesPagedResponse> + listDatabasesSettings() { + return listDatabasesSettings; + } + + /** Returns the builder for the settings used for calls to getDatabase. */ + public UnaryCallSettings.Builder getDatabaseSettings() { + return getDatabaseSettings; + } + /** Returns the builder for the settings used for calls to dropDatabase. */ public UnaryCallSettings.Builder dropDatabaseSettings() { return dropDatabaseSettings; @@ -1097,19 +1161,6 @@ public UnaryCallSettings.Builder getIamPolicySettin return testIamPermissionsSettings; } - /** Returns the builder for the settings used for calls to createBackup. */ - public UnaryCallSettings.Builder createBackupSettings() { - return createBackupSettings; - } - - /** Returns the builder for the settings used for calls to createBackup. */ - @BetaApi( - "The surface for use by generated code is not stable yet and may change in the future.") - public OperationCallSettings.Builder - createBackupOperationSettings() { - return createBackupOperationSettings; - } - /** Returns the builder for the settings used for calls to getBackup. */ public UnaryCallSettings.Builder getBackupSettings() { return getBackupSettings; @@ -1132,19 +1183,6 @@ public UnaryCallSettings.Builder deleteBackupSetting return listBackupsSettings; } - /** Returns the builder for the settings used for calls to restoreDatabase. */ - public UnaryCallSettings.Builder restoreDatabaseSettings() { - return restoreDatabaseSettings; - } - - /** Returns the builder for the settings used for calls to restoreDatabase. */ - @BetaApi( - "The surface for use by generated code is not stable yet and may change in the future.") - public OperationCallSettings.Builder - restoreDatabaseOperationSettings() { - return restoreDatabaseOperationSettings; - } - /** Returns the builder for the settings used for calls to listDatabaseOperations. */ public PagedCallSettings.Builder< ListDatabaseOperationsRequest, diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/admin/database/v1/stub/GrpcDatabaseAdminStub.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/admin/database/v1/stub/GrpcDatabaseAdminStub.java index aaf318ca90..c3d8b39713 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/admin/database/v1/stub/GrpcDatabaseAdminStub.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/admin/database/v1/stub/GrpcDatabaseAdminStub.java @@ -80,16 +80,6 @@ @BetaApi("A restructuring of stub classes is planned, so this may break in the future") public class GrpcDatabaseAdminStub extends DatabaseAdminStub { - private static final MethodDescriptor - listDatabasesMethodDescriptor = - MethodDescriptor.newBuilder() - .setType(MethodDescriptor.MethodType.UNARY) - .setFullMethodName("google.spanner.admin.database.v1.DatabaseAdmin/ListDatabases") - .setRequestMarshaller( - ProtoUtils.marshaller(ListDatabasesRequest.getDefaultInstance())) - .setResponseMarshaller( - ProtoUtils.marshaller(ListDatabasesResponse.getDefaultInstance())) - .build(); private static final MethodDescriptor createDatabaseMethodDescriptor = MethodDescriptor.newBuilder() @@ -99,13 +89,6 @@ public class GrpcDatabaseAdminStub extends DatabaseAdminStub { ProtoUtils.marshaller(CreateDatabaseRequest.getDefaultInstance())) .setResponseMarshaller(ProtoUtils.marshaller(Operation.getDefaultInstance())) .build(); - private static final MethodDescriptor getDatabaseMethodDescriptor = - MethodDescriptor.newBuilder() - .setType(MethodDescriptor.MethodType.UNARY) - .setFullMethodName("google.spanner.admin.database.v1.DatabaseAdmin/GetDatabase") - .setRequestMarshaller(ProtoUtils.marshaller(GetDatabaseRequest.getDefaultInstance())) - .setResponseMarshaller(ProtoUtils.marshaller(Database.getDefaultInstance())) - .build(); private static final MethodDescriptor updateDatabaseDdlMethodDescriptor = MethodDescriptor.newBuilder() @@ -115,6 +98,40 @@ public class GrpcDatabaseAdminStub extends DatabaseAdminStub { ProtoUtils.marshaller(UpdateDatabaseDdlRequest.getDefaultInstance())) .setResponseMarshaller(ProtoUtils.marshaller(Operation.getDefaultInstance())) .build(); + private static final MethodDescriptor + createBackupMethodDescriptor = + MethodDescriptor.newBuilder() + .setType(MethodDescriptor.MethodType.UNARY) + .setFullMethodName("google.spanner.admin.database.v1.DatabaseAdmin/CreateBackup") + .setRequestMarshaller(ProtoUtils.marshaller(CreateBackupRequest.getDefaultInstance())) + .setResponseMarshaller(ProtoUtils.marshaller(Operation.getDefaultInstance())) + .build(); + private static final MethodDescriptor + restoreDatabaseMethodDescriptor = + MethodDescriptor.newBuilder() + .setType(MethodDescriptor.MethodType.UNARY) + .setFullMethodName("google.spanner.admin.database.v1.DatabaseAdmin/RestoreDatabase") + .setRequestMarshaller( + ProtoUtils.marshaller(RestoreDatabaseRequest.getDefaultInstance())) + .setResponseMarshaller(ProtoUtils.marshaller(Operation.getDefaultInstance())) + .build(); + private static final MethodDescriptor + listDatabasesMethodDescriptor = + MethodDescriptor.newBuilder() + .setType(MethodDescriptor.MethodType.UNARY) + .setFullMethodName("google.spanner.admin.database.v1.DatabaseAdmin/ListDatabases") + .setRequestMarshaller( + ProtoUtils.marshaller(ListDatabasesRequest.getDefaultInstance())) + .setResponseMarshaller( + ProtoUtils.marshaller(ListDatabasesResponse.getDefaultInstance())) + .build(); + private static final MethodDescriptor getDatabaseMethodDescriptor = + MethodDescriptor.newBuilder() + .setType(MethodDescriptor.MethodType.UNARY) + .setFullMethodName("google.spanner.admin.database.v1.DatabaseAdmin/GetDatabase") + .setRequestMarshaller(ProtoUtils.marshaller(GetDatabaseRequest.getDefaultInstance())) + .setResponseMarshaller(ProtoUtils.marshaller(Database.getDefaultInstance())) + .build(); private static final MethodDescriptor dropDatabaseMethodDescriptor = MethodDescriptor.newBuilder() .setType(MethodDescriptor.MethodType.UNARY) @@ -157,14 +174,6 @@ public class GrpcDatabaseAdminStub extends DatabaseAdminStub { .setResponseMarshaller( ProtoUtils.marshaller(TestIamPermissionsResponse.getDefaultInstance())) .build(); - private static final MethodDescriptor - createBackupMethodDescriptor = - MethodDescriptor.newBuilder() - .setType(MethodDescriptor.MethodType.UNARY) - .setFullMethodName("google.spanner.admin.database.v1.DatabaseAdmin/CreateBackup") - .setRequestMarshaller(ProtoUtils.marshaller(CreateBackupRequest.getDefaultInstance())) - .setResponseMarshaller(ProtoUtils.marshaller(Operation.getDefaultInstance())) - .build(); private static final MethodDescriptor getBackupMethodDescriptor = MethodDescriptor.newBuilder() .setType(MethodDescriptor.MethodType.UNARY) @@ -195,15 +204,6 @@ public class GrpcDatabaseAdminStub extends DatabaseAdminStub { .setResponseMarshaller( ProtoUtils.marshaller(ListBackupsResponse.getDefaultInstance())) .build(); - private static final MethodDescriptor - restoreDatabaseMethodDescriptor = - MethodDescriptor.newBuilder() - .setType(MethodDescriptor.MethodType.UNARY) - .setFullMethodName("google.spanner.admin.database.v1.DatabaseAdmin/RestoreDatabase") - .setRequestMarshaller( - ProtoUtils.marshaller(RestoreDatabaseRequest.getDefaultInstance())) - .setResponseMarshaller(ProtoUtils.marshaller(Operation.getDefaultInstance())) - .build(); private static final MethodDescriptor< ListDatabaseOperationsRequest, ListDatabaseOperationsResponse> listDatabaseOperationsMethodDescriptor = @@ -232,34 +232,34 @@ public class GrpcDatabaseAdminStub extends DatabaseAdminStub { private final BackgroundResource backgroundResources; private final GrpcOperationsStub operationsStub; - private final UnaryCallable listDatabasesCallable; - private final UnaryCallable - listDatabasesPagedCallable; private final UnaryCallable createDatabaseCallable; private final OperationCallable createDatabaseOperationCallable; - private final UnaryCallable getDatabaseCallable; private final UnaryCallable updateDatabaseDdlCallable; private final OperationCallable updateDatabaseDdlOperationCallable; + private final UnaryCallable createBackupCallable; + private final OperationCallable + createBackupOperationCallable; + private final UnaryCallable restoreDatabaseCallable; + private final OperationCallable + restoreDatabaseOperationCallable; + private final UnaryCallable listDatabasesCallable; + private final UnaryCallable + listDatabasesPagedCallable; + private final UnaryCallable getDatabaseCallable; private final UnaryCallable dropDatabaseCallable; private final UnaryCallable getDatabaseDdlCallable; private final UnaryCallable setIamPolicyCallable; private final UnaryCallable getIamPolicyCallable; private final UnaryCallable testIamPermissionsCallable; - private final UnaryCallable createBackupCallable; - private final OperationCallable - createBackupOperationCallable; private final UnaryCallable getBackupCallable; private final UnaryCallable updateBackupCallable; private final UnaryCallable deleteBackupCallable; private final UnaryCallable listBackupsCallable; private final UnaryCallable listBackupsPagedCallable; - private final UnaryCallable restoreDatabaseCallable; - private final OperationCallable - restoreDatabaseOperationCallable; private final UnaryCallable listDatabaseOperationsCallable; private final UnaryCallable @@ -309,54 +309,80 @@ protected GrpcDatabaseAdminStub( this.callableFactory = callableFactory; this.operationsStub = GrpcOperationsStub.create(clientContext, callableFactory); - GrpcCallSettings listDatabasesTransportSettings = - GrpcCallSettings.newBuilder() - .setMethodDescriptor(listDatabasesMethodDescriptor) + GrpcCallSettings createDatabaseTransportSettings = + GrpcCallSettings.newBuilder() + .setMethodDescriptor(createDatabaseMethodDescriptor) .setParamsExtractor( - new RequestParamsExtractor() { + new RequestParamsExtractor() { @Override - public Map extract(ListDatabasesRequest request) { + public Map extract(CreateDatabaseRequest request) { ImmutableMap.Builder params = ImmutableMap.builder(); params.put("parent", String.valueOf(request.getParent())); return params.build(); } }) .build(); - GrpcCallSettings createDatabaseTransportSettings = - GrpcCallSettings.newBuilder() - .setMethodDescriptor(createDatabaseMethodDescriptor) + GrpcCallSettings updateDatabaseDdlTransportSettings = + GrpcCallSettings.newBuilder() + .setMethodDescriptor(updateDatabaseDdlMethodDescriptor) .setParamsExtractor( - new RequestParamsExtractor() { + new RequestParamsExtractor() { @Override - public Map extract(CreateDatabaseRequest request) { + public Map extract(UpdateDatabaseDdlRequest request) { + ImmutableMap.Builder params = ImmutableMap.builder(); + params.put("database", String.valueOf(request.getDatabase())); + return params.build(); + } + }) + .build(); + GrpcCallSettings createBackupTransportSettings = + GrpcCallSettings.newBuilder() + .setMethodDescriptor(createBackupMethodDescriptor) + .setParamsExtractor( + new RequestParamsExtractor() { + @Override + public Map extract(CreateBackupRequest request) { ImmutableMap.Builder params = ImmutableMap.builder(); params.put("parent", String.valueOf(request.getParent())); return params.build(); } }) .build(); - GrpcCallSettings getDatabaseTransportSettings = - GrpcCallSettings.newBuilder() - .setMethodDescriptor(getDatabaseMethodDescriptor) + GrpcCallSettings restoreDatabaseTransportSettings = + GrpcCallSettings.newBuilder() + .setMethodDescriptor(restoreDatabaseMethodDescriptor) .setParamsExtractor( - new RequestParamsExtractor() { + new RequestParamsExtractor() { @Override - public Map extract(GetDatabaseRequest request) { + public Map extract(RestoreDatabaseRequest request) { ImmutableMap.Builder params = ImmutableMap.builder(); - params.put("name", String.valueOf(request.getName())); + params.put("parent", String.valueOf(request.getParent())); return params.build(); } }) .build(); - GrpcCallSettings updateDatabaseDdlTransportSettings = - GrpcCallSettings.newBuilder() - .setMethodDescriptor(updateDatabaseDdlMethodDescriptor) + GrpcCallSettings listDatabasesTransportSettings = + GrpcCallSettings.newBuilder() + .setMethodDescriptor(listDatabasesMethodDescriptor) .setParamsExtractor( - new RequestParamsExtractor() { + new RequestParamsExtractor() { @Override - public Map extract(UpdateDatabaseDdlRequest request) { + public Map extract(ListDatabasesRequest request) { ImmutableMap.Builder params = ImmutableMap.builder(); - params.put("database", String.valueOf(request.getDatabase())); + params.put("parent", String.valueOf(request.getParent())); + return params.build(); + } + }) + .build(); + GrpcCallSettings getDatabaseTransportSettings = + GrpcCallSettings.newBuilder() + .setMethodDescriptor(getDatabaseMethodDescriptor) + .setParamsExtractor( + new RequestParamsExtractor() { + @Override + public Map extract(GetDatabaseRequest request) { + ImmutableMap.Builder params = ImmutableMap.builder(); + params.put("name", String.valueOf(request.getName())); return params.build(); } }) @@ -428,19 +454,6 @@ public Map extract(TestIamPermissionsRequest request) { } }) .build(); - GrpcCallSettings createBackupTransportSettings = - GrpcCallSettings.newBuilder() - .setMethodDescriptor(createBackupMethodDescriptor) - .setParamsExtractor( - new RequestParamsExtractor() { - @Override - public Map extract(CreateBackupRequest request) { - ImmutableMap.Builder params = ImmutableMap.builder(); - params.put("parent", String.valueOf(request.getParent())); - return params.build(); - } - }) - .build(); GrpcCallSettings getBackupTransportSettings = GrpcCallSettings.newBuilder() .setMethodDescriptor(getBackupMethodDescriptor) @@ -493,19 +506,6 @@ public Map extract(ListBackupsRequest request) { } }) .build(); - GrpcCallSettings restoreDatabaseTransportSettings = - GrpcCallSettings.newBuilder() - .setMethodDescriptor(restoreDatabaseMethodDescriptor) - .setParamsExtractor( - new RequestParamsExtractor() { - @Override - public Map extract(RestoreDatabaseRequest request) { - ImmutableMap.Builder params = ImmutableMap.builder(); - params.put("parent", String.valueOf(request.getParent())); - return params.build(); - } - }) - .build(); GrpcCallSettings listDatabaseOperationsTransportSettings = GrpcCallSettings @@ -536,12 +536,6 @@ public Map extract(ListBackupOperationsRequest request) { }) .build(); - this.listDatabasesCallable = - callableFactory.createUnaryCallable( - listDatabasesTransportSettings, settings.listDatabasesSettings(), clientContext); - this.listDatabasesPagedCallable = - callableFactory.createPagedCallable( - listDatabasesTransportSettings, settings.listDatabasesSettings(), clientContext); this.createDatabaseCallable = callableFactory.createUnaryCallable( createDatabaseTransportSettings, settings.createDatabaseSettings(), clientContext); @@ -551,9 +545,6 @@ public Map extract(ListBackupOperationsRequest request) { settings.createDatabaseOperationSettings(), clientContext, this.operationsStub); - this.getDatabaseCallable = - callableFactory.createUnaryCallable( - getDatabaseTransportSettings, settings.getDatabaseSettings(), clientContext); this.updateDatabaseDdlCallable = callableFactory.createUnaryCallable( updateDatabaseDdlTransportSettings, @@ -565,6 +556,33 @@ public Map extract(ListBackupOperationsRequest request) { settings.updateDatabaseDdlOperationSettings(), clientContext, this.operationsStub); + this.createBackupCallable = + callableFactory.createUnaryCallable( + createBackupTransportSettings, settings.createBackupSettings(), clientContext); + this.createBackupOperationCallable = + callableFactory.createOperationCallable( + createBackupTransportSettings, + settings.createBackupOperationSettings(), + clientContext, + this.operationsStub); + this.restoreDatabaseCallable = + callableFactory.createUnaryCallable( + restoreDatabaseTransportSettings, settings.restoreDatabaseSettings(), clientContext); + this.restoreDatabaseOperationCallable = + callableFactory.createOperationCallable( + restoreDatabaseTransportSettings, + settings.restoreDatabaseOperationSettings(), + clientContext, + this.operationsStub); + this.listDatabasesCallable = + callableFactory.createUnaryCallable( + listDatabasesTransportSettings, settings.listDatabasesSettings(), clientContext); + this.listDatabasesPagedCallable = + callableFactory.createPagedCallable( + listDatabasesTransportSettings, settings.listDatabasesSettings(), clientContext); + this.getDatabaseCallable = + callableFactory.createUnaryCallable( + getDatabaseTransportSettings, settings.getDatabaseSettings(), clientContext); this.dropDatabaseCallable = callableFactory.createUnaryCallable( dropDatabaseTransportSettings, settings.dropDatabaseSettings(), clientContext); @@ -582,15 +600,6 @@ public Map extract(ListBackupOperationsRequest request) { testIamPermissionsTransportSettings, settings.testIamPermissionsSettings(), clientContext); - this.createBackupCallable = - callableFactory.createUnaryCallable( - createBackupTransportSettings, settings.createBackupSettings(), clientContext); - this.createBackupOperationCallable = - callableFactory.createOperationCallable( - createBackupTransportSettings, - settings.createBackupOperationSettings(), - clientContext, - this.operationsStub); this.getBackupCallable = callableFactory.createUnaryCallable( getBackupTransportSettings, settings.getBackupSettings(), clientContext); @@ -606,15 +615,6 @@ public Map extract(ListBackupOperationsRequest request) { this.listBackupsPagedCallable = callableFactory.createPagedCallable( listBackupsTransportSettings, settings.listBackupsSettings(), clientContext); - this.restoreDatabaseCallable = - callableFactory.createUnaryCallable( - restoreDatabaseTransportSettings, settings.restoreDatabaseSettings(), clientContext); - this.restoreDatabaseOperationCallable = - callableFactory.createOperationCallable( - restoreDatabaseTransportSettings, - settings.restoreDatabaseOperationSettings(), - clientContext, - this.operationsStub); this.listDatabaseOperationsCallable = callableFactory.createUnaryCallable( listDatabaseOperationsTransportSettings, @@ -644,15 +644,6 @@ public GrpcOperationsStub getOperationsStub() { return operationsStub; } - public UnaryCallable - listDatabasesPagedCallable() { - return listDatabasesPagedCallable; - } - - public UnaryCallable listDatabasesCallable() { - return listDatabasesCallable; - } - @BetaApi("The surface for use by generated code is not stable yet and may change in the future.") public OperationCallable createDatabaseOperationCallable() { @@ -663,10 +654,6 @@ public UnaryCallable createDatabaseCallable() return createDatabaseCallable; } - public UnaryCallable getDatabaseCallable() { - return getDatabaseCallable; - } - @BetaApi("The surface for use by generated code is not stable yet and may change in the future.") public OperationCallable updateDatabaseDdlOperationCallable() { @@ -677,6 +664,39 @@ public UnaryCallable updateDatabaseDdlCalla return updateDatabaseDdlCallable; } + @BetaApi("The surface for use by generated code is not stable yet and may change in the future.") + public OperationCallable + createBackupOperationCallable() { + return createBackupOperationCallable; + } + + public UnaryCallable createBackupCallable() { + return createBackupCallable; + } + + @BetaApi("The surface for use by generated code is not stable yet and may change in the future.") + public OperationCallable + restoreDatabaseOperationCallable() { + return restoreDatabaseOperationCallable; + } + + public UnaryCallable restoreDatabaseCallable() { + return restoreDatabaseCallable; + } + + public UnaryCallable + listDatabasesPagedCallable() { + return listDatabasesPagedCallable; + } + + public UnaryCallable listDatabasesCallable() { + return listDatabasesCallable; + } + + public UnaryCallable getDatabaseCallable() { + return getDatabaseCallable; + } + public UnaryCallable dropDatabaseCallable() { return dropDatabaseCallable; } @@ -698,16 +718,6 @@ public UnaryCallable getIamPolicyCallable() { return testIamPermissionsCallable; } - @BetaApi("The surface for use by generated code is not stable yet and may change in the future.") - public OperationCallable - createBackupOperationCallable() { - return createBackupOperationCallable; - } - - public UnaryCallable createBackupCallable() { - return createBackupCallable; - } - public UnaryCallable getBackupCallable() { return getBackupCallable; } @@ -728,16 +738,6 @@ public UnaryCallable listBackupsCallabl return listBackupsCallable; } - @BetaApi("The surface for use by generated code is not stable yet and may change in the future.") - public OperationCallable - restoreDatabaseOperationCallable() { - return restoreDatabaseOperationCallable; - } - - public UnaryCallable restoreDatabaseCallable() { - return restoreDatabaseCallable; - } - public UnaryCallable listDatabaseOperationsPagedCallable() { return listDatabaseOperationsPagedCallable; diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/admin/instance/v1/InstanceAdminClient.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/admin/instance/v1/InstanceAdminClient.java index 4242e4e42e..fb9c411c00 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/admin/instance/v1/InstanceAdminClient.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/admin/instance/v1/InstanceAdminClient.java @@ -212,274 +212,810 @@ public final OperationsClient getOperationsClient() { // AUTO-GENERATED DOCUMENTATION AND METHOD /** - * Lists the supported instance configurations for a given project. + * Creates an instance and begins preparing it to begin serving. The returned [long-running + * operation][google.longrunning.Operation] can be used to track the progress of preparing the new + * instance. The instance name is assigned by the caller. If the named instance already exists, + * `CreateInstance` returns `ALREADY_EXISTS`. + * + *

Immediately upon completion of this request: + * + *

* The instance is readable via the API, with all requested attributes but no allocated + * resources. Its state is `CREATING`. + * + *

Until completion of the returned operation: + * + *

* Cancelling the operation renders the instance immediately unreadable via the API. + * * The instance can be deleted. * All other attempts to modify the instance are + * rejected. + * + *

Upon completion of the returned operation: + * + *

* Billing for all successfully-allocated resources begins (some types may have lower + * than the requested levels). * Databases can be created in the instance. * The + * instance's allocated resource levels are readable via the API. * The instance's state + * becomes `READY`. + * + *

The returned [long-running operation][google.longrunning.Operation] will have a name of the + * format `<instance_name>/operations/<operation_id>` and can be used to track + * creation of the instance. The [metadata][google.longrunning.Operation.metadata] field type is + * [CreateInstanceMetadata][google.spanner.admin.instance.v1.CreateInstanceMetadata]. The + * [response][google.longrunning.Operation.response] field type is + * [Instance][google.spanner.admin.instance.v1.Instance], if successful. * *

Sample code: * *


    * try (InstanceAdminClient instanceAdminClient = InstanceAdminClient.create()) {
    *   ProjectName parent = ProjectName.of("[PROJECT]");
-   *   for (InstanceConfig element : instanceAdminClient.listInstanceConfigs(parent).iterateAll()) {
-   *     // doThingsWith(element);
-   *   }
+   *   String instanceId = "";
+   *   Instance instance = Instance.newBuilder().build();
+   *   Instance response = instanceAdminClient.createInstanceAsync(parent, instanceId, instance).get();
    * }
    * 
* - * @param parent Required. The name of the project for which a list of supported instance - * configurations is requested. Values are of the form `projects/<project>`. + * @param parent Required. The name of the project in which to create the instance. Values are of + * the form `projects/<project>`. + * @param instanceId Required. The ID of the instance to create. Valid identifiers are of the form + * `[a-z][-a-z0-9]*[a-z0-9]` and must be between 2 and 64 characters in length. + * @param instance Required. The instance to create. The name may be omitted, but if specified + * must be `<parent>/instances/<instance_id>`. * @throws com.google.api.gax.rpc.ApiException if the remote call fails */ - public final ListInstanceConfigsPagedResponse listInstanceConfigs(ProjectName parent) { - ListInstanceConfigsRequest request = - ListInstanceConfigsRequest.newBuilder() + @BetaApi( + "The surface for long-running operations is not stable yet and may change in the future.") + public final OperationFuture createInstanceAsync( + ProjectName parent, String instanceId, Instance instance) { + CreateInstanceRequest request = + CreateInstanceRequest.newBuilder() .setParent(parent == null ? null : parent.toString()) + .setInstanceId(instanceId) + .setInstance(instance) .build(); - return listInstanceConfigs(request); + return createInstanceAsync(request); } // AUTO-GENERATED DOCUMENTATION AND METHOD /** - * Lists the supported instance configurations for a given project. + * Creates an instance and begins preparing it to begin serving. The returned [long-running + * operation][google.longrunning.Operation] can be used to track the progress of preparing the new + * instance. The instance name is assigned by the caller. If the named instance already exists, + * `CreateInstance` returns `ALREADY_EXISTS`. + * + *

Immediately upon completion of this request: + * + *

* The instance is readable via the API, with all requested attributes but no allocated + * resources. Its state is `CREATING`. + * + *

Until completion of the returned operation: + * + *

* Cancelling the operation renders the instance immediately unreadable via the API. + * * The instance can be deleted. * All other attempts to modify the instance are + * rejected. + * + *

Upon completion of the returned operation: + * + *

* Billing for all successfully-allocated resources begins (some types may have lower + * than the requested levels). * Databases can be created in the instance. * The + * instance's allocated resource levels are readable via the API. * The instance's state + * becomes `READY`. + * + *

The returned [long-running operation][google.longrunning.Operation] will have a name of the + * format `<instance_name>/operations/<operation_id>` and can be used to track + * creation of the instance. The [metadata][google.longrunning.Operation.metadata] field type is + * [CreateInstanceMetadata][google.spanner.admin.instance.v1.CreateInstanceMetadata]. The + * [response][google.longrunning.Operation.response] field type is + * [Instance][google.spanner.admin.instance.v1.Instance], if successful. * *

Sample code: * *


    * try (InstanceAdminClient instanceAdminClient = InstanceAdminClient.create()) {
    *   ProjectName parent = ProjectName.of("[PROJECT]");
-   *   for (InstanceConfig element : instanceAdminClient.listInstanceConfigs(parent.toString()).iterateAll()) {
-   *     // doThingsWith(element);
-   *   }
+   *   String instanceId = "";
+   *   Instance instance = Instance.newBuilder().build();
+   *   Instance response = instanceAdminClient.createInstanceAsync(parent.toString(), instanceId, instance).get();
    * }
    * 
* - * @param parent Required. The name of the project for which a list of supported instance - * configurations is requested. Values are of the form `projects/<project>`. + * @param parent Required. The name of the project in which to create the instance. Values are of + * the form `projects/<project>`. + * @param instanceId Required. The ID of the instance to create. Valid identifiers are of the form + * `[a-z][-a-z0-9]*[a-z0-9]` and must be between 2 and 64 characters in length. + * @param instance Required. The instance to create. The name may be omitted, but if specified + * must be `<parent>/instances/<instance_id>`. * @throws com.google.api.gax.rpc.ApiException if the remote call fails */ - public final ListInstanceConfigsPagedResponse listInstanceConfigs(String parent) { - ListInstanceConfigsRequest request = - ListInstanceConfigsRequest.newBuilder().setParent(parent).build(); - return listInstanceConfigs(request); + @BetaApi( + "The surface for long-running operations is not stable yet and may change in the future.") + public final OperationFuture createInstanceAsync( + String parent, String instanceId, Instance instance) { + CreateInstanceRequest request = + CreateInstanceRequest.newBuilder() + .setParent(parent) + .setInstanceId(instanceId) + .setInstance(instance) + .build(); + return createInstanceAsync(request); } // AUTO-GENERATED DOCUMENTATION AND METHOD /** - * Lists the supported instance configurations for a given project. + * Creates an instance and begins preparing it to begin serving. The returned [long-running + * operation][google.longrunning.Operation] can be used to track the progress of preparing the new + * instance. The instance name is assigned by the caller. If the named instance already exists, + * `CreateInstance` returns `ALREADY_EXISTS`. + * + *

Immediately upon completion of this request: + * + *

* The instance is readable via the API, with all requested attributes but no allocated + * resources. Its state is `CREATING`. + * + *

Until completion of the returned operation: + * + *

* Cancelling the operation renders the instance immediately unreadable via the API. + * * The instance can be deleted. * All other attempts to modify the instance are + * rejected. + * + *

Upon completion of the returned operation: + * + *

* Billing for all successfully-allocated resources begins (some types may have lower + * than the requested levels). * Databases can be created in the instance. * The + * instance's allocated resource levels are readable via the API. * The instance's state + * becomes `READY`. + * + *

The returned [long-running operation][google.longrunning.Operation] will have a name of the + * format `<instance_name>/operations/<operation_id>` and can be used to track + * creation of the instance. The [metadata][google.longrunning.Operation.metadata] field type is + * [CreateInstanceMetadata][google.spanner.admin.instance.v1.CreateInstanceMetadata]. The + * [response][google.longrunning.Operation.response] field type is + * [Instance][google.spanner.admin.instance.v1.Instance], if successful. * *

Sample code: * *


    * try (InstanceAdminClient instanceAdminClient = InstanceAdminClient.create()) {
    *   ProjectName parent = ProjectName.of("[PROJECT]");
-   *   ListInstanceConfigsRequest request = ListInstanceConfigsRequest.newBuilder()
+   *   String instanceId = "";
+   *   Instance instance = Instance.newBuilder().build();
+   *   CreateInstanceRequest request = CreateInstanceRequest.newBuilder()
    *     .setParent(parent.toString())
+   *     .setInstanceId(instanceId)
+   *     .setInstance(instance)
    *     .build();
-   *   for (InstanceConfig element : instanceAdminClient.listInstanceConfigs(request).iterateAll()) {
-   *     // doThingsWith(element);
-   *   }
+   *   Instance response = instanceAdminClient.createInstanceAsync(request).get();
    * }
    * 
* * @param request The request object containing all of the parameters for the API call. * @throws com.google.api.gax.rpc.ApiException if the remote call fails */ - public final ListInstanceConfigsPagedResponse listInstanceConfigs( - ListInstanceConfigsRequest request) { - return listInstanceConfigsPagedCallable().call(request); + @BetaApi( + "The surface for long-running operations is not stable yet and may change in the future.") + public final OperationFuture createInstanceAsync( + CreateInstanceRequest request) { + return createInstanceOperationCallable().futureCall(request); } // AUTO-GENERATED DOCUMENTATION AND METHOD /** - * Lists the supported instance configurations for a given project. + * Creates an instance and begins preparing it to begin serving. The returned [long-running + * operation][google.longrunning.Operation] can be used to track the progress of preparing the new + * instance. The instance name is assigned by the caller. If the named instance already exists, + * `CreateInstance` returns `ALREADY_EXISTS`. + * + *

Immediately upon completion of this request: + * + *

* The instance is readable via the API, with all requested attributes but no allocated + * resources. Its state is `CREATING`. + * + *

Until completion of the returned operation: + * + *

* Cancelling the operation renders the instance immediately unreadable via the API. + * * The instance can be deleted. * All other attempts to modify the instance are + * rejected. + * + *

Upon completion of the returned operation: + * + *

* Billing for all successfully-allocated resources begins (some types may have lower + * than the requested levels). * Databases can be created in the instance. * The + * instance's allocated resource levels are readable via the API. * The instance's state + * becomes `READY`. + * + *

The returned [long-running operation][google.longrunning.Operation] will have a name of the + * format `<instance_name>/operations/<operation_id>` and can be used to track + * creation of the instance. The [metadata][google.longrunning.Operation.metadata] field type is + * [CreateInstanceMetadata][google.spanner.admin.instance.v1.CreateInstanceMetadata]. The + * [response][google.longrunning.Operation.response] field type is + * [Instance][google.spanner.admin.instance.v1.Instance], if successful. * *

Sample code: * *


    * try (InstanceAdminClient instanceAdminClient = InstanceAdminClient.create()) {
    *   ProjectName parent = ProjectName.of("[PROJECT]");
-   *   ListInstanceConfigsRequest request = ListInstanceConfigsRequest.newBuilder()
+   *   String instanceId = "";
+   *   Instance instance = Instance.newBuilder().build();
+   *   CreateInstanceRequest request = CreateInstanceRequest.newBuilder()
    *     .setParent(parent.toString())
+   *     .setInstanceId(instanceId)
+   *     .setInstance(instance)
    *     .build();
-   *   ApiFuture<ListInstanceConfigsPagedResponse> future = instanceAdminClient.listInstanceConfigsPagedCallable().futureCall(request);
+   *   OperationFuture<Instance, CreateInstanceMetadata> future = instanceAdminClient.createInstanceOperationCallable().futureCall(request);
    *   // Do something
-   *   for (InstanceConfig element : future.get().iterateAll()) {
-   *     // doThingsWith(element);
-   *   }
+   *   Instance response = future.get();
    * }
    * 
*/ - public final UnaryCallable - listInstanceConfigsPagedCallable() { - return stub.listInstanceConfigsPagedCallable(); + @BetaApi("The surface for use by generated code is not stable yet and may change in the future.") + public final OperationCallable + createInstanceOperationCallable() { + return stub.createInstanceOperationCallable(); } // AUTO-GENERATED DOCUMENTATION AND METHOD /** - * Lists the supported instance configurations for a given project. + * Creates an instance and begins preparing it to begin serving. The returned [long-running + * operation][google.longrunning.Operation] can be used to track the progress of preparing the new + * instance. The instance name is assigned by the caller. If the named instance already exists, + * `CreateInstance` returns `ALREADY_EXISTS`. + * + *

Immediately upon completion of this request: + * + *

* The instance is readable via the API, with all requested attributes but no allocated + * resources. Its state is `CREATING`. + * + *

Until completion of the returned operation: + * + *

* Cancelling the operation renders the instance immediately unreadable via the API. + * * The instance can be deleted. * All other attempts to modify the instance are + * rejected. + * + *

Upon completion of the returned operation: + * + *

* Billing for all successfully-allocated resources begins (some types may have lower + * than the requested levels). * Databases can be created in the instance. * The + * instance's allocated resource levels are readable via the API. * The instance's state + * becomes `READY`. + * + *

The returned [long-running operation][google.longrunning.Operation] will have a name of the + * format `<instance_name>/operations/<operation_id>` and can be used to track + * creation of the instance. The [metadata][google.longrunning.Operation.metadata] field type is + * [CreateInstanceMetadata][google.spanner.admin.instance.v1.CreateInstanceMetadata]. The + * [response][google.longrunning.Operation.response] field type is + * [Instance][google.spanner.admin.instance.v1.Instance], if successful. * *

Sample code: * *


    * try (InstanceAdminClient instanceAdminClient = InstanceAdminClient.create()) {
    *   ProjectName parent = ProjectName.of("[PROJECT]");
-   *   ListInstanceConfigsRequest request = ListInstanceConfigsRequest.newBuilder()
+   *   String instanceId = "";
+   *   Instance instance = Instance.newBuilder().build();
+   *   CreateInstanceRequest request = CreateInstanceRequest.newBuilder()
    *     .setParent(parent.toString())
+   *     .setInstanceId(instanceId)
+   *     .setInstance(instance)
    *     .build();
-   *   while (true) {
-   *     ListInstanceConfigsResponse response = instanceAdminClient.listInstanceConfigsCallable().call(request);
-   *     for (InstanceConfig element : response.getInstanceConfigsList()) {
-   *       // doThingsWith(element);
-   *     }
-   *     String nextPageToken = response.getNextPageToken();
-   *     if (!Strings.isNullOrEmpty(nextPageToken)) {
-   *       request = request.toBuilder().setPageToken(nextPageToken).build();
-   *     } else {
-   *       break;
-   *     }
-   *   }
+   *   ApiFuture<Operation> future = instanceAdminClient.createInstanceCallable().futureCall(request);
+   *   // Do something
+   *   Operation response = future.get();
    * }
    * 
*/ - public final UnaryCallable - listInstanceConfigsCallable() { - return stub.listInstanceConfigsCallable(); + public final UnaryCallable createInstanceCallable() { + return stub.createInstanceCallable(); } // AUTO-GENERATED DOCUMENTATION AND METHOD /** - * Gets information about a particular instance configuration. + * Updates an instance, and begins allocating or releasing resources as requested. The returned + * [long-running operation][google.longrunning.Operation] can be used to track the progress of + * updating the instance. If the named instance does not exist, returns `NOT_FOUND`. + * + *

Immediately upon completion of this request: + * + *

* For resource types for which a decrease in the instance's allocation has been + * requested, billing is based on the newly-requested level. + * + *

Until completion of the returned operation: + * + *

* Cancelling the operation sets its metadata's + * [cancel_time][google.spanner.admin.instance.v1.UpdateInstanceMetadata.cancel_time], and begins + * restoring resources to their pre-request values. The operation is guaranteed to succeed at + * undoing all resource changes, after which point it terminates with a `CANCELLED` status. * + * All other attempts to modify the instance are rejected. * Reading the instance via the API + * continues to give the pre-request resource levels. + * + *

Upon completion of the returned operation: + * + *

* Billing begins for all successfully-allocated resources (some types may have lower + * than the requested levels). * All newly-reserved resources are available for serving the + * instance's tables. * The instance's new resource levels are readable via the API. + * + *

The returned [long-running operation][google.longrunning.Operation] will have a name of the + * format `<instance_name>/operations/<operation_id>` and can be used to track the + * instance modification. The [metadata][google.longrunning.Operation.metadata] field type is + * [UpdateInstanceMetadata][google.spanner.admin.instance.v1.UpdateInstanceMetadata]. The + * [response][google.longrunning.Operation.response] field type is + * [Instance][google.spanner.admin.instance.v1.Instance], if successful. + * + *

Authorization requires `spanner.instances.update` permission on resource + * [name][google.spanner.admin.instance.v1.Instance.name]. * *

Sample code: * *


    * try (InstanceAdminClient instanceAdminClient = InstanceAdminClient.create()) {
-   *   InstanceConfigName name = InstanceConfigName.of("[PROJECT]", "[INSTANCE_CONFIG]");
-   *   InstanceConfig response = instanceAdminClient.getInstanceConfig(name);
+   *   Instance instance = Instance.newBuilder().build();
+   *   FieldMask fieldMask = FieldMask.newBuilder().build();
+   *   Instance response = instanceAdminClient.updateInstanceAsync(instance, fieldMask).get();
    * }
    * 
* - * @param name Required. The name of the requested instance configuration. Values are of the form - * `projects/<project>/instanceConfigs/<config>`. + * @param instance Required. The instance to update, which must always include the instance name. + * Otherwise, only fields mentioned in + * [field_mask][google.spanner.admin.instance.v1.UpdateInstanceRequest.field_mask] need be + * included. + * @param fieldMask Required. A mask specifying which fields in + * [Instance][google.spanner.admin.instance.v1.Instance] should be updated. The field mask + * must always be specified; this prevents any future fields in + * [Instance][google.spanner.admin.instance.v1.Instance] from being erased accidentally by + * clients that do not know about them. * @throws com.google.api.gax.rpc.ApiException if the remote call fails */ - public final InstanceConfig getInstanceConfig(InstanceConfigName name) { - GetInstanceConfigRequest request = - GetInstanceConfigRequest.newBuilder() - .setName(name == null ? null : name.toString()) - .build(); - return getInstanceConfig(request); + @BetaApi( + "The surface for long-running operations is not stable yet and may change in the future.") + public final OperationFuture updateInstanceAsync( + Instance instance, FieldMask fieldMask) { + UpdateInstanceRequest request = + UpdateInstanceRequest.newBuilder().setInstance(instance).setFieldMask(fieldMask).build(); + return updateInstanceAsync(request); } // AUTO-GENERATED DOCUMENTATION AND METHOD /** - * Gets information about a particular instance configuration. + * Updates an instance, and begins allocating or releasing resources as requested. The returned + * [long-running operation][google.longrunning.Operation] can be used to track the progress of + * updating the instance. If the named instance does not exist, returns `NOT_FOUND`. + * + *

Immediately upon completion of this request: + * + *

* For resource types for which a decrease in the instance's allocation has been + * requested, billing is based on the newly-requested level. + * + *

Until completion of the returned operation: + * + *

* Cancelling the operation sets its metadata's + * [cancel_time][google.spanner.admin.instance.v1.UpdateInstanceMetadata.cancel_time], and begins + * restoring resources to their pre-request values. The operation is guaranteed to succeed at + * undoing all resource changes, after which point it terminates with a `CANCELLED` status. * + * All other attempts to modify the instance are rejected. * Reading the instance via the API + * continues to give the pre-request resource levels. + * + *

Upon completion of the returned operation: + * + *

* Billing begins for all successfully-allocated resources (some types may have lower + * than the requested levels). * All newly-reserved resources are available for serving the + * instance's tables. * The instance's new resource levels are readable via the API. + * + *

The returned [long-running operation][google.longrunning.Operation] will have a name of the + * format `<instance_name>/operations/<operation_id>` and can be used to track the + * instance modification. The [metadata][google.longrunning.Operation.metadata] field type is + * [UpdateInstanceMetadata][google.spanner.admin.instance.v1.UpdateInstanceMetadata]. The + * [response][google.longrunning.Operation.response] field type is + * [Instance][google.spanner.admin.instance.v1.Instance], if successful. + * + *

Authorization requires `spanner.instances.update` permission on resource + * [name][google.spanner.admin.instance.v1.Instance.name]. * *

Sample code: * *


    * try (InstanceAdminClient instanceAdminClient = InstanceAdminClient.create()) {
-   *   InstanceConfigName name = InstanceConfigName.of("[PROJECT]", "[INSTANCE_CONFIG]");
-   *   InstanceConfig response = instanceAdminClient.getInstanceConfig(name.toString());
+   *   Instance instance = Instance.newBuilder().build();
+   *   FieldMask fieldMask = FieldMask.newBuilder().build();
+   *   UpdateInstanceRequest request = UpdateInstanceRequest.newBuilder()
+   *     .setInstance(instance)
+   *     .setFieldMask(fieldMask)
+   *     .build();
+   *   Instance response = instanceAdminClient.updateInstanceAsync(request).get();
    * }
    * 
* - * @param name Required. The name of the requested instance configuration. Values are of the form - * `projects/<project>/instanceConfigs/<config>`. + * @param request The request object containing all of the parameters for the API call. * @throws com.google.api.gax.rpc.ApiException if the remote call fails */ - public final InstanceConfig getInstanceConfig(String name) { - GetInstanceConfigRequest request = GetInstanceConfigRequest.newBuilder().setName(name).build(); - return getInstanceConfig(request); + @BetaApi( + "The surface for long-running operations is not stable yet and may change in the future.") + public final OperationFuture updateInstanceAsync( + UpdateInstanceRequest request) { + return updateInstanceOperationCallable().futureCall(request); } // AUTO-GENERATED DOCUMENTATION AND METHOD /** - * Gets information about a particular instance configuration. + * Updates an instance, and begins allocating or releasing resources as requested. The returned + * [long-running operation][google.longrunning.Operation] can be used to track the progress of + * updating the instance. If the named instance does not exist, returns `NOT_FOUND`. + * + *

Immediately upon completion of this request: + * + *

* For resource types for which a decrease in the instance's allocation has been + * requested, billing is based on the newly-requested level. + * + *

Until completion of the returned operation: + * + *

* Cancelling the operation sets its metadata's + * [cancel_time][google.spanner.admin.instance.v1.UpdateInstanceMetadata.cancel_time], and begins + * restoring resources to their pre-request values. The operation is guaranteed to succeed at + * undoing all resource changes, after which point it terminates with a `CANCELLED` status. * + * All other attempts to modify the instance are rejected. * Reading the instance via the API + * continues to give the pre-request resource levels. + * + *

Upon completion of the returned operation: + * + *

* Billing begins for all successfully-allocated resources (some types may have lower + * than the requested levels). * All newly-reserved resources are available for serving the + * instance's tables. * The instance's new resource levels are readable via the API. + * + *

The returned [long-running operation][google.longrunning.Operation] will have a name of the + * format `<instance_name>/operations/<operation_id>` and can be used to track the + * instance modification. The [metadata][google.longrunning.Operation.metadata] field type is + * [UpdateInstanceMetadata][google.spanner.admin.instance.v1.UpdateInstanceMetadata]. The + * [response][google.longrunning.Operation.response] field type is + * [Instance][google.spanner.admin.instance.v1.Instance], if successful. + * + *

Authorization requires `spanner.instances.update` permission on resource + * [name][google.spanner.admin.instance.v1.Instance.name]. * *

Sample code: * *


    * try (InstanceAdminClient instanceAdminClient = InstanceAdminClient.create()) {
-   *   InstanceConfigName name = InstanceConfigName.of("[PROJECT]", "[INSTANCE_CONFIG]");
-   *   GetInstanceConfigRequest request = GetInstanceConfigRequest.newBuilder()
-   *     .setName(name.toString())
+   *   Instance instance = Instance.newBuilder().build();
+   *   FieldMask fieldMask = FieldMask.newBuilder().build();
+   *   UpdateInstanceRequest request = UpdateInstanceRequest.newBuilder()
+   *     .setInstance(instance)
+   *     .setFieldMask(fieldMask)
    *     .build();
-   *   InstanceConfig response = instanceAdminClient.getInstanceConfig(request);
+   *   OperationFuture<Instance, UpdateInstanceMetadata> future = instanceAdminClient.updateInstanceOperationCallable().futureCall(request);
+   *   // Do something
+   *   Instance response = future.get();
    * }
    * 
- * - * @param request The request object containing all of the parameters for the API call. - * @throws com.google.api.gax.rpc.ApiException if the remote call fails */ - public final InstanceConfig getInstanceConfig(GetInstanceConfigRequest request) { - return getInstanceConfigCallable().call(request); + @BetaApi("The surface for use by generated code is not stable yet and may change in the future.") + public final OperationCallable + updateInstanceOperationCallable() { + return stub.updateInstanceOperationCallable(); } // AUTO-GENERATED DOCUMENTATION AND METHOD /** - * Gets information about a particular instance configuration. + * Updates an instance, and begins allocating or releasing resources as requested. The returned + * [long-running operation][google.longrunning.Operation] can be used to track the progress of + * updating the instance. If the named instance does not exist, returns `NOT_FOUND`. + * + *

Immediately upon completion of this request: + * + *

* For resource types for which a decrease in the instance's allocation has been + * requested, billing is based on the newly-requested level. + * + *

Until completion of the returned operation: + * + *

* Cancelling the operation sets its metadata's + * [cancel_time][google.spanner.admin.instance.v1.UpdateInstanceMetadata.cancel_time], and begins + * restoring resources to their pre-request values. The operation is guaranteed to succeed at + * undoing all resource changes, after which point it terminates with a `CANCELLED` status. * + * All other attempts to modify the instance are rejected. * Reading the instance via the API + * continues to give the pre-request resource levels. + * + *

Upon completion of the returned operation: + * + *

* Billing begins for all successfully-allocated resources (some types may have lower + * than the requested levels). * All newly-reserved resources are available for serving the + * instance's tables. * The instance's new resource levels are readable via the API. + * + *

The returned [long-running operation][google.longrunning.Operation] will have a name of the + * format `<instance_name>/operations/<operation_id>` and can be used to track the + * instance modification. The [metadata][google.longrunning.Operation.metadata] field type is + * [UpdateInstanceMetadata][google.spanner.admin.instance.v1.UpdateInstanceMetadata]. The + * [response][google.longrunning.Operation.response] field type is + * [Instance][google.spanner.admin.instance.v1.Instance], if successful. + * + *

Authorization requires `spanner.instances.update` permission on resource + * [name][google.spanner.admin.instance.v1.Instance.name]. * *

Sample code: * *


    * try (InstanceAdminClient instanceAdminClient = InstanceAdminClient.create()) {
-   *   InstanceConfigName name = InstanceConfigName.of("[PROJECT]", "[INSTANCE_CONFIG]");
-   *   GetInstanceConfigRequest request = GetInstanceConfigRequest.newBuilder()
-   *     .setName(name.toString())
+   *   Instance instance = Instance.newBuilder().build();
+   *   FieldMask fieldMask = FieldMask.newBuilder().build();
+   *   UpdateInstanceRequest request = UpdateInstanceRequest.newBuilder()
+   *     .setInstance(instance)
+   *     .setFieldMask(fieldMask)
    *     .build();
-   *   ApiFuture<InstanceConfig> future = instanceAdminClient.getInstanceConfigCallable().futureCall(request);
+   *   ApiFuture<Operation> future = instanceAdminClient.updateInstanceCallable().futureCall(request);
    *   // Do something
-   *   InstanceConfig response = future.get();
+   *   Operation response = future.get();
    * }
    * 
*/ - public final UnaryCallable getInstanceConfigCallable() { - return stub.getInstanceConfigCallable(); + public final UnaryCallable updateInstanceCallable() { + return stub.updateInstanceCallable(); } // AUTO-GENERATED DOCUMENTATION AND METHOD /** - * Lists all instances in the given project. + * Lists the supported instance configurations for a given project. * *

Sample code: * *


    * try (InstanceAdminClient instanceAdminClient = InstanceAdminClient.create()) {
    *   ProjectName parent = ProjectName.of("[PROJECT]");
-   *   for (Instance element : instanceAdminClient.listInstances(parent).iterateAll()) {
+   *   for (InstanceConfig element : instanceAdminClient.listInstanceConfigs(parent).iterateAll()) {
    *     // doThingsWith(element);
    *   }
    * }
    * 
* - * @param parent Required. The name of the project for which a list of instances is requested. - * Values are of the form `projects/<project>`. + * @param parent Required. The name of the project for which a list of supported instance + * configurations is requested. Values are of the form `projects/<project>`. * @throws com.google.api.gax.rpc.ApiException if the remote call fails */ - public final ListInstancesPagedResponse listInstances(ProjectName parent) { - ListInstancesRequest request = - ListInstancesRequest.newBuilder() + public final ListInstanceConfigsPagedResponse listInstanceConfigs(ProjectName parent) { + ListInstanceConfigsRequest request = + ListInstanceConfigsRequest.newBuilder() .setParent(parent == null ? null : parent.toString()) .build(); - return listInstances(request); + return listInstanceConfigs(request); } // AUTO-GENERATED DOCUMENTATION AND METHOD /** - * Lists all instances in the given project. + * Lists the supported instance configurations for a given project. * *

Sample code: * *


    * try (InstanceAdminClient instanceAdminClient = InstanceAdminClient.create()) {
    *   ProjectName parent = ProjectName.of("[PROJECT]");
-   *   for (Instance element : instanceAdminClient.listInstances(parent.toString()).iterateAll()) {
+   *   for (InstanceConfig element : instanceAdminClient.listInstanceConfigs(parent.toString()).iterateAll()) {
    *     // doThingsWith(element);
    *   }
    * }
    * 
* - * @param parent Required. The name of the project for which a list of instances is requested. + * @param parent Required. The name of the project for which a list of supported instance + * configurations is requested. Values are of the form `projects/<project>`. + * @throws com.google.api.gax.rpc.ApiException if the remote call fails + */ + public final ListInstanceConfigsPagedResponse listInstanceConfigs(String parent) { + ListInstanceConfigsRequest request = + ListInstanceConfigsRequest.newBuilder().setParent(parent).build(); + return listInstanceConfigs(request); + } + + // AUTO-GENERATED DOCUMENTATION AND METHOD + /** + * Lists the supported instance configurations for a given project. + * + *

Sample code: + * + *


+   * try (InstanceAdminClient instanceAdminClient = InstanceAdminClient.create()) {
+   *   ProjectName parent = ProjectName.of("[PROJECT]");
+   *   ListInstanceConfigsRequest request = ListInstanceConfigsRequest.newBuilder()
+   *     .setParent(parent.toString())
+   *     .build();
+   *   for (InstanceConfig element : instanceAdminClient.listInstanceConfigs(request).iterateAll()) {
+   *     // doThingsWith(element);
+   *   }
+   * }
+   * 
+ * + * @param request The request object containing all of the parameters for the API call. + * @throws com.google.api.gax.rpc.ApiException if the remote call fails + */ + public final ListInstanceConfigsPagedResponse listInstanceConfigs( + ListInstanceConfigsRequest request) { + return listInstanceConfigsPagedCallable().call(request); + } + + // AUTO-GENERATED DOCUMENTATION AND METHOD + /** + * Lists the supported instance configurations for a given project. + * + *

Sample code: + * + *


+   * try (InstanceAdminClient instanceAdminClient = InstanceAdminClient.create()) {
+   *   ProjectName parent = ProjectName.of("[PROJECT]");
+   *   ListInstanceConfigsRequest request = ListInstanceConfigsRequest.newBuilder()
+   *     .setParent(parent.toString())
+   *     .build();
+   *   ApiFuture<ListInstanceConfigsPagedResponse> future = instanceAdminClient.listInstanceConfigsPagedCallable().futureCall(request);
+   *   // Do something
+   *   for (InstanceConfig element : future.get().iterateAll()) {
+   *     // doThingsWith(element);
+   *   }
+   * }
+   * 
+ */ + public final UnaryCallable + listInstanceConfigsPagedCallable() { + return stub.listInstanceConfigsPagedCallable(); + } + + // AUTO-GENERATED DOCUMENTATION AND METHOD + /** + * Lists the supported instance configurations for a given project. + * + *

Sample code: + * + *


+   * try (InstanceAdminClient instanceAdminClient = InstanceAdminClient.create()) {
+   *   ProjectName parent = ProjectName.of("[PROJECT]");
+   *   ListInstanceConfigsRequest request = ListInstanceConfigsRequest.newBuilder()
+   *     .setParent(parent.toString())
+   *     .build();
+   *   while (true) {
+   *     ListInstanceConfigsResponse response = instanceAdminClient.listInstanceConfigsCallable().call(request);
+   *     for (InstanceConfig element : response.getInstanceConfigsList()) {
+   *       // doThingsWith(element);
+   *     }
+   *     String nextPageToken = response.getNextPageToken();
+   *     if (!Strings.isNullOrEmpty(nextPageToken)) {
+   *       request = request.toBuilder().setPageToken(nextPageToken).build();
+   *     } else {
+   *       break;
+   *     }
+   *   }
+   * }
+   * 
+ */ + public final UnaryCallable + listInstanceConfigsCallable() { + return stub.listInstanceConfigsCallable(); + } + + // AUTO-GENERATED DOCUMENTATION AND METHOD + /** + * Gets information about a particular instance configuration. + * + *

Sample code: + * + *


+   * try (InstanceAdminClient instanceAdminClient = InstanceAdminClient.create()) {
+   *   InstanceConfigName name = InstanceConfigName.of("[PROJECT]", "[INSTANCE_CONFIG]");
+   *   InstanceConfig response = instanceAdminClient.getInstanceConfig(name);
+   * }
+   * 
+ * + * @param name Required. The name of the requested instance configuration. Values are of the form + * `projects/<project>/instanceConfigs/<config>`. + * @throws com.google.api.gax.rpc.ApiException if the remote call fails + */ + public final InstanceConfig getInstanceConfig(InstanceConfigName name) { + GetInstanceConfigRequest request = + GetInstanceConfigRequest.newBuilder() + .setName(name == null ? null : name.toString()) + .build(); + return getInstanceConfig(request); + } + + // AUTO-GENERATED DOCUMENTATION AND METHOD + /** + * Gets information about a particular instance configuration. + * + *

Sample code: + * + *


+   * try (InstanceAdminClient instanceAdminClient = InstanceAdminClient.create()) {
+   *   InstanceConfigName name = InstanceConfigName.of("[PROJECT]", "[INSTANCE_CONFIG]");
+   *   InstanceConfig response = instanceAdminClient.getInstanceConfig(name.toString());
+   * }
+   * 
+ * + * @param name Required. The name of the requested instance configuration. Values are of the form + * `projects/<project>/instanceConfigs/<config>`. + * @throws com.google.api.gax.rpc.ApiException if the remote call fails + */ + public final InstanceConfig getInstanceConfig(String name) { + GetInstanceConfigRequest request = GetInstanceConfigRequest.newBuilder().setName(name).build(); + return getInstanceConfig(request); + } + + // AUTO-GENERATED DOCUMENTATION AND METHOD + /** + * Gets information about a particular instance configuration. + * + *

Sample code: + * + *


+   * try (InstanceAdminClient instanceAdminClient = InstanceAdminClient.create()) {
+   *   InstanceConfigName name = InstanceConfigName.of("[PROJECT]", "[INSTANCE_CONFIG]");
+   *   GetInstanceConfigRequest request = GetInstanceConfigRequest.newBuilder()
+   *     .setName(name.toString())
+   *     .build();
+   *   InstanceConfig response = instanceAdminClient.getInstanceConfig(request);
+   * }
+   * 
+ * + * @param request The request object containing all of the parameters for the API call. + * @throws com.google.api.gax.rpc.ApiException if the remote call fails + */ + public final InstanceConfig getInstanceConfig(GetInstanceConfigRequest request) { + return getInstanceConfigCallable().call(request); + } + + // AUTO-GENERATED DOCUMENTATION AND METHOD + /** + * Gets information about a particular instance configuration. + * + *

Sample code: + * + *


+   * try (InstanceAdminClient instanceAdminClient = InstanceAdminClient.create()) {
+   *   InstanceConfigName name = InstanceConfigName.of("[PROJECT]", "[INSTANCE_CONFIG]");
+   *   GetInstanceConfigRequest request = GetInstanceConfigRequest.newBuilder()
+   *     .setName(name.toString())
+   *     .build();
+   *   ApiFuture<InstanceConfig> future = instanceAdminClient.getInstanceConfigCallable().futureCall(request);
+   *   // Do something
+   *   InstanceConfig response = future.get();
+   * }
+   * 
+ */ + public final UnaryCallable getInstanceConfigCallable() { + return stub.getInstanceConfigCallable(); + } + + // AUTO-GENERATED DOCUMENTATION AND METHOD + /** + * Lists all instances in the given project. + * + *

Sample code: + * + *


+   * try (InstanceAdminClient instanceAdminClient = InstanceAdminClient.create()) {
+   *   ProjectName parent = ProjectName.of("[PROJECT]");
+   *   for (Instance element : instanceAdminClient.listInstances(parent).iterateAll()) {
+   *     // doThingsWith(element);
+   *   }
+   * }
+   * 
+ * + * @param parent Required. The name of the project for which a list of instances is requested. + * Values are of the form `projects/<project>`. + * @throws com.google.api.gax.rpc.ApiException if the remote call fails + */ + public final ListInstancesPagedResponse listInstances(ProjectName parent) { + ListInstancesRequest request = + ListInstancesRequest.newBuilder() + .setParent(parent == null ? null : parent.toString()) + .build(); + return listInstances(request); + } + + // AUTO-GENERATED DOCUMENTATION AND METHOD + /** + * Lists all instances in the given project. + * + *

Sample code: + * + *


+   * try (InstanceAdminClient instanceAdminClient = InstanceAdminClient.create()) {
+   *   ProjectName parent = ProjectName.of("[PROJECT]");
+   *   for (Instance element : instanceAdminClient.listInstances(parent.toString()).iterateAll()) {
+   *     // doThingsWith(element);
+   *   }
+   * }
+   * 
+ * + * @param parent Required. The name of the project for which a list of instances is requested. * Values are of the form `projects/<project>`. * @throws com.google.api.gax.rpc.ApiException if the remote call fails */ @@ -659,542 +1195,6 @@ public final UnaryCallable getInstanceCallable() { return stub.getInstanceCallable(); } - // AUTO-GENERATED DOCUMENTATION AND METHOD - /** - * Creates an instance and begins preparing it to begin serving. The returned [long-running - * operation][google.longrunning.Operation] can be used to track the progress of preparing the new - * instance. The instance name is assigned by the caller. If the named instance already exists, - * `CreateInstance` returns `ALREADY_EXISTS`. - * - *

Immediately upon completion of this request: - * - *

* The instance is readable via the API, with all requested attributes but no allocated - * resources. Its state is `CREATING`. - * - *

Until completion of the returned operation: - * - *

* Cancelling the operation renders the instance immediately unreadable via the API. - * * The instance can be deleted. * All other attempts to modify the instance are - * rejected. - * - *

Upon completion of the returned operation: - * - *

* Billing for all successfully-allocated resources begins (some types may have lower - * than the requested levels). * Databases can be created in the instance. * The - * instance's allocated resource levels are readable via the API. * The instance's state - * becomes `READY`. - * - *

The returned [long-running operation][google.longrunning.Operation] will have a name of the - * format `<instance_name>/operations/<operation_id>` and can be used to track - * creation of the instance. The [metadata][google.longrunning.Operation.metadata] field type is - * [CreateInstanceMetadata][google.spanner.admin.instance.v1.CreateInstanceMetadata]. The - * [response][google.longrunning.Operation.response] field type is - * [Instance][google.spanner.admin.instance.v1.Instance], if successful. - * - *

Sample code: - * - *


-   * try (InstanceAdminClient instanceAdminClient = InstanceAdminClient.create()) {
-   *   ProjectName parent = ProjectName.of("[PROJECT]");
-   *   String instanceId = "";
-   *   Instance instance = Instance.newBuilder().build();
-   *   Instance response = instanceAdminClient.createInstanceAsync(parent, instanceId, instance).get();
-   * }
-   * 
- * - * @param parent Required. The name of the project in which to create the instance. Values are of - * the form `projects/<project>`. - * @param instanceId Required. The ID of the instance to create. Valid identifiers are of the form - * `[a-z][-a-z0-9]*[a-z0-9]` and must be between 2 and 64 characters in length. - * @param instance Required. The instance to create. The name may be omitted, but if specified - * must be `<parent>/instances/<instance_id>`. - * @throws com.google.api.gax.rpc.ApiException if the remote call fails - */ - @BetaApi( - "The surface for long-running operations is not stable yet and may change in the future.") - public final OperationFuture createInstanceAsync( - ProjectName parent, String instanceId, Instance instance) { - CreateInstanceRequest request = - CreateInstanceRequest.newBuilder() - .setParent(parent == null ? null : parent.toString()) - .setInstanceId(instanceId) - .setInstance(instance) - .build(); - return createInstanceAsync(request); - } - - // AUTO-GENERATED DOCUMENTATION AND METHOD - /** - * Creates an instance and begins preparing it to begin serving. The returned [long-running - * operation][google.longrunning.Operation] can be used to track the progress of preparing the new - * instance. The instance name is assigned by the caller. If the named instance already exists, - * `CreateInstance` returns `ALREADY_EXISTS`. - * - *

Immediately upon completion of this request: - * - *

* The instance is readable via the API, with all requested attributes but no allocated - * resources. Its state is `CREATING`. - * - *

Until completion of the returned operation: - * - *

* Cancelling the operation renders the instance immediately unreadable via the API. - * * The instance can be deleted. * All other attempts to modify the instance are - * rejected. - * - *

Upon completion of the returned operation: - * - *

* Billing for all successfully-allocated resources begins (some types may have lower - * than the requested levels). * Databases can be created in the instance. * The - * instance's allocated resource levels are readable via the API. * The instance's state - * becomes `READY`. - * - *

The returned [long-running operation][google.longrunning.Operation] will have a name of the - * format `<instance_name>/operations/<operation_id>` and can be used to track - * creation of the instance. The [metadata][google.longrunning.Operation.metadata] field type is - * [CreateInstanceMetadata][google.spanner.admin.instance.v1.CreateInstanceMetadata]. The - * [response][google.longrunning.Operation.response] field type is - * [Instance][google.spanner.admin.instance.v1.Instance], if successful. - * - *

Sample code: - * - *


-   * try (InstanceAdminClient instanceAdminClient = InstanceAdminClient.create()) {
-   *   ProjectName parent = ProjectName.of("[PROJECT]");
-   *   String instanceId = "";
-   *   Instance instance = Instance.newBuilder().build();
-   *   Instance response = instanceAdminClient.createInstanceAsync(parent.toString(), instanceId, instance).get();
-   * }
-   * 
- * - * @param parent Required. The name of the project in which to create the instance. Values are of - * the form `projects/<project>`. - * @param instanceId Required. The ID of the instance to create. Valid identifiers are of the form - * `[a-z][-a-z0-9]*[a-z0-9]` and must be between 2 and 64 characters in length. - * @param instance Required. The instance to create. The name may be omitted, but if specified - * must be `<parent>/instances/<instance_id>`. - * @throws com.google.api.gax.rpc.ApiException if the remote call fails - */ - @BetaApi( - "The surface for long-running operations is not stable yet and may change in the future.") - public final OperationFuture createInstanceAsync( - String parent, String instanceId, Instance instance) { - CreateInstanceRequest request = - CreateInstanceRequest.newBuilder() - .setParent(parent) - .setInstanceId(instanceId) - .setInstance(instance) - .build(); - return createInstanceAsync(request); - } - - // AUTO-GENERATED DOCUMENTATION AND METHOD - /** - * Creates an instance and begins preparing it to begin serving. The returned [long-running - * operation][google.longrunning.Operation] can be used to track the progress of preparing the new - * instance. The instance name is assigned by the caller. If the named instance already exists, - * `CreateInstance` returns `ALREADY_EXISTS`. - * - *

Immediately upon completion of this request: - * - *

* The instance is readable via the API, with all requested attributes but no allocated - * resources. Its state is `CREATING`. - * - *

Until completion of the returned operation: - * - *

* Cancelling the operation renders the instance immediately unreadable via the API. - * * The instance can be deleted. * All other attempts to modify the instance are - * rejected. - * - *

Upon completion of the returned operation: - * - *

* Billing for all successfully-allocated resources begins (some types may have lower - * than the requested levels). * Databases can be created in the instance. * The - * instance's allocated resource levels are readable via the API. * The instance's state - * becomes `READY`. - * - *

The returned [long-running operation][google.longrunning.Operation] will have a name of the - * format `<instance_name>/operations/<operation_id>` and can be used to track - * creation of the instance. The [metadata][google.longrunning.Operation.metadata] field type is - * [CreateInstanceMetadata][google.spanner.admin.instance.v1.CreateInstanceMetadata]. The - * [response][google.longrunning.Operation.response] field type is - * [Instance][google.spanner.admin.instance.v1.Instance], if successful. - * - *

Sample code: - * - *


-   * try (InstanceAdminClient instanceAdminClient = InstanceAdminClient.create()) {
-   *   ProjectName parent = ProjectName.of("[PROJECT]");
-   *   String instanceId = "";
-   *   Instance instance = Instance.newBuilder().build();
-   *   CreateInstanceRequest request = CreateInstanceRequest.newBuilder()
-   *     .setParent(parent.toString())
-   *     .setInstanceId(instanceId)
-   *     .setInstance(instance)
-   *     .build();
-   *   Instance response = instanceAdminClient.createInstanceAsync(request).get();
-   * }
-   * 
- * - * @param request The request object containing all of the parameters for the API call. - * @throws com.google.api.gax.rpc.ApiException if the remote call fails - */ - @BetaApi( - "The surface for long-running operations is not stable yet and may change in the future.") - public final OperationFuture createInstanceAsync( - CreateInstanceRequest request) { - return createInstanceOperationCallable().futureCall(request); - } - - // AUTO-GENERATED DOCUMENTATION AND METHOD - /** - * Creates an instance and begins preparing it to begin serving. The returned [long-running - * operation][google.longrunning.Operation] can be used to track the progress of preparing the new - * instance. The instance name is assigned by the caller. If the named instance already exists, - * `CreateInstance` returns `ALREADY_EXISTS`. - * - *

Immediately upon completion of this request: - * - *

* The instance is readable via the API, with all requested attributes but no allocated - * resources. Its state is `CREATING`. - * - *

Until completion of the returned operation: - * - *

* Cancelling the operation renders the instance immediately unreadable via the API. - * * The instance can be deleted. * All other attempts to modify the instance are - * rejected. - * - *

Upon completion of the returned operation: - * - *

* Billing for all successfully-allocated resources begins (some types may have lower - * than the requested levels). * Databases can be created in the instance. * The - * instance's allocated resource levels are readable via the API. * The instance's state - * becomes `READY`. - * - *

The returned [long-running operation][google.longrunning.Operation] will have a name of the - * format `<instance_name>/operations/<operation_id>` and can be used to track - * creation of the instance. The [metadata][google.longrunning.Operation.metadata] field type is - * [CreateInstanceMetadata][google.spanner.admin.instance.v1.CreateInstanceMetadata]. The - * [response][google.longrunning.Operation.response] field type is - * [Instance][google.spanner.admin.instance.v1.Instance], if successful. - * - *

Sample code: - * - *


-   * try (InstanceAdminClient instanceAdminClient = InstanceAdminClient.create()) {
-   *   ProjectName parent = ProjectName.of("[PROJECT]");
-   *   String instanceId = "";
-   *   Instance instance = Instance.newBuilder().build();
-   *   CreateInstanceRequest request = CreateInstanceRequest.newBuilder()
-   *     .setParent(parent.toString())
-   *     .setInstanceId(instanceId)
-   *     .setInstance(instance)
-   *     .build();
-   *   OperationFuture<Instance, CreateInstanceMetadata> future = instanceAdminClient.createInstanceOperationCallable().futureCall(request);
-   *   // Do something
-   *   Instance response = future.get();
-   * }
-   * 
- */ - @BetaApi("The surface for use by generated code is not stable yet and may change in the future.") - public final OperationCallable - createInstanceOperationCallable() { - return stub.createInstanceOperationCallable(); - } - - // AUTO-GENERATED DOCUMENTATION AND METHOD - /** - * Creates an instance and begins preparing it to begin serving. The returned [long-running - * operation][google.longrunning.Operation] can be used to track the progress of preparing the new - * instance. The instance name is assigned by the caller. If the named instance already exists, - * `CreateInstance` returns `ALREADY_EXISTS`. - * - *

Immediately upon completion of this request: - * - *

* The instance is readable via the API, with all requested attributes but no allocated - * resources. Its state is `CREATING`. - * - *

Until completion of the returned operation: - * - *

* Cancelling the operation renders the instance immediately unreadable via the API. - * * The instance can be deleted. * All other attempts to modify the instance are - * rejected. - * - *

Upon completion of the returned operation: - * - *

* Billing for all successfully-allocated resources begins (some types may have lower - * than the requested levels). * Databases can be created in the instance. * The - * instance's allocated resource levels are readable via the API. * The instance's state - * becomes `READY`. - * - *

The returned [long-running operation][google.longrunning.Operation] will have a name of the - * format `<instance_name>/operations/<operation_id>` and can be used to track - * creation of the instance. The [metadata][google.longrunning.Operation.metadata] field type is - * [CreateInstanceMetadata][google.spanner.admin.instance.v1.CreateInstanceMetadata]. The - * [response][google.longrunning.Operation.response] field type is - * [Instance][google.spanner.admin.instance.v1.Instance], if successful. - * - *

Sample code: - * - *


-   * try (InstanceAdminClient instanceAdminClient = InstanceAdminClient.create()) {
-   *   ProjectName parent = ProjectName.of("[PROJECT]");
-   *   String instanceId = "";
-   *   Instance instance = Instance.newBuilder().build();
-   *   CreateInstanceRequest request = CreateInstanceRequest.newBuilder()
-   *     .setParent(parent.toString())
-   *     .setInstanceId(instanceId)
-   *     .setInstance(instance)
-   *     .build();
-   *   ApiFuture<Operation> future = instanceAdminClient.createInstanceCallable().futureCall(request);
-   *   // Do something
-   *   Operation response = future.get();
-   * }
-   * 
- */ - public final UnaryCallable createInstanceCallable() { - return stub.createInstanceCallable(); - } - - // AUTO-GENERATED DOCUMENTATION AND METHOD - /** - * Updates an instance, and begins allocating or releasing resources as requested. The returned - * [long-running operation][google.longrunning.Operation] can be used to track the progress of - * updating the instance. If the named instance does not exist, returns `NOT_FOUND`. - * - *

Immediately upon completion of this request: - * - *

* For resource types for which a decrease in the instance's allocation has been - * requested, billing is based on the newly-requested level. - * - *

Until completion of the returned operation: - * - *

* Cancelling the operation sets its metadata's - * [cancel_time][google.spanner.admin.instance.v1.UpdateInstanceMetadata.cancel_time], and begins - * restoring resources to their pre-request values. The operation is guaranteed to succeed at - * undoing all resource changes, after which point it terminates with a `CANCELLED` status. * - * All other attempts to modify the instance are rejected. * Reading the instance via the API - * continues to give the pre-request resource levels. - * - *

Upon completion of the returned operation: - * - *

* Billing begins for all successfully-allocated resources (some types may have lower - * than the requested levels). * All newly-reserved resources are available for serving the - * instance's tables. * The instance's new resource levels are readable via the API. - * - *

The returned [long-running operation][google.longrunning.Operation] will have a name of the - * format `<instance_name>/operations/<operation_id>` and can be used to track the - * instance modification. The [metadata][google.longrunning.Operation.metadata] field type is - * [UpdateInstanceMetadata][google.spanner.admin.instance.v1.UpdateInstanceMetadata]. The - * [response][google.longrunning.Operation.response] field type is - * [Instance][google.spanner.admin.instance.v1.Instance], if successful. - * - *

Authorization requires `spanner.instances.update` permission on resource - * [name][google.spanner.admin.instance.v1.Instance.name]. - * - *

Sample code: - * - *


-   * try (InstanceAdminClient instanceAdminClient = InstanceAdminClient.create()) {
-   *   Instance instance = Instance.newBuilder().build();
-   *   FieldMask fieldMask = FieldMask.newBuilder().build();
-   *   Instance response = instanceAdminClient.updateInstanceAsync(instance, fieldMask).get();
-   * }
-   * 
- * - * @param instance Required. The instance to update, which must always include the instance name. - * Otherwise, only fields mentioned in - * [field_mask][google.spanner.admin.instance.v1.UpdateInstanceRequest.field_mask] need be - * included. - * @param fieldMask Required. A mask specifying which fields in - * [Instance][google.spanner.admin.instance.v1.Instance] should be updated. The field mask - * must always be specified; this prevents any future fields in - * [Instance][google.spanner.admin.instance.v1.Instance] from being erased accidentally by - * clients that do not know about them. - * @throws com.google.api.gax.rpc.ApiException if the remote call fails - */ - @BetaApi( - "The surface for long-running operations is not stable yet and may change in the future.") - public final OperationFuture updateInstanceAsync( - Instance instance, FieldMask fieldMask) { - UpdateInstanceRequest request = - UpdateInstanceRequest.newBuilder().setInstance(instance).setFieldMask(fieldMask).build(); - return updateInstanceAsync(request); - } - - // AUTO-GENERATED DOCUMENTATION AND METHOD - /** - * Updates an instance, and begins allocating or releasing resources as requested. The returned - * [long-running operation][google.longrunning.Operation] can be used to track the progress of - * updating the instance. If the named instance does not exist, returns `NOT_FOUND`. - * - *

Immediately upon completion of this request: - * - *

* For resource types for which a decrease in the instance's allocation has been - * requested, billing is based on the newly-requested level. - * - *

Until completion of the returned operation: - * - *

* Cancelling the operation sets its metadata's - * [cancel_time][google.spanner.admin.instance.v1.UpdateInstanceMetadata.cancel_time], and begins - * restoring resources to their pre-request values. The operation is guaranteed to succeed at - * undoing all resource changes, after which point it terminates with a `CANCELLED` status. * - * All other attempts to modify the instance are rejected. * Reading the instance via the API - * continues to give the pre-request resource levels. - * - *

Upon completion of the returned operation: - * - *

* Billing begins for all successfully-allocated resources (some types may have lower - * than the requested levels). * All newly-reserved resources are available for serving the - * instance's tables. * The instance's new resource levels are readable via the API. - * - *

The returned [long-running operation][google.longrunning.Operation] will have a name of the - * format `<instance_name>/operations/<operation_id>` and can be used to track the - * instance modification. The [metadata][google.longrunning.Operation.metadata] field type is - * [UpdateInstanceMetadata][google.spanner.admin.instance.v1.UpdateInstanceMetadata]. The - * [response][google.longrunning.Operation.response] field type is - * [Instance][google.spanner.admin.instance.v1.Instance], if successful. - * - *

Authorization requires `spanner.instances.update` permission on resource - * [name][google.spanner.admin.instance.v1.Instance.name]. - * - *

Sample code: - * - *


-   * try (InstanceAdminClient instanceAdminClient = InstanceAdminClient.create()) {
-   *   Instance instance = Instance.newBuilder().build();
-   *   FieldMask fieldMask = FieldMask.newBuilder().build();
-   *   UpdateInstanceRequest request = UpdateInstanceRequest.newBuilder()
-   *     .setInstance(instance)
-   *     .setFieldMask(fieldMask)
-   *     .build();
-   *   Instance response = instanceAdminClient.updateInstanceAsync(request).get();
-   * }
-   * 
- * - * @param request The request object containing all of the parameters for the API call. - * @throws com.google.api.gax.rpc.ApiException if the remote call fails - */ - @BetaApi( - "The surface for long-running operations is not stable yet and may change in the future.") - public final OperationFuture updateInstanceAsync( - UpdateInstanceRequest request) { - return updateInstanceOperationCallable().futureCall(request); - } - - // AUTO-GENERATED DOCUMENTATION AND METHOD - /** - * Updates an instance, and begins allocating or releasing resources as requested. The returned - * [long-running operation][google.longrunning.Operation] can be used to track the progress of - * updating the instance. If the named instance does not exist, returns `NOT_FOUND`. - * - *

Immediately upon completion of this request: - * - *

* For resource types for which a decrease in the instance's allocation has been - * requested, billing is based on the newly-requested level. - * - *

Until completion of the returned operation: - * - *

* Cancelling the operation sets its metadata's - * [cancel_time][google.spanner.admin.instance.v1.UpdateInstanceMetadata.cancel_time], and begins - * restoring resources to their pre-request values. The operation is guaranteed to succeed at - * undoing all resource changes, after which point it terminates with a `CANCELLED` status. * - * All other attempts to modify the instance are rejected. * Reading the instance via the API - * continues to give the pre-request resource levels. - * - *

Upon completion of the returned operation: - * - *

* Billing begins for all successfully-allocated resources (some types may have lower - * than the requested levels). * All newly-reserved resources are available for serving the - * instance's tables. * The instance's new resource levels are readable via the API. - * - *

The returned [long-running operation][google.longrunning.Operation] will have a name of the - * format `<instance_name>/operations/<operation_id>` and can be used to track the - * instance modification. The [metadata][google.longrunning.Operation.metadata] field type is - * [UpdateInstanceMetadata][google.spanner.admin.instance.v1.UpdateInstanceMetadata]. The - * [response][google.longrunning.Operation.response] field type is - * [Instance][google.spanner.admin.instance.v1.Instance], if successful. - * - *

Authorization requires `spanner.instances.update` permission on resource - * [name][google.spanner.admin.instance.v1.Instance.name]. - * - *

Sample code: - * - *


-   * try (InstanceAdminClient instanceAdminClient = InstanceAdminClient.create()) {
-   *   Instance instance = Instance.newBuilder().build();
-   *   FieldMask fieldMask = FieldMask.newBuilder().build();
-   *   UpdateInstanceRequest request = UpdateInstanceRequest.newBuilder()
-   *     .setInstance(instance)
-   *     .setFieldMask(fieldMask)
-   *     .build();
-   *   OperationFuture<Instance, UpdateInstanceMetadata> future = instanceAdminClient.updateInstanceOperationCallable().futureCall(request);
-   *   // Do something
-   *   Instance response = future.get();
-   * }
-   * 
- */ - @BetaApi("The surface for use by generated code is not stable yet and may change in the future.") - public final OperationCallable - updateInstanceOperationCallable() { - return stub.updateInstanceOperationCallable(); - } - - // AUTO-GENERATED DOCUMENTATION AND METHOD - /** - * Updates an instance, and begins allocating or releasing resources as requested. The returned - * [long-running operation][google.longrunning.Operation] can be used to track the progress of - * updating the instance. If the named instance does not exist, returns `NOT_FOUND`. - * - *

Immediately upon completion of this request: - * - *

* For resource types for which a decrease in the instance's allocation has been - * requested, billing is based on the newly-requested level. - * - *

Until completion of the returned operation: - * - *

* Cancelling the operation sets its metadata's - * [cancel_time][google.spanner.admin.instance.v1.UpdateInstanceMetadata.cancel_time], and begins - * restoring resources to their pre-request values. The operation is guaranteed to succeed at - * undoing all resource changes, after which point it terminates with a `CANCELLED` status. * - * All other attempts to modify the instance are rejected. * Reading the instance via the API - * continues to give the pre-request resource levels. - * - *

Upon completion of the returned operation: - * - *

* Billing begins for all successfully-allocated resources (some types may have lower - * than the requested levels). * All newly-reserved resources are available for serving the - * instance's tables. * The instance's new resource levels are readable via the API. - * - *

The returned [long-running operation][google.longrunning.Operation] will have a name of the - * format `<instance_name>/operations/<operation_id>` and can be used to track the - * instance modification. The [metadata][google.longrunning.Operation.metadata] field type is - * [UpdateInstanceMetadata][google.spanner.admin.instance.v1.UpdateInstanceMetadata]. The - * [response][google.longrunning.Operation.response] field type is - * [Instance][google.spanner.admin.instance.v1.Instance], if successful. - * - *

Authorization requires `spanner.instances.update` permission on resource - * [name][google.spanner.admin.instance.v1.Instance.name]. - * - *

Sample code: - * - *


-   * try (InstanceAdminClient instanceAdminClient = InstanceAdminClient.create()) {
-   *   Instance instance = Instance.newBuilder().build();
-   *   FieldMask fieldMask = FieldMask.newBuilder().build();
-   *   UpdateInstanceRequest request = UpdateInstanceRequest.newBuilder()
-   *     .setInstance(instance)
-   *     .setFieldMask(fieldMask)
-   *     .build();
-   *   ApiFuture<Operation> future = instanceAdminClient.updateInstanceCallable().futureCall(request);
-   *   // Do something
-   *   Operation response = future.get();
-   * }
-   * 
- */ - public final UnaryCallable updateInstanceCallable() { - return stub.updateInstanceCallable(); - } - // AUTO-GENERATED DOCUMENTATION AND METHOD /** * Deletes an instance. diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/admin/instance/v1/InstanceAdminSettings.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/admin/instance/v1/InstanceAdminSettings.java index b64951873c..f9cdab9d26 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/admin/instance/v1/InstanceAdminSettings.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/admin/instance/v1/InstanceAdminSettings.java @@ -89,29 +89,6 @@ @Generated("by gapic-generator") @BetaApi public class InstanceAdminSettings extends ClientSettings { - /** Returns the object with the settings used for calls to listInstanceConfigs. */ - public PagedCallSettings< - ListInstanceConfigsRequest, ListInstanceConfigsResponse, ListInstanceConfigsPagedResponse> - listInstanceConfigsSettings() { - return ((InstanceAdminStubSettings) getStubSettings()).listInstanceConfigsSettings(); - } - - /** Returns the object with the settings used for calls to getInstanceConfig. */ - public UnaryCallSettings getInstanceConfigSettings() { - return ((InstanceAdminStubSettings) getStubSettings()).getInstanceConfigSettings(); - } - - /** Returns the object with the settings used for calls to listInstances. */ - public PagedCallSettings - listInstancesSettings() { - return ((InstanceAdminStubSettings) getStubSettings()).listInstancesSettings(); - } - - /** Returns the object with the settings used for calls to getInstance. */ - public UnaryCallSettings getInstanceSettings() { - return ((InstanceAdminStubSettings) getStubSettings()).getInstanceSettings(); - } - /** Returns the object with the settings used for calls to createInstance. */ public UnaryCallSettings createInstanceSettings() { return ((InstanceAdminStubSettings) getStubSettings()).createInstanceSettings(); @@ -138,6 +115,29 @@ public UnaryCallSettings updateInstanceSetting return ((InstanceAdminStubSettings) getStubSettings()).updateInstanceOperationSettings(); } + /** Returns the object with the settings used for calls to listInstanceConfigs. */ + public PagedCallSettings< + ListInstanceConfigsRequest, ListInstanceConfigsResponse, ListInstanceConfigsPagedResponse> + listInstanceConfigsSettings() { + return ((InstanceAdminStubSettings) getStubSettings()).listInstanceConfigsSettings(); + } + + /** Returns the object with the settings used for calls to getInstanceConfig. */ + public UnaryCallSettings getInstanceConfigSettings() { + return ((InstanceAdminStubSettings) getStubSettings()).getInstanceConfigSettings(); + } + + /** Returns the object with the settings used for calls to listInstances. */ + public PagedCallSettings + listInstancesSettings() { + return ((InstanceAdminStubSettings) getStubSettings()).listInstancesSettings(); + } + + /** Returns the object with the settings used for calls to getInstance. */ + public UnaryCallSettings getInstanceSettings() { + return ((InstanceAdminStubSettings) getStubSettings()).getInstanceSettings(); + } + /** Returns the object with the settings used for calls to deleteInstance. */ public UnaryCallSettings deleteInstanceSettings() { return ((InstanceAdminStubSettings) getStubSettings()).deleteInstanceSettings(); @@ -256,6 +256,32 @@ public Builder applyToAllUnaryMethods( return this; } + /** Returns the builder for the settings used for calls to createInstance. */ + public UnaryCallSettings.Builder createInstanceSettings() { + return getStubSettingsBuilder().createInstanceSettings(); + } + + /** Returns the builder for the settings used for calls to createInstance. */ + @BetaApi( + "The surface for long-running operations is not stable yet and may change in the future.") + public OperationCallSettings.Builder + createInstanceOperationSettings() { + return getStubSettingsBuilder().createInstanceOperationSettings(); + } + + /** Returns the builder for the settings used for calls to updateInstance. */ + public UnaryCallSettings.Builder updateInstanceSettings() { + return getStubSettingsBuilder().updateInstanceSettings(); + } + + /** Returns the builder for the settings used for calls to updateInstance. */ + @BetaApi( + "The surface for long-running operations is not stable yet and may change in the future.") + public OperationCallSettings.Builder + updateInstanceOperationSettings() { + return getStubSettingsBuilder().updateInstanceOperationSettings(); + } + /** Returns the builder for the settings used for calls to listInstanceConfigs. */ public PagedCallSettings.Builder< ListInstanceConfigsRequest, @@ -283,32 +309,6 @@ public UnaryCallSettings.Builder getInstanceSettin return getStubSettingsBuilder().getInstanceSettings(); } - /** Returns the builder for the settings used for calls to createInstance. */ - public UnaryCallSettings.Builder createInstanceSettings() { - return getStubSettingsBuilder().createInstanceSettings(); - } - - /** Returns the builder for the settings used for calls to createInstance. */ - @BetaApi( - "The surface for long-running operations is not stable yet and may change in the future.") - public OperationCallSettings.Builder - createInstanceOperationSettings() { - return getStubSettingsBuilder().createInstanceOperationSettings(); - } - - /** Returns the builder for the settings used for calls to updateInstance. */ - public UnaryCallSettings.Builder updateInstanceSettings() { - return getStubSettingsBuilder().updateInstanceSettings(); - } - - /** Returns the builder for the settings used for calls to updateInstance. */ - @BetaApi( - "The surface for long-running operations is not stable yet and may change in the future.") - public OperationCallSettings.Builder - updateInstanceOperationSettings() { - return getStubSettingsBuilder().updateInstanceOperationSettings(); - } - /** Returns the builder for the settings used for calls to deleteInstance. */ public UnaryCallSettings.Builder deleteInstanceSettings() { return getStubSettingsBuilder().deleteInstanceSettings(); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/admin/instance/v1/stub/GrpcInstanceAdminStub.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/admin/instance/v1/stub/GrpcInstanceAdminStub.java index bbc144e69a..f64f7275cb 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/admin/instance/v1/stub/GrpcInstanceAdminStub.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/admin/instance/v1/stub/GrpcInstanceAdminStub.java @@ -66,6 +66,24 @@ @BetaApi("A restructuring of stub classes is planned, so this may break in the future") public class GrpcInstanceAdminStub extends InstanceAdminStub { + private static final MethodDescriptor + createInstanceMethodDescriptor = + MethodDescriptor.newBuilder() + .setType(MethodDescriptor.MethodType.UNARY) + .setFullMethodName("google.spanner.admin.instance.v1.InstanceAdmin/CreateInstance") + .setRequestMarshaller( + ProtoUtils.marshaller(CreateInstanceRequest.getDefaultInstance())) + .setResponseMarshaller(ProtoUtils.marshaller(Operation.getDefaultInstance())) + .build(); + private static final MethodDescriptor + updateInstanceMethodDescriptor = + MethodDescriptor.newBuilder() + .setType(MethodDescriptor.MethodType.UNARY) + .setFullMethodName("google.spanner.admin.instance.v1.InstanceAdmin/UpdateInstance") + .setRequestMarshaller( + ProtoUtils.marshaller(UpdateInstanceRequest.getDefaultInstance())) + .setResponseMarshaller(ProtoUtils.marshaller(Operation.getDefaultInstance())) + .build(); private static final MethodDescriptor listInstanceConfigsMethodDescriptor = MethodDescriptor.newBuilder() @@ -103,24 +121,6 @@ public class GrpcInstanceAdminStub extends InstanceAdminStub { .setRequestMarshaller(ProtoUtils.marshaller(GetInstanceRequest.getDefaultInstance())) .setResponseMarshaller(ProtoUtils.marshaller(Instance.getDefaultInstance())) .build(); - private static final MethodDescriptor - createInstanceMethodDescriptor = - MethodDescriptor.newBuilder() - .setType(MethodDescriptor.MethodType.UNARY) - .setFullMethodName("google.spanner.admin.instance.v1.InstanceAdmin/CreateInstance") - .setRequestMarshaller( - ProtoUtils.marshaller(CreateInstanceRequest.getDefaultInstance())) - .setResponseMarshaller(ProtoUtils.marshaller(Operation.getDefaultInstance())) - .build(); - private static final MethodDescriptor - updateInstanceMethodDescriptor = - MethodDescriptor.newBuilder() - .setType(MethodDescriptor.MethodType.UNARY) - .setFullMethodName("google.spanner.admin.instance.v1.InstanceAdmin/UpdateInstance") - .setRequestMarshaller( - ProtoUtils.marshaller(UpdateInstanceRequest.getDefaultInstance())) - .setResponseMarshaller(ProtoUtils.marshaller(Operation.getDefaultInstance())) - .build(); private static final MethodDescriptor deleteInstanceMethodDescriptor = MethodDescriptor.newBuilder() @@ -159,6 +159,12 @@ public class GrpcInstanceAdminStub extends InstanceAdminStub { private final BackgroundResource backgroundResources; private final GrpcOperationsStub operationsStub; + private final UnaryCallable createInstanceCallable; + private final OperationCallable + createInstanceOperationCallable; + private final UnaryCallable updateInstanceCallable; + private final OperationCallable + updateInstanceOperationCallable; private final UnaryCallable listInstanceConfigsCallable; private final UnaryCallable @@ -168,12 +174,6 @@ public class GrpcInstanceAdminStub extends InstanceAdminStub { private final UnaryCallable listInstancesPagedCallable; private final UnaryCallable getInstanceCallable; - private final UnaryCallable createInstanceCallable; - private final OperationCallable - createInstanceOperationCallable; - private final UnaryCallable updateInstanceCallable; - private final OperationCallable - updateInstanceOperationCallable; private final UnaryCallable deleteInstanceCallable; private final UnaryCallable setIamPolicyCallable; private final UnaryCallable getIamPolicyCallable; @@ -220,6 +220,32 @@ protected GrpcInstanceAdminStub( this.callableFactory = callableFactory; this.operationsStub = GrpcOperationsStub.create(clientContext, callableFactory); + GrpcCallSettings createInstanceTransportSettings = + GrpcCallSettings.newBuilder() + .setMethodDescriptor(createInstanceMethodDescriptor) + .setParamsExtractor( + new RequestParamsExtractor() { + @Override + public Map extract(CreateInstanceRequest request) { + ImmutableMap.Builder params = ImmutableMap.builder(); + params.put("parent", String.valueOf(request.getParent())); + return params.build(); + } + }) + .build(); + GrpcCallSettings updateInstanceTransportSettings = + GrpcCallSettings.newBuilder() + .setMethodDescriptor(updateInstanceMethodDescriptor) + .setParamsExtractor( + new RequestParamsExtractor() { + @Override + public Map extract(UpdateInstanceRequest request) { + ImmutableMap.Builder params = ImmutableMap.builder(); + params.put("instance.name", String.valueOf(request.getInstance().getName())); + return params.build(); + } + }) + .build(); GrpcCallSettings listInstanceConfigsTransportSettings = GrpcCallSettings.newBuilder() @@ -273,32 +299,6 @@ public Map extract(GetInstanceRequest request) { } }) .build(); - GrpcCallSettings createInstanceTransportSettings = - GrpcCallSettings.newBuilder() - .setMethodDescriptor(createInstanceMethodDescriptor) - .setParamsExtractor( - new RequestParamsExtractor() { - @Override - public Map extract(CreateInstanceRequest request) { - ImmutableMap.Builder params = ImmutableMap.builder(); - params.put("parent", String.valueOf(request.getParent())); - return params.build(); - } - }) - .build(); - GrpcCallSettings updateInstanceTransportSettings = - GrpcCallSettings.newBuilder() - .setMethodDescriptor(updateInstanceMethodDescriptor) - .setParamsExtractor( - new RequestParamsExtractor() { - @Override - public Map extract(UpdateInstanceRequest request) { - ImmutableMap.Builder params = ImmutableMap.builder(); - params.put("instance.name", String.valueOf(request.getInstance().getName())); - return params.build(); - } - }) - .build(); GrpcCallSettings deleteInstanceTransportSettings = GrpcCallSettings.newBuilder() .setMethodDescriptor(deleteInstanceMethodDescriptor) @@ -353,6 +353,24 @@ public Map extract(TestIamPermissionsRequest request) { }) .build(); + this.createInstanceCallable = + callableFactory.createUnaryCallable( + createInstanceTransportSettings, settings.createInstanceSettings(), clientContext); + this.createInstanceOperationCallable = + callableFactory.createOperationCallable( + createInstanceTransportSettings, + settings.createInstanceOperationSettings(), + clientContext, + this.operationsStub); + this.updateInstanceCallable = + callableFactory.createUnaryCallable( + updateInstanceTransportSettings, settings.updateInstanceSettings(), clientContext); + this.updateInstanceOperationCallable = + callableFactory.createOperationCallable( + updateInstanceTransportSettings, + settings.updateInstanceOperationSettings(), + clientContext, + this.operationsStub); this.listInstanceConfigsCallable = callableFactory.createUnaryCallable( listInstanceConfigsTransportSettings, @@ -377,24 +395,6 @@ public Map extract(TestIamPermissionsRequest request) { this.getInstanceCallable = callableFactory.createUnaryCallable( getInstanceTransportSettings, settings.getInstanceSettings(), clientContext); - this.createInstanceCallable = - callableFactory.createUnaryCallable( - createInstanceTransportSettings, settings.createInstanceSettings(), clientContext); - this.createInstanceOperationCallable = - callableFactory.createOperationCallable( - createInstanceTransportSettings, - settings.createInstanceOperationSettings(), - clientContext, - this.operationsStub); - this.updateInstanceCallable = - callableFactory.createUnaryCallable( - updateInstanceTransportSettings, settings.updateInstanceSettings(), clientContext); - this.updateInstanceOperationCallable = - callableFactory.createOperationCallable( - updateInstanceTransportSettings, - settings.updateInstanceOperationSettings(), - clientContext, - this.operationsStub); this.deleteInstanceCallable = callableFactory.createUnaryCallable( deleteInstanceTransportSettings, settings.deleteInstanceSettings(), clientContext); @@ -418,6 +418,26 @@ public GrpcOperationsStub getOperationsStub() { return operationsStub; } + @BetaApi("The surface for use by generated code is not stable yet and may change in the future.") + public OperationCallable + createInstanceOperationCallable() { + return createInstanceOperationCallable; + } + + public UnaryCallable createInstanceCallable() { + return createInstanceCallable; + } + + @BetaApi("The surface for use by generated code is not stable yet and may change in the future.") + public OperationCallable + updateInstanceOperationCallable() { + return updateInstanceOperationCallable; + } + + public UnaryCallable updateInstanceCallable() { + return updateInstanceCallable; + } + public UnaryCallable listInstanceConfigsPagedCallable() { return listInstanceConfigsPagedCallable; @@ -445,26 +465,6 @@ public UnaryCallable getInstanceCallable() { return getInstanceCallable; } - @BetaApi("The surface for use by generated code is not stable yet and may change in the future.") - public OperationCallable - createInstanceOperationCallable() { - return createInstanceOperationCallable; - } - - public UnaryCallable createInstanceCallable() { - return createInstanceCallable; - } - - @BetaApi("The surface for use by generated code is not stable yet and may change in the future.") - public OperationCallable - updateInstanceOperationCallable() { - return updateInstanceOperationCallable; - } - - public UnaryCallable updateInstanceCallable() { - return updateInstanceCallable; - } - public UnaryCallable deleteInstanceCallable() { return deleteInstanceCallable; } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/admin/instance/v1/stub/InstanceAdminStub.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/admin/instance/v1/stub/InstanceAdminStub.java index 77334eb1a2..085a696307 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/admin/instance/v1/stub/InstanceAdminStub.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/admin/instance/v1/stub/InstanceAdminStub.java @@ -60,6 +60,26 @@ public OperationsStub getOperationsStub() { throw new UnsupportedOperationException("Not implemented: getOperationsStub()"); } + @BetaApi("The surface for use by generated code is not stable yet and may change in the future.") + public OperationCallable + createInstanceOperationCallable() { + throw new UnsupportedOperationException("Not implemented: createInstanceOperationCallable()"); + } + + public UnaryCallable createInstanceCallable() { + throw new UnsupportedOperationException("Not implemented: createInstanceCallable()"); + } + + @BetaApi("The surface for use by generated code is not stable yet and may change in the future.") + public OperationCallable + updateInstanceOperationCallable() { + throw new UnsupportedOperationException("Not implemented: updateInstanceOperationCallable()"); + } + + public UnaryCallable updateInstanceCallable() { + throw new UnsupportedOperationException("Not implemented: updateInstanceCallable()"); + } + public UnaryCallable listInstanceConfigsPagedCallable() { throw new UnsupportedOperationException("Not implemented: listInstanceConfigsPagedCallable()"); @@ -87,26 +107,6 @@ public UnaryCallable getInstanceCallable() { throw new UnsupportedOperationException("Not implemented: getInstanceCallable()"); } - @BetaApi("The surface for use by generated code is not stable yet and may change in the future.") - public OperationCallable - createInstanceOperationCallable() { - throw new UnsupportedOperationException("Not implemented: createInstanceOperationCallable()"); - } - - public UnaryCallable createInstanceCallable() { - throw new UnsupportedOperationException("Not implemented: createInstanceCallable()"); - } - - @BetaApi("The surface for use by generated code is not stable yet and may change in the future.") - public OperationCallable - updateInstanceOperationCallable() { - throw new UnsupportedOperationException("Not implemented: updateInstanceOperationCallable()"); - } - - public UnaryCallable updateInstanceCallable() { - throw new UnsupportedOperationException("Not implemented: updateInstanceCallable()"); - } - public UnaryCallable deleteInstanceCallable() { throw new UnsupportedOperationException("Not implemented: deleteInstanceCallable()"); } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/admin/instance/v1/stub/InstanceAdminStubSettings.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/admin/instance/v1/stub/InstanceAdminStubSettings.java index c975119498..4c31214912 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/admin/instance/v1/stub/InstanceAdminStubSettings.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/admin/instance/v1/stub/InstanceAdminStubSettings.java @@ -114,6 +114,12 @@ public class InstanceAdminStubSettings extends StubSettings createInstanceSettings; + private final OperationCallSettings + createInstanceOperationSettings; + private final UnaryCallSettings updateInstanceSettings; + private final OperationCallSettings + updateInstanceOperationSettings; private final PagedCallSettings< ListInstanceConfigsRequest, ListInstanceConfigsResponse, ListInstanceConfigsPagedResponse> listInstanceConfigsSettings; @@ -123,41 +129,12 @@ public class InstanceAdminStubSettings extends StubSettings listInstancesSettings; private final UnaryCallSettings getInstanceSettings; - private final UnaryCallSettings createInstanceSettings; - private final OperationCallSettings - createInstanceOperationSettings; - private final UnaryCallSettings updateInstanceSettings; - private final OperationCallSettings - updateInstanceOperationSettings; private final UnaryCallSettings deleteInstanceSettings; private final UnaryCallSettings setIamPolicySettings; private final UnaryCallSettings getIamPolicySettings; private final UnaryCallSettings testIamPermissionsSettings; - /** Returns the object with the settings used for calls to listInstanceConfigs. */ - public PagedCallSettings< - ListInstanceConfigsRequest, ListInstanceConfigsResponse, ListInstanceConfigsPagedResponse> - listInstanceConfigsSettings() { - return listInstanceConfigsSettings; - } - - /** Returns the object with the settings used for calls to getInstanceConfig. */ - public UnaryCallSettings getInstanceConfigSettings() { - return getInstanceConfigSettings; - } - - /** Returns the object with the settings used for calls to listInstances. */ - public PagedCallSettings - listInstancesSettings() { - return listInstancesSettings; - } - - /** Returns the object with the settings used for calls to getInstance. */ - public UnaryCallSettings getInstanceSettings() { - return getInstanceSettings; - } - /** Returns the object with the settings used for calls to createInstance. */ public UnaryCallSettings createInstanceSettings() { return createInstanceSettings; @@ -182,6 +159,29 @@ public UnaryCallSettings updateInstanceSetting return updateInstanceOperationSettings; } + /** Returns the object with the settings used for calls to listInstanceConfigs. */ + public PagedCallSettings< + ListInstanceConfigsRequest, ListInstanceConfigsResponse, ListInstanceConfigsPagedResponse> + listInstanceConfigsSettings() { + return listInstanceConfigsSettings; + } + + /** Returns the object with the settings used for calls to getInstanceConfig. */ + public UnaryCallSettings getInstanceConfigSettings() { + return getInstanceConfigSettings; + } + + /** Returns the object with the settings used for calls to listInstances. */ + public PagedCallSettings + listInstancesSettings() { + return listInstancesSettings; + } + + /** Returns the object with the settings used for calls to getInstance. */ + public UnaryCallSettings getInstanceSettings() { + return getInstanceSettings; + } + /** Returns the object with the settings used for calls to deleteInstance. */ public UnaryCallSettings deleteInstanceSettings() { return deleteInstanceSettings; @@ -272,14 +272,14 @@ public Builder toBuilder() { protected InstanceAdminStubSettings(Builder settingsBuilder) throws IOException { super(settingsBuilder); - listInstanceConfigsSettings = settingsBuilder.listInstanceConfigsSettings().build(); - getInstanceConfigSettings = settingsBuilder.getInstanceConfigSettings().build(); - listInstancesSettings = settingsBuilder.listInstancesSettings().build(); - getInstanceSettings = settingsBuilder.getInstanceSettings().build(); createInstanceSettings = settingsBuilder.createInstanceSettings().build(); createInstanceOperationSettings = settingsBuilder.createInstanceOperationSettings().build(); updateInstanceSettings = settingsBuilder.updateInstanceSettings().build(); updateInstanceOperationSettings = settingsBuilder.updateInstanceOperationSettings().build(); + listInstanceConfigsSettings = settingsBuilder.listInstanceConfigsSettings().build(); + getInstanceConfigSettings = settingsBuilder.getInstanceConfigSettings().build(); + listInstancesSettings = settingsBuilder.listInstancesSettings().build(); + getInstanceSettings = settingsBuilder.getInstanceSettings().build(); deleteInstanceSettings = settingsBuilder.deleteInstanceSettings().build(); setIamPolicySettings = settingsBuilder.setIamPolicySettings().build(); getIamPolicySettings = settingsBuilder.getIamPolicySettings().build(); @@ -404,6 +404,16 @@ public ApiFuture getFuturePagedResponse( public static class Builder extends StubSettings.Builder { private final ImmutableList> unaryMethodSettingsBuilders; + private final UnaryCallSettings.Builder + createInstanceSettings; + private final OperationCallSettings.Builder< + CreateInstanceRequest, Instance, CreateInstanceMetadata> + createInstanceOperationSettings; + private final UnaryCallSettings.Builder + updateInstanceSettings; + private final OperationCallSettings.Builder< + UpdateInstanceRequest, Instance, UpdateInstanceMetadata> + updateInstanceOperationSettings; private final PagedCallSettings.Builder< ListInstanceConfigsRequest, ListInstanceConfigsResponse, @@ -415,16 +425,6 @@ public static class Builder extends StubSettings.Builder listInstancesSettings; private final UnaryCallSettings.Builder getInstanceSettings; - private final UnaryCallSettings.Builder - createInstanceSettings; - private final OperationCallSettings.Builder< - CreateInstanceRequest, Instance, CreateInstanceMetadata> - createInstanceOperationSettings; - private final UnaryCallSettings.Builder - updateInstanceSettings; - private final OperationCallSettings.Builder< - UpdateInstanceRequest, Instance, UpdateInstanceMetadata> - updateInstanceOperationSettings; private final UnaryCallSettings.Builder deleteInstanceSettings; private final UnaryCallSettings.Builder setIamPolicySettings; private final UnaryCallSettings.Builder getIamPolicySettings; @@ -438,11 +438,20 @@ public static class Builder extends StubSettings.Builder> definitions = ImmutableMap.builder(); definitions.put( - "idempotent", + "retry_policy_1_codes", ImmutableSet.copyOf( Lists.newArrayList( - StatusCode.Code.DEADLINE_EXCEEDED, StatusCode.Code.UNAVAILABLE))); - definitions.put("non_idempotent", ImmutableSet.copyOf(Lists.newArrayList())); + StatusCode.Code.UNAVAILABLE, StatusCode.Code.DEADLINE_EXCEEDED))); + definitions.put( + "no_retry_2_codes", ImmutableSet.copyOf(Lists.newArrayList())); + definitions.put("no_retry_codes", ImmutableSet.copyOf(Lists.newArrayList())); + definitions.put( + "retry_policy_2_codes", + ImmutableSet.copyOf( + Lists.newArrayList( + StatusCode.Code.UNAVAILABLE, StatusCode.Code.DEADLINE_EXCEEDED))); + definitions.put( + "no_retry_1_codes", ImmutableSet.copyOf(Lists.newArrayList())); RETRYABLE_CODE_DEFINITIONS = definitions.build(); } @@ -456,12 +465,41 @@ public static class Builder extends StubSettings.Builder>of( + createInstanceSettings, + updateInstanceSettings, listInstanceConfigsSettings, getInstanceConfigSettings, listInstancesSettings, getInstanceSettings, - createInstanceSettings, - updateInstanceSettings, deleteInstanceSettings, setIamPolicySettings, getIamPolicySettings, @@ -525,61 +563,61 @@ private static Builder createDefault() { private static Builder initDefaults(Builder builder) { builder - .listInstanceConfigsSettings() - .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("idempotent")) - .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("default")); + .createInstanceSettings() + .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("no_retry_1_codes")) + .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("no_retry_1_params")); builder - .getInstanceConfigSettings() - .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("idempotent")) - .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("default")); + .updateInstanceSettings() + .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("no_retry_1_codes")) + .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("no_retry_1_params")); builder - .listInstancesSettings() - .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("idempotent")) - .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("default")); + .listInstanceConfigsSettings() + .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("retry_policy_1_codes")) + .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("retry_policy_1_params")); builder - .getInstanceSettings() - .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("idempotent")) - .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("default")); + .getInstanceConfigSettings() + .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("retry_policy_1_codes")) + .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("retry_policy_1_params")); builder - .createInstanceSettings() - .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("non_idempotent")) - .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("default")); + .listInstancesSettings() + .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("retry_policy_1_codes")) + .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("retry_policy_1_params")); builder - .updateInstanceSettings() - .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("non_idempotent")) - .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("default")); + .getInstanceSettings() + .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("retry_policy_1_codes")) + .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("retry_policy_1_params")); builder .deleteInstanceSettings() - .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("idempotent")) - .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("default")); + .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("retry_policy_1_codes")) + .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("retry_policy_1_params")); builder .setIamPolicySettings() - .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("non_idempotent")) - .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("default")); + .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("no_retry_2_codes")) + .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("no_retry_2_params")); builder .getIamPolicySettings() - .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("idempotent")) - .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("default")); + .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("retry_policy_2_codes")) + .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("retry_policy_2_params")); builder .testIamPermissionsSettings() - .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("non_idempotent")) - .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("default")); + .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("no_retry_2_codes")) + .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("no_retry_2_params")); builder .createInstanceOperationSettings() .setInitialCallSettings( UnaryCallSettings .newUnaryCallSettingsBuilder() - .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("non_idempotent")) - .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("default")) + .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("no_retry_1_codes")) + .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("no_retry_1_params")) .build()) .setResponseTransformer( ProtoOperationTransformers.ResponseTransformer.create(Instance.class)) @@ -601,8 +639,8 @@ private static Builder initDefaults(Builder builder) { .setInitialCallSettings( UnaryCallSettings .newUnaryCallSettingsBuilder() - .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("non_idempotent")) - .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("default")) + .setRetryableCodes(RETRYABLE_CODE_DEFINITIONS.get("no_retry_1_codes")) + .setRetrySettings(RETRY_PARAM_DEFINITIONS.get("no_retry_1_params")) .build()) .setResponseTransformer( ProtoOperationTransformers.ResponseTransformer.create(Instance.class)) @@ -626,14 +664,14 @@ private static Builder initDefaults(Builder builder) { protected Builder(InstanceAdminStubSettings settings) { super(settings); - listInstanceConfigsSettings = settings.listInstanceConfigsSettings.toBuilder(); - getInstanceConfigSettings = settings.getInstanceConfigSettings.toBuilder(); - listInstancesSettings = settings.listInstancesSettings.toBuilder(); - getInstanceSettings = settings.getInstanceSettings.toBuilder(); createInstanceSettings = settings.createInstanceSettings.toBuilder(); createInstanceOperationSettings = settings.createInstanceOperationSettings.toBuilder(); updateInstanceSettings = settings.updateInstanceSettings.toBuilder(); updateInstanceOperationSettings = settings.updateInstanceOperationSettings.toBuilder(); + listInstanceConfigsSettings = settings.listInstanceConfigsSettings.toBuilder(); + getInstanceConfigSettings = settings.getInstanceConfigSettings.toBuilder(); + listInstancesSettings = settings.listInstancesSettings.toBuilder(); + getInstanceSettings = settings.getInstanceSettings.toBuilder(); deleteInstanceSettings = settings.deleteInstanceSettings.toBuilder(); setIamPolicySettings = settings.setIamPolicySettings.toBuilder(); getIamPolicySettings = settings.getIamPolicySettings.toBuilder(); @@ -641,12 +679,12 @@ protected Builder(InstanceAdminStubSettings settings) { unaryMethodSettingsBuilders = ImmutableList.>of( + createInstanceSettings, + updateInstanceSettings, listInstanceConfigsSettings, getInstanceConfigSettings, listInstancesSettings, getInstanceSettings, - createInstanceSettings, - updateInstanceSettings, deleteInstanceSettings, setIamPolicySettings, getIamPolicySettings, @@ -669,6 +707,32 @@ public Builder applyToAllUnaryMethods( return unaryMethodSettingsBuilders; } + /** Returns the builder for the settings used for calls to createInstance. */ + public UnaryCallSettings.Builder createInstanceSettings() { + return createInstanceSettings; + } + + /** Returns the builder for the settings used for calls to createInstance. */ + @BetaApi( + "The surface for use by generated code is not stable yet and may change in the future.") + public OperationCallSettings.Builder + createInstanceOperationSettings() { + return createInstanceOperationSettings; + } + + /** Returns the builder for the settings used for calls to updateInstance. */ + public UnaryCallSettings.Builder updateInstanceSettings() { + return updateInstanceSettings; + } + + /** Returns the builder for the settings used for calls to updateInstance. */ + @BetaApi( + "The surface for use by generated code is not stable yet and may change in the future.") + public OperationCallSettings.Builder + updateInstanceOperationSettings() { + return updateInstanceOperationSettings; + } + /** Returns the builder for the settings used for calls to listInstanceConfigs. */ public PagedCallSettings.Builder< ListInstanceConfigsRequest, @@ -696,32 +760,6 @@ public UnaryCallSettings.Builder getInstanceSettin return getInstanceSettings; } - /** Returns the builder for the settings used for calls to createInstance. */ - public UnaryCallSettings.Builder createInstanceSettings() { - return createInstanceSettings; - } - - /** Returns the builder for the settings used for calls to createInstance. */ - @BetaApi( - "The surface for use by generated code is not stable yet and may change in the future.") - public OperationCallSettings.Builder - createInstanceOperationSettings() { - return createInstanceOperationSettings; - } - - /** Returns the builder for the settings used for calls to updateInstance. */ - public UnaryCallSettings.Builder updateInstanceSettings() { - return updateInstanceSettings; - } - - /** Returns the builder for the settings used for calls to updateInstance. */ - @BetaApi( - "The surface for use by generated code is not stable yet and may change in the future.") - public OperationCallSettings.Builder - updateInstanceOperationSettings() { - return updateInstanceOperationSettings; - } - /** Returns the builder for the settings used for calls to deleteInstance. */ public UnaryCallSettings.Builder deleteInstanceSettings() { return deleteInstanceSettings; diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ChecksumResultSet.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ChecksumResultSet.java index 0170a9f572..d8e0e85844 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ChecksumResultSet.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ChecksumResultSet.java @@ -38,7 +38,6 @@ import com.google.common.hash.PrimitiveSink; import java.util.Objects; import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; /** * {@link ResultSet} implementation that keeps a running checksum that can be used to determine @@ -117,7 +116,7 @@ public boolean next() { } @VisibleForTesting - HashCode getChecksum() throws InterruptedException, ExecutionException { + HashCode getChecksum() { // HashCode is immutable and can be safely returned. return checksumCalculator.getChecksum(); } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/EncodingInterceptor.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/EncodingInterceptor.java new file mode 100644 index 0000000000..a30135533a --- /dev/null +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/EncodingInterceptor.java @@ -0,0 +1,50 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.cloud.spanner.spi.v1; + +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.ForwardingClientCall; +import io.grpc.Metadata; +import io.grpc.Metadata.Key; +import io.grpc.MethodDescriptor; + +class EncodingInterceptor implements ClientInterceptor { + private static final String RESPONSE_ENCODING_KEY_NAME = "x-response-encoding"; + private static final Key RESPONSE_ENCODING_KEY = + Metadata.Key.of(RESPONSE_ENCODING_KEY_NAME, Metadata.ASCII_STRING_MARSHALLER); + + private final String encoding; + + EncodingInterceptor(String encoding) { + this.encoding = encoding; + } + + @Override + public ClientCall interceptCall( + MethodDescriptor method, CallOptions callOptions, Channel next) { + return new ForwardingClientCall.SimpleForwardingClientCall( + next.newCall(method, callOptions)) { + @Override + public void start(Listener responseListener, Metadata headers) { + headers.put(RESPONSE_ENCODING_KEY, encoding); + super.start(responseListener, headers); + } + }; + } +} diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java index 97f4b5c88a..b1e571510d 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java @@ -38,13 +38,16 @@ import com.google.api.gax.rpc.InstantiatingWatchdogProvider; import com.google.api.gax.rpc.OperationCallable; import com.google.api.gax.rpc.ResponseObserver; +import com.google.api.gax.rpc.ServerStream; import com.google.api.gax.rpc.StatusCode; import com.google.api.gax.rpc.StreamController; import com.google.api.gax.rpc.TransportChannelProvider; +import com.google.api.gax.rpc.UnavailableException; import com.google.api.gax.rpc.WatchdogProvider; import com.google.api.pathtemplate.PathTemplate; import com.google.cloud.RetryHelper; import com.google.cloud.grpc.GrpcTransportOptions; +import com.google.cloud.spanner.ErrorCode; import com.google.cloud.spanner.SpannerException; import com.google.cloud.spanner.SpannerExceptionFactory; import com.google.cloud.spanner.SpannerOptions; @@ -54,6 +57,8 @@ import com.google.cloud.spanner.admin.database.v1.stub.GrpcDatabaseAdminStub; import com.google.cloud.spanner.admin.instance.v1.stub.GrpcInstanceAdminStub; import com.google.cloud.spanner.admin.instance.v1.stub.InstanceAdminStub; +import com.google.cloud.spanner.admin.instance.v1.stub.InstanceAdminStubSettings; +import com.google.cloud.spanner.spi.v1.SpannerRpc.Option; import com.google.cloud.spanner.v1.stub.GrpcSpannerStub; import com.google.cloud.spanner.v1.stub.SpannerStub; import com.google.cloud.spanner.v1.stub.SpannerStubSettings; @@ -134,6 +139,7 @@ import com.google.spanner.v1.Transaction; import io.grpc.CallCredentials; import io.grpc.Context; +import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; @@ -163,7 +169,8 @@ public class GapicSpannerRpc implements SpannerRpc { * down when the {@link SpannerRpc} is closed. */ private static final class ManagedInstantiatingExecutorProvider implements ExecutorProvider { - private static final int DEFAULT_THREAD_COUNT = 4; + // 4 Gapic clients * 4 channels per client. + private static final int DEFAULT_MIN_THREAD_COUNT = 16; private final List executors = new LinkedList<>(); private final ThreadFactory threadFactory; @@ -178,8 +185,10 @@ public boolean shouldAutoClose() { @Override public ScheduledExecutorService getExecutor() { + int numCpus = Runtime.getRuntime().availableProcessors(); + int numThreads = Math.max(DEFAULT_MIN_THREAD_COUNT, numCpus); ScheduledExecutorService executor = - new ScheduledThreadPoolExecutor(DEFAULT_THREAD_COUNT, threadFactory); + new ScheduledThreadPoolExecutor(numThreads, threadFactory); synchronized (this) { executors.add(executor); } @@ -226,6 +235,7 @@ private void awaitTermination() throws InterruptedException { private final String projectName; private final SpannerMetadataProvider metadataProvider; private final CallCredentialsProvider callCredentialsProvider; + private final String compressorName; private final Duration waitTimeout = systemProperty(PROPERTY_TIMEOUT_SECONDS, DEFAULT_TIMEOUT_SECONDS); private final Duration idleTimeout = @@ -279,6 +289,7 @@ public GapicSpannerRpc(final SpannerOptions options) { mergedHeaderProvider.getHeaders(), internalHeaderProviderBuilder.getResourceHeaderKey()); this.callCredentialsProvider = options.getCallCredentialsProvider(); + this.compressorName = options.getCompressorName(); // Create a managed executor provider. this.executorProvider = @@ -298,18 +309,20 @@ public GapicSpannerRpc(final SpannerOptions options) { .setMaxInboundMessageSize(MAX_MESSAGE_SIZE) .setMaxInboundMetadataSize(MAX_METADATA_SIZE) .setPoolSize(options.getNumChannels()) - .setExecutorProvider(executorProvider) + .setExecutor(executorProvider.getExecutor()) // Set a keepalive time of 120 seconds to help long running // commit GRPC calls succeed - .setKeepAliveTime(Duration.ofSeconds(GRPC_KEEPALIVE_SECONDS * 1000)) + .setKeepAliveTime(Duration.ofSeconds(GRPC_KEEPALIVE_SECONDS)) // Then check if SpannerOptions provides an InterceptorProvider. Create a default // SpannerInterceptorProvider if none is provided .setInterceptorProvider( - MoreObjects.firstNonNull( - options.getInterceptorProvider(), - SpannerInterceptorProvider.createDefault())) + SpannerInterceptorProvider.create( + MoreObjects.firstNonNull( + options.getInterceptorProvider(), + SpannerInterceptorProvider.createDefault())) + .withEncoding(compressorName)) .setHeaderProvider(mergedHeaderProvider) .build()); @@ -356,6 +369,21 @@ public GapicSpannerRpc(final SpannerOptions options) { .setStreamWatchdogProvider(watchdogProvider) .executeSqlSettings() .setRetrySettings(partitionedDmlRetrySettings); + // The stream watchdog will by default only check for a timeout every 10 seconds, so if the + // timeout is less than 10 seconds, it would be ignored for the first 10 seconds unless we + // also change the StreamWatchdogCheckInterval. + if (options + .getPartitionedDmlTimeout() + .dividedBy(10L) + .compareTo(pdmlSettings.getStreamWatchdogCheckInterval()) + < 0) { + pdmlSettings.setStreamWatchdogCheckInterval( + options.getPartitionedDmlTimeout().dividedBy(10)); + pdmlSettings.setStreamWatchdogProvider( + pdmlSettings + .getStreamWatchdogProvider() + .withCheckInterval(pdmlSettings.getStreamWatchdogCheckInterval())); + } this.partitionedDmlStub = GrpcSpannerStub.create(pdmlSettings.build()); this.instanceAdminStub = @@ -377,11 +405,57 @@ public GapicSpannerRpc(final SpannerOptions options) { .setStreamWatchdogProvider(watchdogProvider) .build(); this.databaseAdminStub = GrpcDatabaseAdminStub.create(this.databaseAdminStubSettings); + + // Check whether the SPANNER_EMULATOR_HOST env var has been set, and if so, if the emulator is + // actually running. + checkEmulatorConnection(options, channelProvider, credentialsProvider); } catch (Exception e) { throw newSpannerException(e); } } + private static void checkEmulatorConnection( + SpannerOptions options, + TransportChannelProvider channelProvider, + CredentialsProvider credentialsProvider) + throws IOException { + final String emulatorHost = System.getenv("SPANNER_EMULATOR_HOST"); + // Only do the check if the emulator environment variable has been set to localhost. + if (options.getChannelProvider() == null + && emulatorHost != null + && options.getHost() != null + && options.getHost().startsWith("http://localhost") + && options.getHost().endsWith(emulatorHost)) { + // Do a quick check to see if the emulator is actually running. + try { + InstanceAdminStubSettings.Builder testEmulatorSettings = + options + .getInstanceAdminStubSettings() + .toBuilder() + .setTransportChannelProvider(channelProvider) + .setCredentialsProvider(credentialsProvider); + testEmulatorSettings + .listInstanceConfigsSettings() + .setSimpleTimeoutNoRetries(Duration.ofSeconds(10L)); + try (GrpcInstanceAdminStub stub = + GrpcInstanceAdminStub.create(testEmulatorSettings.build())) { + stub.listInstanceConfigsCallable() + .call( + ListInstanceConfigsRequest.newBuilder() + .setParent(String.format("projects/%s", options.getProjectId())) + .build()); + } + } catch (UnavailableException e) { + throw SpannerExceptionFactory.newSpannerException( + ErrorCode.UNAVAILABLE, + String.format( + "The environment variable SPANNER_EMULATOR_HOST has been set to %s, but no running emulator could be found at that address.\n" + + "Did you forget to start the emulator, or to unset the environment variable?", + emulatorHost)); + } + } + } + private static final class OperationFutureRetryAlgorithm implements ResultRetryAlgorithm> { private static final ImmutableList RETRYABLE_CODES = @@ -1054,8 +1128,14 @@ public void cancel(String message) { @Override public ResultSet executeQuery(ExecuteSqlRequest request, @Nullable Map options) { + return get(executeQueryAsync(request, options)); + } + + @Override + public ApiFuture executeQueryAsync( + ExecuteSqlRequest request, @Nullable Map options) { GrpcCallContext context = newCallContext(options, request.getSession()); - return get(spannerStub.executeSqlCallable().futureCall(request, context)); + return spannerStub.executeSqlCallable().futureCall(request, context); } @Override @@ -1070,6 +1150,14 @@ public RetrySettings getPartitionedDmlRetrySettings() { return partitionedDmlRetrySettings; } + @Override + public ServerStream executeStreamingPartitionedDml( + ExecuteSqlRequest request, Map options, Duration timeout) { + GrpcCallContext context = newCallContext(options, request.getSession()); + context = context.withStreamWaitTimeout(timeout); + return partitionedDmlStub.executeStreamingSqlCallable().call(request, context); + } + @Override public StreamingCall executeQuery( ExecuteSqlRequest request, ResultStreamConsumer consumer, @Nullable Map options) { @@ -1095,30 +1183,52 @@ public void cancel(String message) { @Override public ExecuteBatchDmlResponse executeBatchDml( ExecuteBatchDmlRequest request, @Nullable Map options) { + return get(executeBatchDmlAsync(request, options)); + } + @Override + public ApiFuture executeBatchDmlAsync( + ExecuteBatchDmlRequest request, @Nullable Map options) { + GrpcCallContext context = newCallContext(options, request.getSession()); + return spannerStub.executeBatchDmlCallable().futureCall(request, context); + } + + @Override + public ApiFuture beginTransactionAsync( + BeginTransactionRequest request, @Nullable Map options) { GrpcCallContext context = newCallContext(options, request.getSession()); - return get(spannerStub.executeBatchDmlCallable().futureCall(request, context)); + return spannerStub.beginTransactionCallable().futureCall(request, context); } @Override public Transaction beginTransaction( BeginTransactionRequest request, @Nullable Map options) throws SpannerException { - GrpcCallContext context = newCallContext(options, request.getSession()); - return get(spannerStub.beginTransactionCallable().futureCall(request, context)); + return get(beginTransactionAsync(request, options)); + } + + @Override + public ApiFuture commitAsync( + CommitRequest commitRequest, @Nullable Map options) { + GrpcCallContext context = newCallContext(options, commitRequest.getSession()); + return spannerStub.commitCallable().futureCall(commitRequest, context); } @Override public CommitResponse commit(CommitRequest commitRequest, @Nullable Map options) throws SpannerException { - GrpcCallContext context = newCallContext(options, commitRequest.getSession()); - return get(spannerStub.commitCallable().futureCall(commitRequest, context)); + return get(commitAsync(commitRequest, options)); + } + + @Override + public ApiFuture rollbackAsync(RollbackRequest request, @Nullable Map options) { + GrpcCallContext context = newCallContext(options, request.getSession()); + return spannerStub.rollbackCallable().futureCall(request, context); } @Override public void rollback(RollbackRequest request, @Nullable Map options) throws SpannerException { - GrpcCallContext context = newCallContext(options, request.getSession()); - get(spannerStub.rollbackCallable().futureCall(request, context)); + get(rollbackAsync(request, options)); } @Override diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerInterceptorProvider.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerInterceptorProvider.java index 060f4ca9d1..c262780935 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerInterceptorProvider.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerInterceptorProvider.java @@ -45,6 +45,10 @@ public static SpannerInterceptorProvider createDefault() { return new SpannerInterceptorProvider(defaultInterceptors); } + static SpannerInterceptorProvider create(GrpcInterceptorProvider provider) { + return new SpannerInterceptorProvider(ImmutableList.copyOf(provider.getInterceptors())); + } + public SpannerInterceptorProvider with(ClientInterceptor clientInterceptor) { List interceptors = ImmutableList.builder() @@ -54,6 +58,13 @@ public SpannerInterceptorProvider with(ClientInterceptor clientInterceptor) { return new SpannerInterceptorProvider(interceptors); } + SpannerInterceptorProvider withEncoding(String encoding) { + if (encoding != null) { + return with(new EncodingInterceptor(encoding)); + } + return this; + } + @Override public List getInterceptors() { return clientInterceptors; diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerRpc.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerRpc.java index 31dc209c91..6b42c0a754 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerRpc.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerRpc.java @@ -20,6 +20,7 @@ import com.google.api.core.InternalApi; import com.google.api.gax.longrunning.OperationFuture; import com.google.api.gax.retrying.RetrySettings; +import com.google.api.gax.rpc.ServerStream; import com.google.cloud.ServiceRpc; import com.google.cloud.spanner.SpannerException; import com.google.cloud.spanner.admin.database.v1.stub.DatabaseAdminStub; @@ -58,6 +59,7 @@ import java.util.List; import java.util.Map; import javax.annotation.Nullable; +import org.threeten.bp.Duration; /** * Abstracts remote calls to the Cloud Spanner service. Typically end-consumer code will never use @@ -282,23 +284,40 @@ StreamingCall read( ResultSet executeQuery(ExecuteSqlRequest request, @Nullable Map options); + ApiFuture executeQueryAsync( + ExecuteSqlRequest request, @Nullable Map options); + ResultSet executePartitionedDml(ExecuteSqlRequest request, @Nullable Map options); RetrySettings getPartitionedDmlRetrySettings(); + ServerStream executeStreamingPartitionedDml( + ExecuteSqlRequest request, @Nullable Map options, Duration timeout); + StreamingCall executeQuery( ExecuteSqlRequest request, ResultStreamConsumer consumer, @Nullable Map options); ExecuteBatchDmlResponse executeBatchDml(ExecuteBatchDmlRequest build, Map options); + ApiFuture executeBatchDmlAsync( + ExecuteBatchDmlRequest build, Map options); + Transaction beginTransaction(BeginTransactionRequest request, @Nullable Map options) throws SpannerException; + ApiFuture beginTransactionAsync( + BeginTransactionRequest request, @Nullable Map options); + CommitResponse commit(CommitRequest commitRequest, @Nullable Map options) throws SpannerException; + ApiFuture commitAsync( + CommitRequest commitRequest, @Nullable Map options); + void rollback(RollbackRequest request, @Nullable Map options) throws SpannerException; + ApiFuture rollbackAsync(RollbackRequest request, @Nullable Map options); + PartitionResponse partitionQuery(PartitionQueryRequest request, @Nullable Map options) throws SpannerException; diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/testing/RemoteSpannerHelper.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/testing/RemoteSpannerHelper.java index 2ab5635431..f20354950f 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/testing/RemoteSpannerHelper.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/testing/RemoteSpannerHelper.java @@ -129,7 +129,6 @@ public void cleanUp() { } } logger.log(Level.INFO, "Dropped {0} test database(s)", numDropped); - client.close(); } /** diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/v1/stub/SpannerStubSettings.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/v1/stub/SpannerStubSettings.java index f99372d498..e288143469 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/v1/stub/SpannerStubSettings.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/v1/stub/SpannerStubSettings.java @@ -386,12 +386,17 @@ public static class Builder extends StubSettings.Builder> definitions = ImmutableMap.builder(); definitions.put( - "idempotent", + "retry_policy_1_codes", ImmutableSet.copyOf(Lists.newArrayList(StatusCode.Code.UNAVAILABLE))); - definitions.put("non_idempotent", ImmutableSet.copyOf(Lists.newArrayList())); + definitions.put("no_retry_codes", ImmutableSet.copyOf(Lists.newArrayList())); definitions.put( - "long_running", + "retry_policy_3_codes", ImmutableSet.copyOf(Lists.newArrayList(StatusCode.Code.UNAVAILABLE))); + definitions.put( + "retry_policy_2_codes", + ImmutableSet.copyOf(Lists.newArrayList(StatusCode.Code.UNAVAILABLE))); + definitions.put( + "no_retry_1_codes", ImmutableSet.copyOf(Lists.newArrayList())); RETRYABLE_CODE_DEFINITIONS = definitions.build(); } @@ -410,29 +415,39 @@ public static class Builder extends StubSettings.Builder() { + @Override + public ManagedChannelBuilder apply(ManagedChannelBuilder input) { + input.usePlaintext(); + return input; + } + }) + .setHost("http://" + endpoint) + .setCredentials(NoCredentials.getInstance()) + .setSessionPoolOption(SessionPoolOptions.newBuilder().setFailOnSessionLeak().build()) + .build() + .getService(); + spannerWithEmptySessionPool = + spanner + .getOptions() + .toBuilder() + .setSessionPoolOption( + SessionPoolOptions.newBuilder() + .setFailOnSessionLeak() + .setMinSessions(0) + .setIncStep(1) + .build()) + .build() + .getService(); + } + + @After + public void after() throws Exception { + spanner.close(); + spannerWithEmptySessionPool.close(); + mockSpanner.removeAllExecutionTimes(); + mockSpanner.reset(); + } + + DatabaseClient client() { + return spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); + } + + DatabaseClient clientWithEmptySessionPool() { + return spannerWithEmptySessionPool.getDatabaseClient( + DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); + } +} diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AbstractReadContextTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AbstractReadContextTest.java index bfd739d553..f9cee1c488 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AbstractReadContextTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AbstractReadContextTest.java @@ -20,6 +20,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import com.google.api.gax.core.ExecutorProvider; import com.google.cloud.spanner.spi.v1.SpannerRpc; import com.google.spanner.v1.ExecuteSqlRequest; import com.google.spanner.v1.ExecuteSqlRequest.QueryMode; @@ -80,6 +81,7 @@ public void setup() { .setSession(session) .setRpc(mock(SpannerRpc.class)) .setDefaultQueryOptions(defaultQueryOptions) + .setExecutorProvider(mock(ExecutorProvider.class)) .build(); } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AbstractStructReaderTypesTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AbstractStructReaderTypesTest.java index 04b6183148..bde868fe55 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AbstractStructReaderTypesTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AbstractStructReaderTypesTest.java @@ -19,6 +19,7 @@ import static com.google.cloud.spanner.Type.StructField; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.fail; import static org.junit.runners.Parameterized.Parameter; @@ -33,9 +34,7 @@ import java.util.List; import javax.annotation.Nullable; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.mockito.Mockito; @@ -274,7 +273,6 @@ public static Collection parameters() { @Nullable public List otherAllowedGetters; - @Rule public ExpectedException expectedException = ExpectedException.none(); private TestStructReader reader; @Before @@ -330,7 +328,7 @@ public void getter() throws Exception { } @Test - public void getterForIncorrectType() throws Exception { + public void getterForIncorrectType() { Mockito.when(reader.getType()).thenReturn(Type.struct(StructField.of("F1", type))); int columnIndex = 0; Mockito.when(reader.isNull(columnIndex)).thenReturn(false); @@ -369,18 +367,26 @@ public void getterForIncorrectType() throws Exception { } @Test - public void getterWhenNull() throws Exception { + public void getterWhenNull() { Mockito.when(reader.getType()).thenReturn(Type.struct(StructField.of("F1", type))); Mockito.when(reader.isNull(0)).thenReturn(true); - expectedException.expect(NullPointerException.class); - getterByIndex(0); + try { + getterByIndex(0); + fail("Expected exception"); + } catch (NullPointerException ex) { + assertNotNull(ex.getMessage()); + } } @Test - public void getterByNameWhenNull() throws Exception { + public void getterByNameWhenNull() { Mockito.when(reader.getType()).thenReturn(Type.struct(StructField.of("F1", type))); Mockito.when(reader.isNull(0)).thenReturn(true); - expectedException.expect(NullPointerException.class); - getterByName("F1"); + try { + getterByName("F1"); + fail("Expected exception"); + } catch (NullPointerException ex) { + assertNotNull(ex.getMessage()); + } } } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncResultSetImplStressTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncResultSetImplStressTest.java new file mode 100644 index 0000000000..c3383cadda --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncResultSetImplStressTest.java @@ -0,0 +1,464 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.spanner; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutures; +import com.google.api.core.SettableApiFuture; +import com.google.api.gax.core.ExecutorProvider; +import com.google.cloud.spanner.AsyncResultSet.CallbackResponse; +import com.google.cloud.spanner.AsyncResultSet.CursorState; +import com.google.cloud.spanner.AsyncResultSet.ReadyCallback; +import com.google.cloud.spanner.Type.StructField; +import com.google.common.base.Function; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.Timeout; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class AsyncResultSetImplStressTest { + private static final int TEST_RUNS = 1000; + + /** Timeout is applied to each test case individually. */ + @Rule public Timeout timeout = new Timeout(120, TimeUnit.SECONDS); + + @Parameter(0) + public int resultSetSize; + + @Parameters(name = "rows = {0}") + public static Collection data() { + List params = new ArrayList<>(); + for (int rows : new int[] {0, 1, 5, 10}) { + params.add(new Object[] {rows}); + } + return params; + } + + /** POJO representing a row in the test {@link ResultSet}. */ + private static final class Row { + private final Long id; + private final String name; + + static Row create(StructReader reader) { + return new Row(reader.getLong("ID"), reader.getString("NAME")); + } + + private Row(Long id, String name) { + this.id = id; + this.name = name; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Row)) { + return false; + } + Row other = (Row) o; + return Objects.equals(this.id, other.id) && Objects.equals(this.name, other.name); + } + + @Override + public int hashCode() { + return Objects.hash(this.id, this.name); + } + + @Override + public String toString() { + return String.format("ID: %d, NAME: %s", id, name); + } + } + + private static final class ResultSetWithRandomErrors extends ForwardingResultSet { + private final Random random = new Random(); + private final double errorFraction; + + private ResultSetWithRandomErrors(ResultSet delegate, double errorFraction) { + super(delegate); + this.errorFraction = errorFraction; + } + + @Override + public boolean next() { + if (random.nextDouble() < errorFraction) { + throw SpannerExceptionFactory.newSpannerException( + ErrorCode.INVALID_ARGUMENT, "random error"); + } + return super.next(); + } + } + + /** Creates a simple in-mem {@link ResultSet}. */ + private ResultSet createResultSet() { + List rows = new ArrayList<>(resultSetSize); + for (int i = 0; i < resultSetSize; i++) { + rows.add( + Struct.newBuilder() + .set("ID") + .to(i + 1) + .set("NAME") + .to(String.format("Row %d", (i + 1))) + .build()); + } + return ResultSets.forRows( + Type.struct(StructField.of("ID", Type.int64()), StructField.of("NAME", Type.string())), + rows); + } + + private ResultSet createResultSetWithErrors(double errorFraction) { + return new ResultSetWithRandomErrors(createResultSet(), errorFraction); + } + + /** + * Generates a list of {@link Row} instances that correspond with the rows in {@link + * #createResultSet()}. + */ + private List createExpectedRows() { + List rows = new ArrayList<>(resultSetSize); + for (int i = 0; i < resultSetSize; i++) { + rows.add(new Row(Long.valueOf(i + 1), String.format("Row %d", (i + 1)))); + } + return rows; + } + + /** Creates a single-threaded {@link ExecutorService}. */ + private static ScheduledExecutorService createExecService() { + return createExecService(1); + } + + /** Creates an {@link ExecutorService} using a bounded pool of threadCount threads. */ + private static ScheduledExecutorService createExecService(int threadCount) { + return Executors.newScheduledThreadPool( + threadCount, new ThreadFactoryBuilder().setDaemon(true).build()); + } + + @Test + public void toList() throws Exception { + ExecutorProvider executorProvider = SpannerOptions.createDefaultAsyncExecutorProvider(); + for (int bufferSize = 1; bufferSize < resultSetSize * 2; bufferSize *= 2) { + for (int i = 0; i < TEST_RUNS; i++) { + try (AsyncResultSetImpl impl = + new AsyncResultSetImpl(executorProvider, createResultSet(), bufferSize)) { + ImmutableList list = + impl.toList( + new Function() { + @Override + public Row apply(StructReader input) { + return Row.create(input); + } + }); + assertThat(list).containsExactlyElementsIn(createExpectedRows()); + } + } + } + } + + @Test + public void toListWithErrors() throws Exception { + ExecutorProvider executorProvider = SpannerOptions.createDefaultAsyncExecutorProvider(); + for (int bufferSize = 1; bufferSize < resultSetSize * 2; bufferSize *= 2) { + for (int i = 0; i < TEST_RUNS; i++) { + try (AsyncResultSetImpl impl = + new AsyncResultSetImpl( + executorProvider, createResultSetWithErrors(1.0 / resultSetSize), bufferSize)) { + ImmutableList list = + impl.toList( + new Function() { + @Override + public Row apply(StructReader input) { + return Row.create(input); + } + }); + assertThat(list).containsExactlyElementsIn(createExpectedRows()); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + assertThat(e.getMessage()).contains("random error"); + } + } + } + } + + @Test + public void asyncToList() throws Exception { + ExecutorProvider executorProvider = SpannerOptions.createDefaultAsyncExecutorProvider(); + for (int bufferSize = 1; bufferSize < resultSetSize * 2; bufferSize *= 2) { + List>> futures = new ArrayList<>(TEST_RUNS); + ExecutorService executor = createExecService(32); + for (int i = 0; i < TEST_RUNS; i++) { + try (AsyncResultSet impl = + new AsyncResultSetImpl(executorProvider, createResultSet(), bufferSize)) { + futures.add( + impl.toListAsync( + new Function() { + @Override + public Row apply(StructReader input) { + return Row.create(input); + } + }, + executor)); + } + } + List> lists = ApiFutures.allAsList(futures).get(); + for (ImmutableList list : lists) { + assertThat(list).containsExactlyElementsIn(createExpectedRows()); + } + executor.shutdown(); + } + } + + @Test + public void consume() throws Exception { + ExecutorProvider executorProvider = SpannerOptions.createDefaultAsyncExecutorProvider(); + final Random random = new Random(); + for (Executor executor : + new Executor[] { + MoreExecutors.directExecutor(), createExecService(), createExecService(32) + }) { + for (int bufferSize = 1; bufferSize < resultSetSize * 2; bufferSize *= 2) { + for (int i = 0; i < TEST_RUNS; i++) { + final SettableApiFuture> future = SettableApiFuture.create(); + try (AsyncResultSetImpl impl = + new AsyncResultSetImpl(executorProvider, createResultSet(), bufferSize)) { + final ImmutableList.Builder builder = ImmutableList.builder(); + impl.setCallback( + executor, + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + // Randomly do something with the received data or not. Not calling tryNext() in + // the onDataReady is not 'normal', but users may do it, and the result set + // should be able to handle that. + if (random.nextBoolean()) { + CursorState state; + while ((state = resultSet.tryNext()) == CursorState.OK) { + builder.add(Row.create(resultSet)); + } + if (state == CursorState.DONE) { + future.set(builder.build()); + } + } + return CallbackResponse.CONTINUE; + } + }); + assertThat(future.get()).containsExactlyElementsIn(createExpectedRows()); + } + } + } + } + } + + @Test + public void pauseResume() throws Exception { + ExecutorProvider executorProvider = SpannerOptions.createDefaultAsyncExecutorProvider(); + final Random random = new Random(); + List>> futures = new ArrayList<>(); + for (Executor executor : + new Executor[] { + MoreExecutors.directExecutor(), createExecService(), createExecService(32) + }) { + final List resultSets = + Collections.synchronizedList(new ArrayList()); + for (int bufferSize = 1; bufferSize < resultSetSize * 2; bufferSize *= 2) { + for (int i = 0; i < TEST_RUNS; i++) { + final SettableApiFuture> future = SettableApiFuture.create(); + futures.add(future); + try (AsyncResultSetImpl impl = + new AsyncResultSetImpl(executorProvider, createResultSet(), bufferSize)) { + resultSets.add(impl); + final ImmutableList.Builder builder = ImmutableList.builder(); + impl.setCallback( + executor, + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + CursorState state; + while ((state = resultSet.tryNext()) == CursorState.OK) { + builder.add(Row.create(resultSet)); + // Randomly request the iterator to pause. + if (random.nextBoolean()) { + return CallbackResponse.PAUSE; + } + } + if (state == CursorState.DONE) { + future.set(builder.build()); + } + return CallbackResponse.CONTINUE; + } + }); + } + } + } + final AtomicBoolean finished = new AtomicBoolean(false); + ExecutorService resumeService = createExecService(); + resumeService.execute( + new Runnable() { + @Override + public void run() { + while (!finished.get()) { + // Randomly resume result sets. + resultSets.get(random.nextInt(resultSets.size())).resume(); + } + } + }); + List> lists = ApiFutures.allAsList(futures).get(); + for (ImmutableList list : lists) { + assertThat(list).containsExactlyElementsIn(createExpectedRows()); + } + if (executor instanceof ExecutorService) { + ((ExecutorService) executor).shutdown(); + } + finished.set(true); + resumeService.shutdown(); + } + } + + @Test + public void cancel() throws Exception { + ExecutorProvider executorProvider = SpannerOptions.createDefaultAsyncExecutorProvider(); + final Random random = new Random(); + for (Executor executor : + new Executor[] { + MoreExecutors.directExecutor(), createExecService(), createExecService(32) + }) { + List>> futures = new ArrayList<>(); + final List resultSets = + Collections.synchronizedList(new ArrayList()); + final Set cancelledIndexes = new HashSet<>(); + for (int bufferSize = 1; bufferSize < resultSetSize * 2; bufferSize *= 2) { + for (int i = 0; i < TEST_RUNS; i++) { + final SettableApiFuture> future = SettableApiFuture.create(); + futures.add(future); + try (AsyncResultSetImpl impl = + new AsyncResultSetImpl(executorProvider, createResultSet(), bufferSize)) { + resultSets.add(impl); + final ImmutableList.Builder builder = ImmutableList.builder(); + impl.setCallback( + executor, + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + try { + CursorState state; + while ((state = resultSet.tryNext()) == CursorState.OK) { + builder.add(Row.create(resultSet)); + // Randomly request the iterator to pause. + if (random.nextBoolean()) { + return CallbackResponse.PAUSE; + } + } + if (state == CursorState.DONE) { + future.set(builder.build()); + } + return CallbackResponse.CONTINUE; + } catch (SpannerException e) { + future.setException(e); + throw e; + } + } + }); + } + } + } + final AtomicBoolean finished = new AtomicBoolean(false); + // Both resume and cancel resultsets randomly. + ExecutorService resumeService = createExecService(); + resumeService.execute( + new Runnable() { + @Override + public void run() { + while (!finished.get()) { + // Randomly resume result sets. + resultSets.get(random.nextInt(resultSets.size())).resume(); + } + // Make sure all result sets finish. + for (AsyncResultSet rs : resultSets) { + rs.resume(); + } + } + }); + ExecutorService cancelService = createExecService(); + cancelService.execute( + new Runnable() { + @Override + public void run() { + while (!finished.get()) { + // Randomly cancel result sets. + int index = random.nextInt(resultSets.size()); + resultSets.get(index).cancel(); + cancelledIndexes.add(index); + } + } + }); + + // First wait until all result sets have finished. + for (ApiFuture> future : futures) { + try { + future.get(); + } catch (Throwable e) { + // ignore for now. + } + } + finished.set(true); + cancelService.shutdown(); + cancelService.awaitTermination(10L, TimeUnit.SECONDS); + + int index = 0; + for (ApiFuture> future : futures) { + try { + ImmutableList list = future.get(30L, TimeUnit.SECONDS); + // Note that the fact that the call succeeded for for this result set, does not + // necessarily mean that the result set was not cancelled. Cancelling a result set is a + // best-effort operation, and the entire result set may still be produced and returned to + // the user. + assertThat(list).containsExactlyElementsIn(createExpectedRows()); + } catch (ExecutionException e) { + assertThat(e.getCause()).isInstanceOf(SpannerException.class); + SpannerException se = (SpannerException) e.getCause(); + assertThat(se.getErrorCode()).isEqualTo(ErrorCode.CANCELLED); + assertThat(cancelledIndexes).contains(index); + } + index++; + } + if (executor instanceof ExecutorService) { + ((ExecutorService) executor).shutdown(); + } + } + } +} diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncResultSetImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncResultSetImplTest.java new file mode 100644 index 0000000000..9359dc6694 --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncResultSetImplTest.java @@ -0,0 +1,443 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.spanner; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.google.api.core.ApiFuture; +import com.google.api.gax.core.ExecutorProvider; +import com.google.cloud.spanner.AsyncResultSet.CallbackResponse; +import com.google.cloud.spanner.AsyncResultSet.CursorState; +import com.google.cloud.spanner.AsyncResultSet.ReadyCallback; +import com.google.common.base.Function; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Range; +import java.util.concurrent.BlockingDeque; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class AsyncResultSetImplTest { + private ExecutorProvider mockedProvider; + private ExecutorProvider simpleProvider; + + @Before + public void setup() { + mockedProvider = mock(ExecutorProvider.class); + when(mockedProvider.getExecutor()).thenReturn(mock(ScheduledExecutorService.class)); + simpleProvider = SpannerOptions.createAsyncExecutorProvider(1, 1L, TimeUnit.SECONDS); + } + + @SuppressWarnings("unchecked") + @Test + public void close() { + AsyncResultSetImpl rs = + new AsyncResultSetImpl( + mockedProvider, mock(ResultSet.class), AsyncResultSetImpl.DEFAULT_BUFFER_SIZE); + rs.close(); + // Closing a second time should be a no-op. + rs.close(); + + // The following methods are not allowed to call after closing the result set. + try { + rs.setCallback(mock(Executor.class), mock(ReadyCallback.class)); + fail("missing expected exception"); + } catch (IllegalStateException e) { + } + try { + rs.toList(mock(Function.class)); + fail("missing expected exception"); + } catch (IllegalStateException e) { + } + try { + rs.toListAsync(mock(Function.class), mock(Executor.class)); + fail("missing expected exception"); + } catch (IllegalStateException e) { + } + + // The following methods are allowed on a closed result set. + AsyncResultSetImpl rs2 = + new AsyncResultSetImpl( + mockedProvider, mock(ResultSet.class), AsyncResultSetImpl.DEFAULT_BUFFER_SIZE); + rs2.setCallback(mock(Executor.class), mock(ReadyCallback.class)); + rs2.close(); + rs2.cancel(); + rs2.resume(); + } + + @Test + public void tryNextNotAllowed() { + try (AsyncResultSetImpl rs = + new AsyncResultSetImpl( + mockedProvider, mock(ResultSet.class), AsyncResultSetImpl.DEFAULT_BUFFER_SIZE)) { + rs.setCallback(mock(Executor.class), mock(ReadyCallback.class)); + try { + rs.tryNext(); + fail("missing expected exception"); + } catch (IllegalStateException e) { + assertThat(e.getMessage()) + .contains("tryNext may only be called from a DataReady callback."); + } + } + } + + @Test + public void toList() { + ResultSet delegate = mock(ResultSet.class); + when(delegate.next()).thenReturn(true, true, true, false); + when(delegate.getCurrentRowAsStruct()).thenReturn(mock(Struct.class)); + try (AsyncResultSetImpl rs = + new AsyncResultSetImpl(simpleProvider, delegate, AsyncResultSetImpl.DEFAULT_BUFFER_SIZE)) { + ImmutableList list = + rs.toList( + new Function() { + @Override + public Object apply(StructReader input) { + return new Object(); + } + }); + assertThat(list).hasSize(3); + } + } + + @Test + public void toListPropagatesError() { + ResultSet delegate = mock(ResultSet.class); + when(delegate.next()) + .thenThrow( + SpannerExceptionFactory.newSpannerException( + ErrorCode.INVALID_ARGUMENT, "invalid query")); + try (AsyncResultSetImpl rs = + new AsyncResultSetImpl(simpleProvider, delegate, AsyncResultSetImpl.DEFAULT_BUFFER_SIZE)) { + rs.toList( + new Function() { + @Override + public Object apply(StructReader input) { + return new Object(); + } + }); + fail("missing expected exception"); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + assertThat(e.getMessage()).contains("invalid query"); + } + } + + @Test + public void toListAsync() throws InterruptedException, ExecutionException { + ExecutorService executor = Executors.newFixedThreadPool(1); + ResultSet delegate = mock(ResultSet.class); + when(delegate.next()).thenReturn(true, true, true, false); + when(delegate.getCurrentRowAsStruct()).thenReturn(mock(Struct.class)); + try (AsyncResultSetImpl rs = + new AsyncResultSetImpl(simpleProvider, delegate, AsyncResultSetImpl.DEFAULT_BUFFER_SIZE)) { + ApiFuture> future = + rs.toListAsync( + new Function() { + @Override + public Object apply(StructReader input) { + return new Object(); + } + }, + executor); + assertThat(future.get()).hasSize(3); + } + executor.shutdown(); + } + + @Test + public void toListAsyncPropagatesError() throws InterruptedException { + ExecutorService executor = Executors.newFixedThreadPool(1); + ResultSet delegate = mock(ResultSet.class); + when(delegate.next()) + .thenThrow( + SpannerExceptionFactory.newSpannerException( + ErrorCode.INVALID_ARGUMENT, "invalid query")); + try (AsyncResultSetImpl rs = + new AsyncResultSetImpl(simpleProvider, delegate, AsyncResultSetImpl.DEFAULT_BUFFER_SIZE)) { + rs.toListAsync( + new Function() { + @Override + public Object apply(StructReader input) { + return new Object(); + } + }, + executor) + .get(); + fail("missing expected exception"); + } catch (ExecutionException e) { + assertThat(e.getCause()).isInstanceOf(SpannerException.class); + SpannerException se = (SpannerException) e.getCause(); + assertThat(se.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + assertThat(se.getMessage()).contains("invalid query"); + } + executor.shutdown(); + } + + @Test + public void withCallback() throws InterruptedException { + Executor executor = Executors.newSingleThreadExecutor(); + ResultSet delegate = mock(ResultSet.class); + when(delegate.next()).thenReturn(true, true, true, false); + when(delegate.getCurrentRowAsStruct()).thenReturn(mock(Struct.class)); + final AtomicInteger callbackCounter = new AtomicInteger(); + final AtomicInteger rowCounter = new AtomicInteger(); + final CountDownLatch finishedLatch = new CountDownLatch(1); + try (AsyncResultSetImpl rs = + new AsyncResultSetImpl(simpleProvider, delegate, AsyncResultSetImpl.DEFAULT_BUFFER_SIZE)) { + rs.setCallback( + executor, + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + callbackCounter.incrementAndGet(); + CursorState state; + while ((state = resultSet.tryNext()) == CursorState.OK) { + rowCounter.incrementAndGet(); + } + if (state == CursorState.DONE) { + finishedLatch.countDown(); + } + return CallbackResponse.CONTINUE; + } + }); + } + finishedLatch.await(); + // There should be between 1 and 4 callbacks, depending on the timing of the threads. + // Normally, there should be just 1 callback. + assertThat(callbackCounter.get()).isIn(Range.closed(1, 4)); + assertThat(rowCounter.get()).isEqualTo(3); + } + + @Test + public void callbackReceivesError() throws InterruptedException { + Executor executor = Executors.newSingleThreadExecutor(); + ResultSet delegate = mock(ResultSet.class); + when(delegate.next()) + .thenThrow( + SpannerExceptionFactory.newSpannerException( + ErrorCode.INVALID_ARGUMENT, "invalid query")); + final BlockingDeque receivedErr = new LinkedBlockingDeque<>(1); + try (AsyncResultSetImpl rs = + new AsyncResultSetImpl(simpleProvider, delegate, AsyncResultSetImpl.DEFAULT_BUFFER_SIZE)) { + rs.setCallback( + executor, + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + try { + resultSet.tryNext(); + receivedErr.push(new Exception("missing expected exception")); + } catch (SpannerException e) { + receivedErr.push(e); + } + return CallbackResponse.DONE; + } + }); + } + Exception e = receivedErr.take(); + assertThat(e).isInstanceOf(SpannerException.class); + SpannerException se = (SpannerException) e; + assertThat(se.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + assertThat(se.getMessage()).contains("invalid query"); + } + + @Test + public void callbackReceivesErrorHalfwayThrough() throws InterruptedException { + Executor executor = Executors.newSingleThreadExecutor(); + ResultSet delegate = mock(ResultSet.class); + when(delegate.next()) + .thenReturn(true) + .thenThrow( + SpannerExceptionFactory.newSpannerException( + ErrorCode.INVALID_ARGUMENT, "invalid query")); + when(delegate.getCurrentRowAsStruct()).thenReturn(mock(Struct.class)); + final AtomicInteger rowCount = new AtomicInteger(); + final BlockingDeque receivedErr = new LinkedBlockingDeque<>(1); + try (AsyncResultSetImpl rs = + new AsyncResultSetImpl(simpleProvider, delegate, AsyncResultSetImpl.DEFAULT_BUFFER_SIZE)) { + rs.setCallback( + executor, + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + try { + if (resultSet.tryNext() != CursorState.DONE) { + rowCount.incrementAndGet(); + return CallbackResponse.CONTINUE; + } + } catch (SpannerException e) { + receivedErr.push(e); + } + return CallbackResponse.DONE; + } + }); + } + Exception e = receivedErr.take(); + assertThat(e).isInstanceOf(SpannerException.class); + SpannerException se = (SpannerException) e; + assertThat(se.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + assertThat(se.getMessage()).contains("invalid query"); + assertThat(rowCount.get()).isEqualTo(1); + } + + @Test + public void pauseResume() throws InterruptedException { + Executor executor = Executors.newSingleThreadExecutor(); + ResultSet delegate = mock(ResultSet.class); + when(delegate.next()).thenReturn(true, true, true, false); + when(delegate.getCurrentRowAsStruct()).thenReturn(mock(Struct.class)); + final AtomicInteger callbackCounter = new AtomicInteger(); + final BlockingDeque queue = new LinkedBlockingDeque<>(1); + final AtomicBoolean finished = new AtomicBoolean(false); + try (AsyncResultSetImpl rs = + new AsyncResultSetImpl(simpleProvider, delegate, AsyncResultSetImpl.DEFAULT_BUFFER_SIZE)) { + rs.setCallback( + executor, + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + callbackCounter.incrementAndGet(); + CursorState state = resultSet.tryNext(); + if (state == CursorState.OK) { + try { + queue.put(new Object()); + } catch (InterruptedException e) { + // Finish early if an error occurs. + return CallbackResponse.DONE; + } + return CallbackResponse.PAUSE; + } + finished.set(true); + return CallbackResponse.DONE; + } + }); + int rowCounter = 0; + while (!finished.get()) { + Object o = queue.poll(1L, TimeUnit.MILLISECONDS); + if (o != null) { + rowCounter++; + } + rs.resume(); + } + // There should be exactly 4 callbacks as we only consume one row per callback. + assertThat(callbackCounter.get()).isEqualTo(4); + assertThat(rowCounter).isEqualTo(3); + } + } + + @Test + public void cancel() throws InterruptedException { + Executor executor = Executors.newSingleThreadExecutor(); + ResultSet delegate = mock(ResultSet.class); + when(delegate.next()).thenReturn(true, true, true, false); + when(delegate.getCurrentRowAsStruct()).thenReturn(mock(Struct.class)); + final AtomicInteger callbackCounter = new AtomicInteger(); + final BlockingDeque queue = new LinkedBlockingDeque<>(1); + final AtomicBoolean finished = new AtomicBoolean(false); + try (AsyncResultSetImpl rs = + new AsyncResultSetImpl(simpleProvider, delegate, AsyncResultSetImpl.DEFAULT_BUFFER_SIZE)) { + rs.setCallback( + executor, + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + callbackCounter.incrementAndGet(); + try { + CursorState state = resultSet.tryNext(); + if (state == CursorState.OK) { + try { + queue.put(new Object()); + } catch (InterruptedException e) { + // Finish early if an error occurs. + return CallbackResponse.DONE; + } + } + // Pause after 2 rows to make sure that no more data is consumed until the cancel + // call has been received. + return callbackCounter.get() == 2 + ? CallbackResponse.PAUSE + : CallbackResponse.CONTINUE; + } catch (SpannerException e) { + if (e.getErrorCode() == ErrorCode.CANCELLED) { + finished.set(true); + } + } + return CallbackResponse.DONE; + } + }); + int rowCounter = 0; + while (!finished.get()) { + Object o = queue.poll(1L, TimeUnit.MILLISECONDS); + if (o != null) { + rowCounter++; + } + if (rowCounter == 2) { + // Cancel the result set and then resume it to get the cancelled error. + rs.cancel(); + rs.resume(); + } + } + assertThat(callbackCounter.get()).isIn(Range.closed(2, 4)); + assertThat(rowCounter).isIn(Range.closed(2, 3)); + } + } + + @Test + public void callbackReturnsError() throws InterruptedException { + Executor executor = Executors.newSingleThreadExecutor(); + ResultSet delegate = mock(ResultSet.class); + when(delegate.next()).thenReturn(true, true, true, false); + when(delegate.getCurrentRowAsStruct()).thenReturn(mock(Struct.class)); + final AtomicInteger callbackCounter = new AtomicInteger(); + try (AsyncResultSetImpl rs = + new AsyncResultSetImpl(simpleProvider, delegate, AsyncResultSetImpl.DEFAULT_BUFFER_SIZE)) { + rs.setCallback( + executor, + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + callbackCounter.incrementAndGet(); + throw new RuntimeException("async test"); + } + }); + rs.getResult().get(); + fail("missing expected exception"); + } catch (ExecutionException e) { + assertThat(e.getCause()).isInstanceOf(SpannerException.class); + SpannerException se = (SpannerException) e.getCause(); + assertThat(se.getErrorCode()).isEqualTo(ErrorCode.UNKNOWN); + assertThat(se.getMessage()).contains("async test"); + assertThat(callbackCounter.get()).isEqualTo(1); + } + } +} diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncRunnerTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncRunnerTest.java new file mode 100644 index 0000000000..eb00047ca4 --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncRunnerTest.java @@ -0,0 +1,618 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.spanner; + +import static com.google.cloud.spanner.MockSpannerTestUtil.*; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import com.google.api.core.ApiFunction; +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutures; +import com.google.api.core.SettableApiFuture; +import com.google.cloud.Timestamp; +import com.google.cloud.spanner.AsyncResultSet.CallbackResponse; +import com.google.cloud.spanner.AsyncResultSet.ReadyCallback; +import com.google.cloud.spanner.AsyncRunner.AsyncWork; +import com.google.cloud.spanner.MockSpannerServiceImpl.SimulatedExecutionTime; +import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult; +import com.google.common.base.Function; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.spanner.v1.BatchCreateSessionsRequest; +import com.google.spanner.v1.BeginTransactionRequest; +import com.google.spanner.v1.CommitRequest; +import com.google.spanner.v1.ExecuteBatchDmlRequest; +import com.google.spanner.v1.ExecuteSqlRequest; +import io.grpc.Status; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class AsyncRunnerTest extends AbstractAsyncTransactionTest { + @Test + public void asyncRunnerUpdate() throws Exception { + AsyncRunner runner = client().runAsync(); + ApiFuture updateCount = + runner.runAsync( + new AsyncWork() { + @Override + public ApiFuture doWorkAsync(TransactionContext txn) { + return txn.executeUpdateAsync(UPDATE_STATEMENT); + } + }, + executor); + assertThat(updateCount.get()).isEqualTo(UPDATE_COUNT); + } + + @Test + public void asyncRunnerIsNonBlocking() throws Exception { + mockSpanner.freeze(); + AsyncRunner runner = clientWithEmptySessionPool().runAsync(); + ApiFuture res = + runner.runAsync( + new AsyncWork() { + @Override + public ApiFuture doWorkAsync(TransactionContext txn) { + txn.executeUpdateAsync(UPDATE_STATEMENT); + return ApiFutures.immediateFuture(null); + } + }, + executor); + ApiFuture ts = runner.getCommitTimestamp(); + mockSpanner.unfreeze(); + assertThat(res.get()).isNull(); + assertThat(ts.get()).isNotNull(); + } + + @Test + public void asyncRunnerInvalidUpdate() throws Exception { + AsyncRunner runner = client().runAsync(); + ApiFuture updateCount = + runner.runAsync( + new AsyncWork() { + @Override + public ApiFuture doWorkAsync(TransactionContext txn) { + return txn.executeUpdateAsync(INVALID_UPDATE_STATEMENT); + } + }, + executor); + try { + updateCount.get(); + fail("missing expected exception"); + } catch (ExecutionException e) { + assertThat(e.getCause()).isInstanceOf(SpannerException.class); + SpannerException se = (SpannerException) e.getCause(); + assertThat(se.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + assertThat(se.getMessage()).contains("invalid statement"); + } + } + + @Test + public void asyncRunnerFireAndForgetInvalidUpdate() throws Exception { + AsyncRunner runner = client().runAsync(); + ApiFuture res = + runner.runAsync( + new AsyncWork() { + @Override + public ApiFuture doWorkAsync(TransactionContext txn) { + txn.executeUpdateAsync(INVALID_UPDATE_STATEMENT); + return txn.executeUpdateAsync(UPDATE_STATEMENT); + } + }, + executor); + assertThat(res.get()).isEqualTo(UPDATE_COUNT); + } + + @Test + public void asyncRunnerUpdateAborted() throws Exception { + try { + // Temporarily set the result of the update to 2 rows. + mockSpanner.putStatementResult(StatementResult.update(UPDATE_STATEMENT, UPDATE_COUNT + 1L)); + final AtomicInteger attempt = new AtomicInteger(); + AsyncRunner runner = client().runAsync(); + ApiFuture updateCount = + runner.runAsync( + new AsyncWork() { + @Override + public ApiFuture doWorkAsync(TransactionContext txn) { + if (attempt.incrementAndGet() == 1) { + mockSpanner.abortTransaction(txn); + } else { + // Set the result of the update statement back to 1 row. + mockSpanner.putStatementResult( + StatementResult.update(UPDATE_STATEMENT, UPDATE_COUNT)); + } + return txn.executeUpdateAsync(UPDATE_STATEMENT); + } + }, + executor); + assertThat(updateCount.get()).isEqualTo(UPDATE_COUNT); + assertThat(attempt.get()).isEqualTo(2); + } finally { + mockSpanner.putStatementResult(StatementResult.update(UPDATE_STATEMENT, UPDATE_COUNT)); + } + } + + @Test + public void asyncRunnerCommitAborted() throws Exception { + try { + // Temporarily set the result of the update to 2 rows. + mockSpanner.putStatementResult(StatementResult.update(UPDATE_STATEMENT, UPDATE_COUNT + 1L)); + final AtomicInteger attempt = new AtomicInteger(); + AsyncRunner runner = client().runAsync(); + ApiFuture updateCount = + runner.runAsync( + new AsyncWork() { + @Override + public ApiFuture doWorkAsync(final TransactionContext txn) { + if (attempt.get() > 0) { + // Set the result of the update statement back to 1 row. + mockSpanner.putStatementResult( + StatementResult.update(UPDATE_STATEMENT, UPDATE_COUNT)); + } + ApiFuture updateCount = txn.executeUpdateAsync(UPDATE_STATEMENT); + if (attempt.incrementAndGet() == 1) { + mockSpanner.abortTransaction(txn); + } + return updateCount; + } + }, + executor); + assertThat(updateCount.get()).isEqualTo(UPDATE_COUNT); + assertThat(attempt.get()).isEqualTo(2); + } finally { + mockSpanner.putStatementResult(StatementResult.update(UPDATE_STATEMENT, UPDATE_COUNT)); + } + } + + @Test + public void asyncRunnerUpdateAbortedWithoutGettingResult() throws Exception { + final AtomicInteger attempt = new AtomicInteger(); + AsyncRunner runner = clientWithEmptySessionPool().runAsync(); + ApiFuture result = + runner.runAsync( + new AsyncWork() { + @Override + public ApiFuture doWorkAsync(TransactionContext txn) { + if (attempt.incrementAndGet() == 1) { + mockSpanner.abortTransaction(txn); + } + // This update statement will be aborted, but the error will not propagated to the + // transaction runner and cause the transaction to retry. Instead, the commit call + // will do that. + txn.executeUpdateAsync(UPDATE_STATEMENT); + // Resolving this future will not resolve the result of the entire transaction. The + // transaction result will be resolved when the commit has actually finished + // successfully. + return ApiFutures.immediateFuture(null); + } + }, + executor); + assertThat(result.get()).isNull(); + assertThat(attempt.get()).isEqualTo(2); + assertThat(mockSpanner.getRequestTypes()) + .containsExactly( + BatchCreateSessionsRequest.class, + BeginTransactionRequest.class, + ExecuteSqlRequest.class, + CommitRequest.class, + BeginTransactionRequest.class, + ExecuteSqlRequest.class, + CommitRequest.class); + } + + @Test + public void asyncRunnerCommitFails() throws Exception { + mockSpanner.setCommitExecutionTime( + SimulatedExecutionTime.ofException( + Status.RESOURCE_EXHAUSTED + .withDescription("mutation limit exceeded") + .asRuntimeException())); + AsyncRunner runner = client().runAsync(); + ApiFuture updateCount = + runner.runAsync( + new AsyncWork() { + @Override + public ApiFuture doWorkAsync(TransactionContext txn) { + // This statement will succeed, but the commit will fail. The error from the commit + // will bubble up to the future that is returned by the transaction, and the update + // count returned here will never reach the user application. + return txn.executeUpdateAsync(UPDATE_STATEMENT); + } + }, + executor); + try { + updateCount.get(); + fail("missing expected exception"); + } catch (ExecutionException e) { + assertThat(e.getCause()).isInstanceOf(SpannerException.class); + SpannerException se = (SpannerException) e.getCause(); + assertThat(se.getErrorCode()).isEqualTo(ErrorCode.RESOURCE_EXHAUSTED); + assertThat(se.getMessage()).contains("mutation limit exceeded"); + } + } + + @Test + public void asyncRunnerWaitsUntilAsyncUpdateHasFinished() throws Exception { + AsyncRunner runner = clientWithEmptySessionPool().runAsync(); + ApiFuture res = + runner.runAsync( + new AsyncWork() { + @Override + public ApiFuture doWorkAsync(TransactionContext txn) { + txn.executeUpdateAsync(UPDATE_STATEMENT); + return ApiFutures.immediateFuture(null); + } + }, + executor); + res.get(); + assertThat(mockSpanner.getRequestTypes()) + .containsExactly( + BatchCreateSessionsRequest.class, + BeginTransactionRequest.class, + ExecuteSqlRequest.class, + CommitRequest.class); + } + + @Test + public void asyncRunnerBatchUpdate() throws Exception { + AsyncRunner runner = client().runAsync(); + ApiFuture updateCount = + runner.runAsync( + new AsyncWork() { + @Override + public ApiFuture doWorkAsync(TransactionContext txn) { + return txn.batchUpdateAsync(ImmutableList.of(UPDATE_STATEMENT, UPDATE_STATEMENT)); + } + }, + executor); + assertThat(updateCount.get()).asList().containsExactly(UPDATE_COUNT, UPDATE_COUNT); + } + + @Test + public void asyncRunnerIsNonBlockingWithBatchUpdate() throws Exception { + mockSpanner.freeze(); + AsyncRunner runner = clientWithEmptySessionPool().runAsync(); + ApiFuture res = + runner.runAsync( + new AsyncWork() { + @Override + public ApiFuture doWorkAsync(TransactionContext txn) { + txn.batchUpdateAsync(ImmutableList.of(UPDATE_STATEMENT)); + return ApiFutures.immediateFuture(null); + } + }, + executor); + ApiFuture ts = runner.getCommitTimestamp(); + mockSpanner.unfreeze(); + assertThat(res.get()).isNull(); + assertThat(ts.get()).isNotNull(); + } + + @Test + public void asyncRunnerInvalidBatchUpdate() throws Exception { + AsyncRunner runner = client().runAsync(); + ApiFuture updateCount = + runner.runAsync( + new AsyncWork() { + @Override + public ApiFuture doWorkAsync(TransactionContext txn) { + return txn.batchUpdateAsync( + ImmutableList.of(UPDATE_STATEMENT, INVALID_UPDATE_STATEMENT)); + } + }, + executor); + try { + updateCount.get(); + fail("missing expected exception"); + } catch (ExecutionException e) { + assertThat(e.getCause()).isInstanceOf(SpannerException.class); + SpannerException se = (SpannerException) e.getCause(); + assertThat(se.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + assertThat(se.getMessage()).contains("invalid statement"); + } + } + + @Test + public void asyncRunnerFireAndForgetInvalidBatchUpdate() throws Exception { + AsyncRunner runner = client().runAsync(); + ApiFuture res = + runner.runAsync( + new AsyncWork() { + @Override + public ApiFuture doWorkAsync(TransactionContext txn) { + txn.batchUpdateAsync(ImmutableList.of(UPDATE_STATEMENT, INVALID_UPDATE_STATEMENT)); + return txn.batchUpdateAsync(ImmutableList.of(UPDATE_STATEMENT, UPDATE_STATEMENT)); + } + }, + executor); + assertThat(res.get()).asList().containsExactly(UPDATE_COUNT, UPDATE_COUNT); + } + + @Test + public void asyncRunnerBatchUpdateAborted() throws Exception { + final AtomicInteger attempt = new AtomicInteger(); + AsyncRunner runner = client().runAsync(); + ApiFuture updateCount = + runner.runAsync( + new AsyncWork() { + @Override + public ApiFuture doWorkAsync(TransactionContext txn) { + if (attempt.incrementAndGet() == 1) { + return txn.batchUpdateAsync( + ImmutableList.of(UPDATE_STATEMENT, UPDATE_ABORTED_STATEMENT)); + } else { + return txn.batchUpdateAsync(ImmutableList.of(UPDATE_STATEMENT, UPDATE_STATEMENT)); + } + } + }, + executor); + assertThat(updateCount.get()).asList().containsExactly(UPDATE_COUNT, UPDATE_COUNT); + assertThat(attempt.get()).isEqualTo(2); + } + + @Test + public void asyncRunnerWithBatchUpdateCommitAborted() throws Exception { + try { + // Temporarily set the result of the update to 2 rows. + mockSpanner.putStatementResult(StatementResult.update(UPDATE_STATEMENT, UPDATE_COUNT + 1L)); + final AtomicInteger attempt = new AtomicInteger(); + AsyncRunner runner = client().runAsync(); + ApiFuture updateCount = + runner.runAsync( + new AsyncWork() { + @Override + public ApiFuture doWorkAsync(final TransactionContext txn) { + if (attempt.get() > 0) { + // Set the result of the update statement back to 1 row. + mockSpanner.putStatementResult( + StatementResult.update(UPDATE_STATEMENT, UPDATE_COUNT)); + } + ApiFuture updateCount = + txn.batchUpdateAsync(ImmutableList.of(UPDATE_STATEMENT, UPDATE_STATEMENT)); + if (attempt.incrementAndGet() == 1) { + mockSpanner.abortTransaction(txn); + } + return updateCount; + } + }, + executor); + assertThat(updateCount.get()).asList().containsExactly(UPDATE_COUNT, UPDATE_COUNT); + assertThat(attempt.get()).isEqualTo(2); + } finally { + mockSpanner.putStatementResult(StatementResult.update(UPDATE_STATEMENT, UPDATE_COUNT)); + } + } + + @Test + public void asyncRunnerBatchUpdateAbortedWithoutGettingResult() throws Exception { + final AtomicInteger attempt = new AtomicInteger(); + AsyncRunner runner = clientWithEmptySessionPool().runAsync(); + ApiFuture result = + runner.runAsync( + new AsyncWork() { + @Override + public ApiFuture doWorkAsync(TransactionContext txn) { + if (attempt.incrementAndGet() == 1) { + mockSpanner.abortTransaction(txn); + } + // This update statement will be aborted, but the error will not propagated to the + // transaction runner and cause the transaction to retry. Instead, the commit call + // will do that. + txn.batchUpdateAsync(ImmutableList.of(UPDATE_STATEMENT, UPDATE_STATEMENT)); + // Resolving this future will not resolve the result of the entire transaction. The + // transaction result will be resolved when the commit has actually finished + // successfully. + return ApiFutures.immediateFuture(null); + } + }, + executor); + assertThat(result.get()).isNull(); + assertThat(attempt.get()).isEqualTo(2); + assertThat(mockSpanner.getRequestTypes()) + .containsExactly( + BatchCreateSessionsRequest.class, + BeginTransactionRequest.class, + ExecuteBatchDmlRequest.class, + CommitRequest.class, + BeginTransactionRequest.class, + ExecuteBatchDmlRequest.class, + CommitRequest.class); + } + + @Test + public void asyncRunnerWithBatchUpdateCommitFails() throws Exception { + mockSpanner.setCommitExecutionTime( + SimulatedExecutionTime.ofException( + Status.RESOURCE_EXHAUSTED + .withDescription("mutation limit exceeded") + .asRuntimeException())); + AsyncRunner runner = client().runAsync(); + ApiFuture updateCount = + runner.runAsync( + new AsyncWork() { + @Override + public ApiFuture doWorkAsync(TransactionContext txn) { + // This statement will succeed, but the commit will fail. The error from the commit + // will bubble up to the future that is returned by the transaction, and the update + // count returned here will never reach the user application. + return txn.batchUpdateAsync(ImmutableList.of(UPDATE_STATEMENT, UPDATE_STATEMENT)); + } + }, + executor); + try { + updateCount.get(); + fail("missing expected exception"); + } catch (ExecutionException e) { + assertThat(e.getCause()).isInstanceOf(SpannerException.class); + SpannerException se = (SpannerException) e.getCause(); + assertThat(se.getErrorCode()).isEqualTo(ErrorCode.RESOURCE_EXHAUSTED); + assertThat(se.getMessage()).contains("mutation limit exceeded"); + } + } + + @Test + public void asyncRunnerWaitsUntilAsyncBatchUpdateHasFinished() throws Exception { + AsyncRunner runner = clientWithEmptySessionPool().runAsync(); + ApiFuture res = + runner.runAsync( + new AsyncWork() { + @Override + public ApiFuture doWorkAsync(TransactionContext txn) { + txn.batchUpdateAsync(ImmutableList.of(UPDATE_STATEMENT)); + return ApiFutures.immediateFuture(null); + } + }, + executor); + res.get(); + assertThat(mockSpanner.getRequestTypes()) + .containsExactly( + BatchCreateSessionsRequest.class, + BeginTransactionRequest.class, + ExecuteBatchDmlRequest.class, + CommitRequest.class); + } + + @Test + public void closeTransactionBeforeEndOfAsyncQuery() throws Exception { + final BlockingQueue results = new SynchronousQueue<>(); + final SettableApiFuture finished = SettableApiFuture.create(); + DatabaseClientImpl clientImpl = (DatabaseClientImpl) client(); + + // There should currently not be any sessions checked out of the pool. + assertThat(clientImpl.pool.getNumberOfSessionsInUse()).isEqualTo(0); + + AsyncRunner runner = clientImpl.runAsync(); + final CountDownLatch dataReceived = new CountDownLatch(1); + final CountDownLatch dataChecked = new CountDownLatch(1); + ApiFuture res = + runner.runAsync( + new AsyncWork() { + @Override + public ApiFuture doWorkAsync(TransactionContext txn) { + try (AsyncResultSet rs = + txn.readAsync( + READ_TABLE_NAME, KeySet.all(), READ_COLUMN_NAMES, Options.bufferRows(1))) { + rs.setCallback( + Executors.newSingleThreadExecutor(), + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + dataReceived.countDown(); + try { + while (true) { + switch (resultSet.tryNext()) { + case DONE: + finished.set(true); + return CallbackResponse.DONE; + case NOT_READY: + return CallbackResponse.CONTINUE; + case OK: + dataChecked.await(); + results.put(resultSet.getString(0)); + } + } + } catch (Throwable t) { + finished.setException(t); + return CallbackResponse.DONE; + } + } + }); + } + try { + dataReceived.await(); + return ApiFutures.immediateFuture(null); + } catch (InterruptedException e) { + return ApiFutures.immediateFailedFuture( + SpannerExceptionFactory.propagateInterrupt(e)); + } + } + }, + executor); + // Wait until at least one row has been fetched. At that moment there should be one session + // checked out. + dataReceived.await(); + assertThat(clientImpl.pool.getNumberOfSessionsInUse()).isEqualTo(1); + assertThat(res.isDone()).isFalse(); + dataChecked.countDown(); + // Get the data from the transaction. + List resultList = new ArrayList<>(); + do { + results.drainTo(resultList); + } while (!finished.isDone() || results.size() > 0); + assertThat(finished.get()).isTrue(); + assertThat(resultList).containsExactly("k1", "k2", "k3"); + assertThat(res.get()).isNull(); + assertThat(clientImpl.pool.getNumberOfSessionsInUse()).isEqualTo(0); + } + + @Test + public void asyncRunnerReadRow() throws Exception { + AsyncRunner runner = client().runAsync(); + ApiFuture val = + runner.runAsync( + new AsyncWork() { + @Override + public ApiFuture doWorkAsync(TransactionContext txn) { + return ApiFutures.transform( + txn.readRowAsync(READ_TABLE_NAME, Key.of(1L), READ_COLUMN_NAMES), + new ApiFunction() { + @Override + public String apply(Struct input) { + return input.getString("Value"); + } + }, + MoreExecutors.directExecutor()); + } + }, + executor); + assertThat(val.get()).isEqualTo("v1"); + } + + @Test + public void asyncRunnerRead() throws Exception { + AsyncRunner runner = client().runAsync(); + ApiFuture> val = + runner.runAsync( + new AsyncWork>() { + @Override + public ApiFuture> doWorkAsync(TransactionContext txn) { + return txn.readAsync(READ_TABLE_NAME, KeySet.all(), READ_COLUMN_NAMES) + .toListAsync( + new Function() { + @Override + public String apply(StructReader input) { + return input.getString("Value"); + } + }, + MoreExecutors.directExecutor()); + } + }, + executor); + assertThat(val.get()).containsExactly("v1", "v2", "v3"); + } +} diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncTransactionManagerTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncTransactionManagerTest.java new file mode 100644 index 0000000000..e2299f3615 --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/AsyncTransactionManagerTest.java @@ -0,0 +1,1045 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.spanner; + +import static com.google.cloud.spanner.MockSpannerTestUtil.INVALID_UPDATE_STATEMENT; +import static com.google.cloud.spanner.MockSpannerTestUtil.READ_COLUMN_NAMES; +import static com.google.cloud.spanner.MockSpannerTestUtil.READ_TABLE_NAME; +import static com.google.cloud.spanner.MockSpannerTestUtil.UPDATE_ABORTED_STATEMENT; +import static com.google.cloud.spanner.MockSpannerTestUtil.UPDATE_COUNT; +import static com.google.cloud.spanner.MockSpannerTestUtil.UPDATE_STATEMENT; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutureCallback; +import com.google.api.core.ApiFutures; +import com.google.api.core.SettableApiFuture; +import com.google.cloud.spanner.AsyncTransactionManager.AsyncTransactionFunction; +import com.google.cloud.spanner.AsyncTransactionManager.AsyncTransactionStep; +import com.google.cloud.spanner.AsyncTransactionManager.CommitTimestampFuture; +import com.google.cloud.spanner.AsyncTransactionManager.TransactionContextFuture; +import com.google.cloud.spanner.MockSpannerServiceImpl.SimulatedExecutionTime; +import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult; +import com.google.cloud.spanner.Options.ReadOption; +import com.google.common.base.Function; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.common.collect.Range; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.protobuf.AbstractMessage; +import com.google.spanner.v1.BatchCreateSessionsRequest; +import com.google.spanner.v1.BeginTransactionRequest; +import com.google.spanner.v1.CommitRequest; +import com.google.spanner.v1.ExecuteBatchDmlRequest; +import com.google.spanner.v1.ExecuteSqlRequest; +import io.grpc.Status; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class AsyncTransactionManagerTest extends AbstractAsyncTransactionTest { + + @Parameter public Executor executor; + + @Parameters(name = "executor = {0}") + public static Collection data() { + return Arrays.asList( + new Object[][] { + {MoreExecutors.directExecutor()}, + {Executors.newSingleThreadExecutor()}, + {Executors.newFixedThreadPool(4)} + }); + } + + /** + * Static helper methods that simplifies creating {@link AsyncTransactionFunction}s for Java7. + * Java8 and higher can use lambda expressions. + */ + public static class AsyncTransactionManagerHelper { + + public static AsyncTransactionFunction readAsync( + final String table, + final KeySet keys, + final Iterable columns, + final ReadOption... options) { + return new AsyncTransactionFunction() { + @Override + public ApiFuture apply(TransactionContext txn, I input) throws Exception { + return ApiFutures.immediateFuture(txn.readAsync(table, keys, columns, options)); + } + }; + } + + public static AsyncTransactionFunction readRowAsync( + final String table, final Key key, final Iterable columns) { + return new AsyncTransactionFunction() { + @Override + public ApiFuture apply(TransactionContext txn, I input) throws Exception { + return txn.readRowAsync(table, key, columns); + } + }; + } + + public static AsyncTransactionFunction buffer(Mutation mutation) { + return buffer(ImmutableList.of(mutation)); + } + + public static AsyncTransactionFunction buffer(final Iterable mutations) { + return new AsyncTransactionFunction() { + @Override + public ApiFuture apply(TransactionContext txn, I input) throws Exception { + txn.buffer(mutations); + return ApiFutures.immediateFuture(null); + } + }; + } + + public static AsyncTransactionFunction executeUpdateAsync(Statement statement) { + return executeUpdateAsync(SettableApiFuture.create(), statement); + } + + public static AsyncTransactionFunction executeUpdateAsync( + final SettableApiFuture result, final Statement statement) { + return new AsyncTransactionFunction() { + @Override + public ApiFuture apply(TransactionContext txn, I input) throws Exception { + ApiFuture updateCount = txn.executeUpdateAsync(statement); + ApiFutures.addCallback( + updateCount, + new ApiFutureCallback() { + @Override + public void onFailure(Throwable t) { + result.setException(t); + } + + @Override + public void onSuccess(Long input) { + result.set(input); + } + }, + MoreExecutors.directExecutor()); + return updateCount; + } + }; + } + + public static AsyncTransactionFunction batchUpdateAsync( + final Statement... statements) { + return batchUpdateAsync(SettableApiFuture.create(), statements); + } + + public static AsyncTransactionFunction batchUpdateAsync( + final SettableApiFuture result, final Statement... statements) { + return new AsyncTransactionFunction() { + @Override + public ApiFuture apply(TransactionContext txn, I input) throws Exception { + ApiFuture updateCounts = txn.batchUpdateAsync(Arrays.asList(statements)); + ApiFutures.addCallback( + updateCounts, + new ApiFutureCallback() { + @Override + public void onFailure(Throwable t) { + result.setException(t); + } + + @Override + public void onSuccess(long[] input) { + result.set(input); + } + }, + MoreExecutors.directExecutor()); + return updateCounts; + } + }; + } + } + + @Test + public void asyncTransactionManagerUpdate() throws Exception { + final SettableApiFuture updateCount = SettableApiFuture.create(); + + try (AsyncTransactionManager manager = client().transactionManagerAsync()) { + TransactionContextFuture txn = manager.beginAsync(); + while (true) { + try { + CommitTimestampFuture commitTimestamp = + txn.then( + AsyncTransactionManagerHelper.executeUpdateAsync( + updateCount, UPDATE_STATEMENT), + executor) + .commitAsync(); + assertThat(updateCount.get()).isEqualTo(UPDATE_COUNT); + assertThat(commitTimestamp.get()).isNotNull(); + break; + } catch (AbortedException e) { + txn = manager.resetForRetryAsync(); + } + } + } + } + + @Test + public void asyncTransactionManagerIsNonBlocking() throws Exception { + SettableApiFuture updateCount = SettableApiFuture.create(); + + mockSpanner.freeze(); + try (AsyncTransactionManager manager = clientWithEmptySessionPool().transactionManagerAsync()) { + TransactionContextFuture txn = manager.beginAsync(); + while (true) { + try { + CommitTimestampFuture commitTimestamp = + txn.then( + AsyncTransactionManagerHelper.executeUpdateAsync( + updateCount, UPDATE_STATEMENT), + executor) + .commitAsync(); + mockSpanner.unfreeze(); + assertThat(updateCount.get(10L, TimeUnit.SECONDS)).isEqualTo(UPDATE_COUNT); + assertThat(commitTimestamp.get(10L, TimeUnit.SECONDS)).isNotNull(); + break; + } catch (AbortedException e) { + txn = manager.resetForRetryAsync(); + } + } + } + } + + @Test + public void asyncTransactionManagerInvalidUpdate() throws Exception { + try (AsyncTransactionManager manager = client().transactionManagerAsync()) { + TransactionContextFuture txn = manager.beginAsync(); + while (true) { + try { + CommitTimestampFuture commitTimestamp = + txn.then( + AsyncTransactionManagerHelper.executeUpdateAsync( + INVALID_UPDATE_STATEMENT), + executor) + .commitAsync(); + commitTimestamp.get(); + fail("missing expected exception"); + } catch (AbortedException e) { + txn = manager.resetForRetryAsync(); + } catch (ExecutionException e) { + manager.rollbackAsync(); + assertThat(e.getCause()).isInstanceOf(SpannerException.class); + SpannerException se = (SpannerException) e.getCause(); + assertThat(se.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + assertThat(se.getMessage()).contains("invalid statement"); + break; + } + } + } + } + + @Test + public void asyncTransactionManagerCommitAborted() throws Exception { + SettableApiFuture updateCount = SettableApiFuture.create(); + final AtomicInteger attempt = new AtomicInteger(); + try (AsyncTransactionManager manager = clientWithEmptySessionPool().transactionManagerAsync()) { + TransactionContextFuture txn = manager.beginAsync(); + while (true) { + try { + attempt.incrementAndGet(); + CommitTimestampFuture commitTimestamp = + txn.then( + AsyncTransactionManagerHelper.executeUpdateAsync( + updateCount, UPDATE_STATEMENT), + executor) + .then( + new AsyncTransactionFunction() { + @Override + public ApiFuture apply(TransactionContext txn, Long input) + throws Exception { + if (attempt.get() == 1) { + mockSpanner.abortTransaction(txn); + } + return ApiFutures.immediateFuture(null); + } + }, + executor) + .commitAsync(); + assertThat(updateCount.get()).isEqualTo(UPDATE_COUNT); + assertThat(commitTimestamp.get()).isNotNull(); + assertThat(attempt.get()).isEqualTo(2); + break; + } catch (AbortedException e) { + txn = manager.resetForRetryAsync(); + } + } + } + } + + @Test + public void asyncTransactionManagerFireAndForgetInvalidUpdate() throws Exception { + final SettableApiFuture updateCount = SettableApiFuture.create(); + + try (AsyncTransactionManager mgr = client().transactionManagerAsync()) { + TransactionContextFuture txn = mgr.beginAsync(); + while (true) { + try { + CommitTimestampFuture ts = + txn.then( + new AsyncTransactionFunction() { + @Override + public ApiFuture apply(TransactionContext txn, Void input) + throws Exception { + // This fire-and-forget update statement should not fail the transaction. + txn.executeUpdateAsync(INVALID_UPDATE_STATEMENT); + ApiFutures.addCallback( + txn.executeUpdateAsync(UPDATE_STATEMENT), + new ApiFutureCallback() { + @Override + public void onFailure(Throwable t) { + updateCount.setException(t); + } + + @Override + public void onSuccess(Long result) { + updateCount.set(result); + } + }, + MoreExecutors.directExecutor()); + return updateCount; + } + }, + executor) + .commitAsync(); + assertThat(updateCount.get()).isEqualTo(UPDATE_COUNT); + assertThat(ts.get()).isNotNull(); + break; + } catch (AbortedException e) { + txn = mgr.resetForRetryAsync(); + } + } + } + } + + @Test + public void asyncTransactionManagerChain() throws Exception { + try (AsyncTransactionManager mgr = client().transactionManagerAsync()) { + TransactionContextFuture txn = mgr.beginAsync(); + while (true) { + try { + CommitTimestampFuture ts = + txn.then( + AsyncTransactionManagerHelper.executeUpdateAsync(UPDATE_STATEMENT), + executor) + .then( + AsyncTransactionManagerHelper.readRowAsync( + READ_TABLE_NAME, Key.of(1L), READ_COLUMN_NAMES), + executor) + .then( + new AsyncTransactionFunction() { + @Override + public ApiFuture apply(TransactionContext txn, Struct input) + throws Exception { + return ApiFutures.immediateFuture(input.getString("Value")); + } + }, + executor) + .then( + new AsyncTransactionFunction() { + @Override + public ApiFuture apply(TransactionContext txn, String input) + throws Exception { + assertThat(input).isEqualTo("v1"); + return ApiFutures.immediateFuture(null); + } + }, + executor) + .commitAsync(); + assertThat(ts.get()).isNotNull(); + break; + } catch (AbortedException e) { + txn = mgr.resetForRetryAsync(); + } + } + } + } + + @Test + public void asyncTransactionManagerChainWithErrorInTheMiddle() throws Exception { + try (AsyncTransactionManager mgr = client().transactionManagerAsync()) { + TransactionContextFuture txn = mgr.beginAsync(); + while (true) { + try { + CommitTimestampFuture ts = + txn.then( + AsyncTransactionManagerHelper.executeUpdateAsync( + INVALID_UPDATE_STATEMENT), + executor) + .then( + new AsyncTransactionFunction() { + @Override + public ApiFuture apply(TransactionContext txn, Long input) + throws Exception { + throw new IllegalStateException("this should not be executed"); + } + }, + executor) + .commitAsync(); + ts.get(); + break; + } catch (AbortedException e) { + txn = mgr.resetForRetryAsync(); + } catch (ExecutionException e) { + mgr.rollbackAsync(); + assertThat(e.getCause()).isInstanceOf(SpannerException.class); + SpannerException se = (SpannerException) e.getCause(); + assertThat(se.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + break; + } + } + } + } + + @Test + public void asyncTransactionManagerUpdateAborted() throws Exception { + try (AsyncTransactionManager mgr = client().transactionManagerAsync()) { + // Temporarily set the result of the update to 2 rows. + mockSpanner.putStatementResult(StatementResult.update(UPDATE_STATEMENT, UPDATE_COUNT + 1L)); + final AtomicInteger attempt = new AtomicInteger(); + + TransactionContextFuture txn = mgr.beginAsync(); + while (true) { + try { + CommitTimestampFuture ts = + txn.then( + new AsyncTransactionFunction() { + @Override + public ApiFuture apply(TransactionContext txn, Void input) + throws Exception { + if (attempt.incrementAndGet() == 1) { + // Abort the first attempt. + mockSpanner.abortTransaction(txn); + } else { + // Set the result of the update statement back to 1 row. + mockSpanner.putStatementResult( + StatementResult.update(UPDATE_STATEMENT, UPDATE_COUNT)); + } + return ApiFutures.immediateFuture(null); + } + }, + executor) + .then( + AsyncTransactionManagerHelper.executeUpdateAsync(UPDATE_STATEMENT), + executor) + .commitAsync(); + assertThat(ts.get()).isNotNull(); + break; + } catch (AbortedException e) { + txn = mgr.resetForRetryAsync(); + } + } + assertThat(attempt.get()).isEqualTo(2); + } finally { + mockSpanner.putStatementResult(StatementResult.update(UPDATE_STATEMENT, UPDATE_COUNT)); + } + } + + @Test + public void asyncTransactionManagerUpdateAbortedWithoutGettingResult() throws Exception { + final AtomicInteger attempt = new AtomicInteger(); + try (AsyncTransactionManager mgr = clientWithEmptySessionPool().transactionManagerAsync()) { + TransactionContextFuture txn = mgr.beginAsync(); + while (true) { + try { + CommitTimestampFuture ts = + txn.then( + new AsyncTransactionFunction() { + @Override + public ApiFuture apply(TransactionContext txn, Void input) + throws Exception { + if (attempt.incrementAndGet() == 1) { + mockSpanner.abortTransaction(txn); + } + // This update statement will be aborted, but the error will not + // propagated to the transaction runner and cause the transaction to + // retry. Instead, the commit call will do that. + txn.executeUpdateAsync(UPDATE_STATEMENT); + // Resolving this future will not resolve the result of the entire + // transaction. The transaction result will be resolved when the commit + // has actually finished successfully. + return ApiFutures.immediateFuture(null); + } + }, + executor) + .commitAsync(); + assertThat(ts.get()).isNotNull(); + assertThat(attempt.get()).isEqualTo(2); + // The server may receive 1 or 2 commit requests depending on whether the call to + // commitAsync() already knows that the transaction has aborted. If it does, it will not + // attempt to call the Commit RPC and instead directly propagate the Aborted error. + assertThat(mockSpanner.getRequestTypes()) + .containsAtLeast( + BatchCreateSessionsRequest.class, + BeginTransactionRequest.class, + ExecuteSqlRequest.class, + BeginTransactionRequest.class, + ExecuteSqlRequest.class, + CommitRequest.class); + break; + } catch (AbortedException e) { + txn = mgr.resetForRetryAsync(); + } + } + } + } + + @Test + public void asyncTransactionManagerCommitFails() throws Exception { + mockSpanner.setCommitExecutionTime( + SimulatedExecutionTime.ofException( + Status.RESOURCE_EXHAUSTED + .withDescription("mutation limit exceeded") + .asRuntimeException())); + try (AsyncTransactionManager mgr = client().transactionManagerAsync()) { + TransactionContextFuture txn = mgr.beginAsync(); + while (true) { + try { + txn.then( + AsyncTransactionManagerHelper.executeUpdateAsync(UPDATE_STATEMENT), + executor) + .commitAsync() + .get(); + fail("missing expected exception"); + } catch (AbortedException e) { + txn = mgr.resetForRetryAsync(); + } catch (ExecutionException e) { + assertThat(e.getCause()).isInstanceOf(SpannerException.class); + SpannerException se = (SpannerException) e.getCause(); + assertThat(se.getErrorCode()).isEqualTo(ErrorCode.RESOURCE_EXHAUSTED); + assertThat(se.getMessage()).contains("mutation limit exceeded"); + break; + } + } + } + } + + @Test + public void asyncTransactionManagerWaitsUntilAsyncUpdateHasFinished() throws Exception { + try (AsyncTransactionManager mgr = clientWithEmptySessionPool().transactionManagerAsync()) { + TransactionContextFuture txn = mgr.beginAsync(); + while (true) { + try { + txn.then( + new AsyncTransactionFunction() { + @Override + public ApiFuture apply(TransactionContext txn, Void input) + throws Exception { + // Shoot-and-forget update. The commit will still wait for this request to + // finish. + txn.executeUpdateAsync(UPDATE_STATEMENT); + return ApiFutures.immediateFuture(null); + } + }, + executor) + .commitAsync() + .get(); + assertThat(mockSpanner.getRequestTypes()) + .containsExactly( + BatchCreateSessionsRequest.class, + BeginTransactionRequest.class, + ExecuteSqlRequest.class, + CommitRequest.class); + break; + } catch (AbortedException e) { + txn = mgr.resetForRetryAsync(); + } + } + } + } + + @Test + public void asyncTransactionManagerBatchUpdate() throws Exception { + final SettableApiFuture result = SettableApiFuture.create(); + try (AsyncTransactionManager mgr = client().transactionManagerAsync()) { + TransactionContextFuture txn = mgr.beginAsync(); + while (true) { + try { + txn.then( + AsyncTransactionManagerHelper.batchUpdateAsync( + result, UPDATE_STATEMENT, UPDATE_STATEMENT), + executor) + .commitAsync() + .get(); + break; + } catch (AbortedException e) { + txn = mgr.resetForRetryAsync(); + } + } + } + assertThat(result.get()).asList().containsExactly(UPDATE_COUNT, UPDATE_COUNT); + } + + @Test + public void asyncTransactionManagerIsNonBlockingWithBatchUpdate() throws Exception { + SettableApiFuture res = SettableApiFuture.create(); + mockSpanner.freeze(); + try (AsyncTransactionManager mgr = clientWithEmptySessionPool().transactionManagerAsync()) { + TransactionContextFuture txn = mgr.beginAsync(); + while (true) { + try { + CommitTimestampFuture ts = + txn.then( + AsyncTransactionManagerHelper.batchUpdateAsync(res, UPDATE_STATEMENT), + executor) + .commitAsync(); + mockSpanner.unfreeze(); + assertThat(ts.get()).isNotNull(); + assertThat(res.get()).asList().containsExactly(UPDATE_COUNT); + break; + } catch (AbortedException e) { + txn = mgr.resetForRetryAsync(); + } + } + } + } + + @Test + public void asyncTransactionManagerInvalidBatchUpdate() throws Exception { + SettableApiFuture result = SettableApiFuture.create(); + try (AsyncTransactionManager mgr = client().transactionManagerAsync()) { + TransactionContextFuture txn = mgr.beginAsync(); + while (true) { + try { + txn.then( + AsyncTransactionManagerHelper.batchUpdateAsync( + result, UPDATE_STATEMENT, INVALID_UPDATE_STATEMENT), + executor) + .commitAsync() + .get(); + fail("missing expected exception"); + } catch (AbortedException e) { + txn = mgr.resetForRetryAsync(); + } catch (ExecutionException e) { + assertThat(e.getCause()).isInstanceOf(SpannerException.class); + SpannerException se = (SpannerException) e.getCause(); + assertThat(se.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + assertThat(se.getMessage()).contains("invalid statement"); + break; + } + } + } + } + + @Test + public void asyncTransactionManagerFireAndForgetInvalidBatchUpdate() throws Exception { + SettableApiFuture result = SettableApiFuture.create(); + try (AsyncTransactionManager mgr = clientWithEmptySessionPool().transactionManagerAsync()) { + TransactionContextFuture txn = mgr.beginAsync(); + while (true) { + try { + txn.then( + new AsyncTransactionFunction() { + @Override + public ApiFuture apply(TransactionContext txn, Void input) + throws Exception { + txn.batchUpdateAsync( + ImmutableList.of(UPDATE_STATEMENT, INVALID_UPDATE_STATEMENT)); + return ApiFutures.immediateFuture(null); + } + }, + executor) + .then( + AsyncTransactionManagerHelper.batchUpdateAsync( + result, UPDATE_STATEMENT, UPDATE_STATEMENT), + executor) + .commitAsync() + .get(); + break; + } catch (AbortedException e) { + txn = mgr.resetForRetryAsync(); + } + } + } + assertThat(result.get()).asList().containsExactly(UPDATE_COUNT, UPDATE_COUNT); + assertThat(mockSpanner.getRequestTypes()) + .containsExactly( + BatchCreateSessionsRequest.class, + BeginTransactionRequest.class, + ExecuteBatchDmlRequest.class, + ExecuteBatchDmlRequest.class, + CommitRequest.class); + } + + @Test + public void asyncTransactionManagerBatchUpdateAborted() throws Exception { + final AtomicInteger attempt = new AtomicInteger(); + try (AsyncTransactionManager mgr = clientWithEmptySessionPool().transactionManagerAsync()) { + TransactionContextFuture txn = mgr.beginAsync(); + while (true) { + try { + txn.then( + new AsyncTransactionFunction() { + @Override + public ApiFuture apply(TransactionContext txn, Void input) + throws Exception { + if (attempt.incrementAndGet() == 1) { + return txn.batchUpdateAsync( + ImmutableList.of(UPDATE_STATEMENT, UPDATE_ABORTED_STATEMENT)); + } else { + return txn.batchUpdateAsync( + ImmutableList.of(UPDATE_STATEMENT, UPDATE_STATEMENT)); + } + } + }, + executor) + .commitAsync() + .get(); + break; + } catch (AbortedException e) { + txn = mgr.resetForRetryAsync(); + } + } + } + assertThat(attempt.get()).isEqualTo(2); + // There should only be 1 CommitRequest, as the first attempt should abort already after the + // ExecuteBatchDmlRequest. + assertThat(mockSpanner.getRequestTypes()) + .containsExactly( + BatchCreateSessionsRequest.class, + BeginTransactionRequest.class, + ExecuteBatchDmlRequest.class, + BeginTransactionRequest.class, + ExecuteBatchDmlRequest.class, + CommitRequest.class); + } + + @Test + public void asyncTransactionManagerWithBatchUpdateCommitAborted() throws Exception { + try (AsyncTransactionManager mgr = clientWithEmptySessionPool().transactionManagerAsync()) { + // Temporarily set the result of the update to 2 rows. + mockSpanner.putStatementResult(StatementResult.update(UPDATE_STATEMENT, UPDATE_COUNT + 1L)); + final AtomicInteger attempt = new AtomicInteger(); + TransactionContextFuture txn = mgr.beginAsync(); + while (true) { + final SettableApiFuture result = SettableApiFuture.create(); + try { + txn.then( + new AsyncTransactionFunction() { + @Override + public ApiFuture apply(TransactionContext txn, Void input) + throws Exception { + if (attempt.get() > 0) { + // Set the result of the update statement back to 1 row. + mockSpanner.putStatementResult( + StatementResult.update(UPDATE_STATEMENT, UPDATE_COUNT)); + } + return ApiFutures.immediateFuture(null); + } + }, + executor) + .then( + AsyncTransactionManagerHelper.batchUpdateAsync( + result, UPDATE_STATEMENT, UPDATE_STATEMENT), + executor) + .then( + new AsyncTransactionFunction() { + @Override + public ApiFuture apply(TransactionContext txn, long[] input) + throws Exception { + if (attempt.incrementAndGet() == 1) { + mockSpanner.abortTransaction(txn); + } + return ApiFutures.immediateFuture(null); + } + }, + executor) + .commitAsync() + .get(); + assertThat(result.get()).asList().containsExactly(UPDATE_COUNT, UPDATE_COUNT); + assertThat(attempt.get()).isEqualTo(2); + break; + } catch (AbortedException e) { + txn = mgr.resetForRetryAsync(); + } + } + } finally { + mockSpanner.putStatementResult(StatementResult.update(UPDATE_STATEMENT, UPDATE_COUNT)); + } + assertThat(mockSpanner.getRequestTypes()) + .containsExactly( + BatchCreateSessionsRequest.class, + BeginTransactionRequest.class, + ExecuteBatchDmlRequest.class, + CommitRequest.class, + BeginTransactionRequest.class, + ExecuteBatchDmlRequest.class, + CommitRequest.class); + } + + @Test + public void asyncTransactionManagerBatchUpdateAbortedWithoutGettingResult() throws Exception { + final AtomicInteger attempt = new AtomicInteger(); + try (AsyncTransactionManager mgr = clientWithEmptySessionPool().transactionManagerAsync()) { + TransactionContextFuture txn = mgr.beginAsync(); + while (true) { + try { + txn.then( + new AsyncTransactionFunction() { + @Override + public ApiFuture apply(TransactionContext txn, Void input) + throws Exception { + if (attempt.incrementAndGet() == 1) { + mockSpanner.abortTransaction(txn); + } + // This update statement will be aborted, but the error will not propagated to + // the transaction manager and cause the transaction to retry. Instead, the + // commit call will do that. Depending on the timing, that will happen + // directly in the transaction manager if the ABORTED error has already been + // returned by the batch update call before the commit call starts. Otherwise, + // the backend will return an ABORTED error for the commit call. + txn.batchUpdateAsync(ImmutableList.of(UPDATE_STATEMENT, UPDATE_STATEMENT)); + return ApiFutures.immediateFuture(null); + } + }, + executor) + .commitAsync() + .get(); + break; + } catch (AbortedException e) { + txn = mgr.resetForRetryAsync(); + } + } + } + assertThat(attempt.get()).isEqualTo(2); + Iterable> requests = mockSpanner.getRequestTypes(); + int size = Iterables.size(requests); + assertThat(size).isIn(Range.closed(6, 7)); + if (size == 6) { + assertThat(requests) + .containsExactly( + BatchCreateSessionsRequest.class, + BeginTransactionRequest.class, + ExecuteBatchDmlRequest.class, + BeginTransactionRequest.class, + ExecuteBatchDmlRequest.class, + CommitRequest.class); + } else { + assertThat(requests) + .containsExactly( + BatchCreateSessionsRequest.class, + BeginTransactionRequest.class, + ExecuteBatchDmlRequest.class, + CommitRequest.class, + BeginTransactionRequest.class, + ExecuteBatchDmlRequest.class, + CommitRequest.class); + } + } + + @Test + public void asyncTransactionManagerWithBatchUpdateCommitFails() throws Exception { + mockSpanner.setCommitExecutionTime( + SimulatedExecutionTime.ofException( + Status.RESOURCE_EXHAUSTED + .withDescription("mutation limit exceeded") + .asRuntimeException())); + try (AsyncTransactionManager mgr = clientWithEmptySessionPool().transactionManagerAsync()) { + TransactionContextFuture txn = mgr.beginAsync(); + while (true) { + try { + txn.then( + AsyncTransactionManagerHelper.batchUpdateAsync( + UPDATE_STATEMENT, UPDATE_STATEMENT), + executor) + .commitAsync() + .get(); + fail("missing expected exception"); + } catch (AbortedException e) { + txn = mgr.resetForRetryAsync(); + } catch (ExecutionException e) { + assertThat(e.getCause()).isInstanceOf(SpannerException.class); + SpannerException se = (SpannerException) e.getCause(); + assertThat(se.getErrorCode()).isEqualTo(ErrorCode.RESOURCE_EXHAUSTED); + assertThat(se.getMessage()).contains("mutation limit exceeded"); + break; + } + } + } + assertThat(mockSpanner.getRequestTypes()) + .containsExactly( + BatchCreateSessionsRequest.class, + BeginTransactionRequest.class, + ExecuteBatchDmlRequest.class, + CommitRequest.class); + } + + @Test + public void asyncTransactionManagerWaitsUntilAsyncBatchUpdateHasFinished() throws Exception { + try (AsyncTransactionManager mgr = clientWithEmptySessionPool().transactionManagerAsync()) { + TransactionContextFuture txn = mgr.beginAsync(); + while (true) { + try { + txn.then( + new AsyncTransactionFunction() { + @Override + public ApiFuture apply(TransactionContext txn, Void input) + throws Exception { + txn.batchUpdateAsync(ImmutableList.of(UPDATE_STATEMENT)); + return ApiFutures.immediateFuture(null); + } + }, + executor) + .commitAsync() + .get(); + break; + } catch (AbortedException e) { + txn = mgr.resetForRetryAsync(); + } + } + } + assertThat(mockSpanner.getRequestTypes()) + .containsExactly( + BatchCreateSessionsRequest.class, + BeginTransactionRequest.class, + ExecuteBatchDmlRequest.class, + CommitRequest.class); + } + + @Test + public void asyncTransactionManagerReadRow() throws Exception { + ApiFuture val; + try (AsyncTransactionManager mgr = client().transactionManagerAsync()) { + TransactionContextFuture txn = mgr.beginAsync(); + while (true) { + try { + AsyncTransactionStep step; + val = + step = + txn.then( + AsyncTransactionManagerHelper.readRowAsync( + READ_TABLE_NAME, Key.of(1L), READ_COLUMN_NAMES), + executor) + .then( + new AsyncTransactionFunction() { + @Override + public ApiFuture apply(TransactionContext txn, Struct input) + throws Exception { + return ApiFutures.immediateFuture(input.getString("Value")); + } + }, + executor); + step.commitAsync().get(); + break; + } catch (AbortedException e) { + txn = mgr.resetForRetryAsync(); + } + } + } + assertThat(val.get()).isEqualTo("v1"); + } + + @Test + public void asyncTransactionManagerRead() throws Exception { + AsyncTransactionStep> res; + try (AsyncTransactionManager mgr = client().transactionManagerAsync()) { + TransactionContextFuture txn = mgr.beginAsync(); + while (true) { + try { + res = + txn.then( + new AsyncTransactionFunction>() { + @Override + public ApiFuture> apply( + TransactionContext txn, Void input) throws Exception { + return txn.readAsync(READ_TABLE_NAME, KeySet.all(), READ_COLUMN_NAMES) + .toListAsync( + new Function() { + @Override + public String apply(StructReader input) { + return input.getString("Value"); + } + }, + MoreExecutors.directExecutor()); + } + }, + executor); + // Commit the transaction. + res.commitAsync().get(); + break; + } catch (AbortedException e) { + txn = mgr.resetForRetryAsync(); + } + } + } + assertThat(res.get()).containsExactly("v1", "v2", "v3"); + } + + @Test + public void asyncTransactionManagerQuery() throws Exception { + mockSpanner.putStatementResult( + StatementResult.query( + Statement.of("SELECT FirstName FROM Singers WHERE ID=1"), + MockSpannerTestUtil.READ_FIRST_NAME_SINGERS_RESULTSET)); + final long singerId = 1L; + try (AsyncTransactionManager manager = client().transactionManagerAsync()) { + TransactionContextFuture txn = manager.beginAsync(); + while (true) { + final String column = "FirstName"; + CommitTimestampFuture commitTimestamp = + txn.then( + new AsyncTransactionFunction() { + @Override + public ApiFuture apply(TransactionContext txn, Void input) + throws Exception { + return txn.readRowAsync( + "Singers", Key.of(singerId), Collections.singleton(column)); + } + }, + executor) + .then( + new AsyncTransactionFunction() { + @Override + public ApiFuture apply(TransactionContext txn, Struct input) + throws Exception { + String name = input.getString(column); + txn.buffer( + Mutation.newUpdateBuilder("Singers") + .set(column) + .to(name.toUpperCase()) + .build()); + return ApiFutures.immediateFuture(null); + } + }, + executor) + .commitAsync(); + try { + commitTimestamp.get(); + break; + } catch (AbortedException e) { + Thread.sleep(e.getRetryDelayInMillis() / 1000); + txn = manager.resetForRetryAsync(); + } + } + } + } +} diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BackendExhaustedTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BackendExhaustedTest.java index f7d6653faf..cb1244f8e3 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BackendExhaustedTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BackendExhaustedTest.java @@ -153,7 +153,7 @@ public void setUp() throws Exception { } @After - public void tearDown() throws Exception { + public void tearDown() { mockSpanner.reset(); mockSpanner.removeAllExecutionTimes(); // This test case force-closes the Spanner instance as it would otherwise wait @@ -209,7 +209,7 @@ public void run() { runner.run( new TransactionCallable() { @Override - public Long run(TransactionContext transaction) throws Exception { + public Long run(TransactionContext transaction) { return transaction.executeUpdate(UPDATE_STATEMENT); } }); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BackupIdTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BackupIdTest.java index c3405a05a0..629d026143 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BackupIdTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BackupIdTest.java @@ -19,16 +19,13 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Unit tests for {@link com.google.cloud.spanner.BackupId}. */ @RunWith(JUnit4.class) public class BackupIdTest { - @Rule public ExpectedException expectedException = ExpectedException.none(); @Test public void basics() { diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BackupTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BackupTest.java index 0e5e1afd39..3bdc673838 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BackupTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BackupTest.java @@ -17,6 +17,9 @@ package com.google.cloud.spanner; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; @@ -29,9 +32,7 @@ import com.google.cloud.spanner.BackupInfo.State; import java.util.Arrays; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.mockito.Mock; @@ -46,7 +47,6 @@ public class BackupTest { private static final String DB = "projects/test-project/instances/test-instance/databases/db-1"; private static final Timestamp EXP_TIME = Timestamp.ofTimeSecondsAndNanos(1000L, 1000); - @Rule public ExpectedException expectedException = ExpectedException.none(); @Mock DatabaseAdminClient dbClient; @Before @@ -56,7 +56,7 @@ public void setUp() { .thenAnswer( new Answer() { @Override - public Builder answer(InvocationOnMock invocation) throws Throwable { + public Builder answer(InvocationOnMock invocation) { return new Backup.Builder(dbClient, (BackupId) invocation.getArguments()[0]); } }); @@ -102,8 +102,12 @@ public void createWithoutSource() { .newBackupBuilder(BackupId.of("test-project", "dest-instance", "backup-id")) .setExpireTime(expireTime) .build(); - expectedException.expect(IllegalStateException.class); - backup.create(); + try { + backup.create(); + fail("Expected exception"); + } catch (IllegalStateException e) { + assertNotNull(e.getMessage()); + } } @Test @@ -113,8 +117,12 @@ public void createWithoutExpireTime() { .newBackupBuilder(BackupId.of("test-project", "instance-id", "backup-id")) .setDatabase(DatabaseId.of("test-project", "instance-id", "src-database")) .build(); - expectedException.expect(IllegalStateException.class); - backup.create(); + try { + backup.create(); + fail("Expected exception"); + } catch (IllegalStateException e) { + assertNotNull(e.getMessage()); + } } @Test @@ -207,8 +215,12 @@ public void updateExpireTimeWithoutExpireTime() { dbClient .newBackupBuilder(BackupId.of("test-project", "test-instance", "test-backup")) .build(); - expectedException.expect(IllegalStateException.class); - backup.updateExpireTime(); + try { + backup.updateExpireTime(); + fail("Expected exception"); + } catch (IllegalStateException e) { + assertNotNull(e.getMessage()); + } } @Test @@ -228,8 +240,12 @@ public void restoreWithoutDestination() { dbClient .newBackupBuilder(BackupId.of("test-project", "test-instance", "test-backup")) .build(); - expectedException.expect(NullPointerException.class); - backup.restore(null); + try { + backup.restore(null); + fail("Expected exception"); + } catch (NullPointerException e) { + assertNull(e.getMessage()); + } } @Test diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BaseSessionPoolTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BaseSessionPoolTest.java index 26bbef4535..1bcb303f72 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BaseSessionPoolTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BaseSessionPoolTest.java @@ -59,7 +59,7 @@ public void release(ScheduledExecutorService executor) { } SessionImpl mockSession() { - SessionImpl session = mock(SessionImpl.class); + final SessionImpl session = mock(SessionImpl.class); when(session.getName()) .thenReturn( "projects/dummy/instances/dummy/database/dummy/sessions/session" + sessionIndex); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BatchClientImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BatchClientImplTest.java index 002a992b6f..f53a3ac406 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BatchClientImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BatchClientImplTest.java @@ -102,7 +102,7 @@ public void testBatchReadOnlyTxnWithBound() throws Exception { } @Test - public void testBatchReadOnlyTxnWithTxnId() throws Exception { + public void testBatchReadOnlyTxnWithTxnId() { when(txnID.getSessionId()).thenReturn(SESSION_NAME); when(txnID.getTransactionId()).thenReturn(TXN_ID); Timestamp t = Timestamp.parseTimestamp(TIMESTAMP); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BatchCreateSessionsTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BatchCreateSessionsTest.java index 222b393113..abac3bd134 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BatchCreateSessionsTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BatchCreateSessionsTest.java @@ -102,7 +102,7 @@ public static void stopServer() throws InterruptedException { } @Before - public void setUp() throws IOException { + public void setUp() { mockSpanner.reset(); mockSpanner.removeAllExecutionTimes(); } @@ -237,7 +237,7 @@ public void testSpannerReturnsResourceExhausted() throws InterruptedException { } @Test - public void testPrepareSessionFailPropagatesToUser() throws InterruptedException { + public void testPrepareSessionFailPropagatesToUser() { // Do not create any sessions by default. // This also means that when a read/write session is requested, the session pool // will start preparing a read session at that time. Any errors that might occur @@ -256,7 +256,7 @@ public void testPrepareSessionFailPropagatesToUser() throws InterruptedException runner.run( new TransactionCallable() { @Override - public Void run(TransactionContext transaction) throws Exception { + public Void run(TransactionContext transaction) { return null; } }); @@ -297,7 +297,7 @@ public void testPrepareSessionFailDoesNotPropagateToUser() throws InterruptedExc runner.run( new TransactionCallable() { @Override - public Void run(TransactionContext transaction) throws Exception { + public Void run(TransactionContext transaction) { return null; } }); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseAdminClientImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseAdminClientImplTest.java index e77fb38439..b2676473cf 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseAdminClientImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseAdminClientImplTest.java @@ -338,7 +338,7 @@ public void listBackups() { } @Test - public void updateBackup() throws Exception { + public void updateBackup() { Timestamp t = Timestamp.ofTimeMicroseconds( TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis()) diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseAdminClientTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseAdminClientTest.java index ea695c3611..8b30df912d 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseAdminClientTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseAdminClientTest.java @@ -48,7 +48,6 @@ import io.grpc.Server; import io.grpc.Status; import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder; -import java.io.IOException; import java.net.InetSocketAddress; import java.util.Arrays; import java.util.Collections; @@ -110,7 +109,7 @@ public static void stopServer() throws Exception { @SuppressWarnings("rawtypes") @Before - public void setUp() throws IOException { + public void setUp() { mockDatabaseAdmin.reset(); mockOperations.reset(); SpannerOptions.Builder builder = SpannerOptions.newBuilder(); @@ -214,7 +213,7 @@ public ManagedChannelBuilder apply(ManagedChannelBuilder input) { } @After - public void tearDown() throws Exception { + public void tearDown() { mockDatabaseAdmin.reset(); mockDatabaseAdmin.removeAllExecutionTimes(); mockOperations.reset(); @@ -311,7 +310,7 @@ public void databaseBackup() throws InterruptedException, ExecutionException { } @Test - public void dbAdminCreateBackupAlreadyExists() throws InterruptedException, ExecutionException { + public void dbAdminCreateBackupAlreadyExists() throws InterruptedException { OperationFuture op = client.createBackup(INSTANCE_ID, BCK_ID, DB_ID, after7Days()); try { @@ -325,7 +324,7 @@ public void dbAdminCreateBackupAlreadyExists() throws InterruptedException, Exec } @Test - public void backupCreateAlreadyExists() throws InterruptedException, ExecutionException { + public void backupCreateAlreadyExists() throws InterruptedException { Backup backup = client .newBackupBuilder(BackupId.of(PROJECT_ID, INSTANCE_ID, BCK_ID)) @@ -343,7 +342,7 @@ public void backupCreateAlreadyExists() throws InterruptedException, ExecutionEx } @Test - public void databaseBackupAlreadyExists() throws InterruptedException, ExecutionException { + public void databaseBackupAlreadyExists() throws InterruptedException { Database db = client.getDatabase(INSTANCE_ID, DB_ID); OperationFuture op = db.backup( @@ -362,7 +361,7 @@ public void databaseBackupAlreadyExists() throws InterruptedException, Execution } @Test - public void dbAdminCreateBackupDbNotFound() throws InterruptedException, ExecutionException { + public void dbAdminCreateBackupDbNotFound() throws InterruptedException { final String backupId = "other-backup-id"; OperationFuture op = client.createBackup(INSTANCE_ID, backupId, "does-not-exist", after7Days()); @@ -376,7 +375,7 @@ public void dbAdminCreateBackupDbNotFound() throws InterruptedException, Executi } @Test - public void backupCreateDbNotFound() throws InterruptedException, ExecutionException { + public void backupCreateDbNotFound() throws InterruptedException { final String backupId = "other-backup-id"; Backup backup = client @@ -394,7 +393,7 @@ public void backupCreateDbNotFound() throws InterruptedException, ExecutionExcep } @Test - public void databaseBackupDbNotFound() throws InterruptedException, ExecutionException { + public void databaseBackupDbNotFound() throws InterruptedException { final String backupId = "other-backup-id"; Database db = new Database( diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseAdminGaxTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseAdminGaxTest.java index caeed50da6..7e4dfb699c 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseAdminGaxTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseAdminGaxTest.java @@ -294,7 +294,7 @@ public Void apply(Builder input) { } @After - public void tearDown() throws Exception { + public void tearDown() { spanner.close(); } 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 ac57476c12..9bff8fbc25 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 @@ -16,35 +16,42 @@ package com.google.cloud.spanner; +import static com.google.cloud.spanner.MockSpannerTestUtil.SELECT1; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutures; import com.google.api.gax.grpc.testing.LocalChannelProvider; import com.google.api.gax.retrying.RetrySettings; import com.google.cloud.NoCredentials; +import com.google.cloud.spanner.AsyncResultSet.CallbackResponse; +import com.google.cloud.spanner.AsyncResultSet.ReadyCallback; +import com.google.cloud.spanner.AsyncRunner.AsyncWork; import com.google.cloud.spanner.MockSpannerServiceImpl.SimulatedExecutionTime; import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult; import com.google.cloud.spanner.ReadContext.QueryAnalyzeMode; import com.google.cloud.spanner.TransactionRunner.TransactionCallable; import com.google.common.base.Stopwatch; +import com.google.common.util.concurrent.SettableFuture; import com.google.protobuf.AbstractMessage; -import com.google.protobuf.ListValue; import com.google.spanner.v1.ExecuteSqlRequest; import com.google.spanner.v1.ExecuteSqlRequest.QueryMode; import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions; -import com.google.spanner.v1.ResultSetMetadata; -import com.google.spanner.v1.StructType; -import com.google.spanner.v1.StructType.Field; -import com.google.spanner.v1.TypeCode; import io.grpc.Server; import io.grpc.Status; import io.grpc.StatusRuntimeException; import io.grpc.inprocess.InProcessServerBuilder; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; @@ -72,37 +79,17 @@ public class DatabaseClientImplTest { private static final Statement INVALID_UPDATE_STATEMENT = Statement.of("UPDATE NON_EXISTENT_TABLE SET BAR=1 WHERE BAZ=2"); private static final long UPDATE_COUNT = 1L; - private static final Statement SELECT1 = Statement.of("SELECT 1 AS COL1"); - private static final ResultSetMetadata SELECT1_METADATA = - ResultSetMetadata.newBuilder() - .setRowType( - StructType.newBuilder() - .addFields( - Field.newBuilder() - .setName("COL1") - .setType( - com.google.spanner.v1.Type.newBuilder() - .setCode(TypeCode.INT64) - .build()) - .build()) - .build()) - .build(); - private static final com.google.spanner.v1.ResultSet SELECT1_RESULTSET = - com.google.spanner.v1.ResultSet.newBuilder() - .addRows( - ListValue.newBuilder() - .addValues(com.google.protobuf.Value.newBuilder().setStringValue("1").build()) - .build()) - .setMetadata(SELECT1_METADATA) - .build(); private Spanner spanner; + private Spanner spannerWithEmptySessionPool; + private static final ExecutorService executor = Executors.newSingleThreadExecutor(); @BeforeClass public static void startStaticServer() throws IOException { mockSpanner = new MockSpannerServiceImpl(); mockSpanner.setAbortProbability(0.0D); // We don't want any unpredictable aborted transactions. mockSpanner.putStatementResult(StatementResult.update(UPDATE_STATEMENT, UPDATE_COUNT)); - mockSpanner.putStatementResult(StatementResult.query(SELECT1, SELECT1_RESULTSET)); + mockSpanner.putStatementResult( + StatementResult.query(SELECT1, MockSpannerTestUtil.SELECT1_RESULTSET)); mockSpanner.putStatementResult( StatementResult.exception( INVALID_UPDATE_STATEMENT, @@ -123,26 +110,483 @@ public static void startStaticServer() throws IOException { public static void stopServer() throws InterruptedException { server.shutdown(); server.awaitTermination(); + executor.shutdown(); } @Before - public void setUp() throws IOException { + public void setUp() { spanner = SpannerOptions.newBuilder() .setProjectId(TEST_PROJECT) .setChannelProvider(channelProvider) .setCredentials(NoCredentials.getInstance()) + .setSessionPoolOption(SessionPoolOptions.newBuilder().setFailOnSessionLeak().build()) + .build() + .getService(); + spannerWithEmptySessionPool = + spanner + .getOptions() + .toBuilder() + .setSessionPoolOption( + SessionPoolOptions.newBuilder().setMinSessions(0).setFailOnSessionLeak().build()) .build() .getService(); } @After - public void tearDown() throws Exception { + public void tearDown() { + mockSpanner.unfreeze(); spanner.close(); + spannerWithEmptySessionPool.close(); mockSpanner.reset(); mockSpanner.removeAllExecutionTimes(); } + @Test + public void write() { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); + client.write( + Arrays.asList( + Mutation.newInsertBuilder("FOO").set("ID").to(1L).set("NAME").to("Bar").build())); + } + + @Test + public void writeAtLeastOnce() { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); + client.writeAtLeastOnce( + Arrays.asList( + Mutation.newInsertBuilder("FOO").set("ID").to(1L).set("NAME").to("Bar").build())); + } + + @Test + public void singleUse() { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); + try (ResultSet rs = client.singleUse().executeQuery(SELECT1)) { + assertThat(rs.next()).isTrue(); + assertThat(rs.getLong(0)).isEqualTo(1L); + assertThat(rs.next()).isFalse(); + } + } + + @Test + public void singleUseIsNonBlocking() { + mockSpanner.freeze(); + // Use a Spanner instance with no initial sessions in the pool to show that getting a session + // from the pool and then preparing a query is non-blocking (i.e. does not wait on a reply from + // the server). + DatabaseClient client = + spannerWithEmptySessionPool.getDatabaseClient( + DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); + try (ResultSet rs = client.singleUse().executeQuery(SELECT1)) { + mockSpanner.unfreeze(); + assertThat(rs.next()).isTrue(); + assertThat(rs.getLong(0)).isEqualTo(1L); + assertThat(rs.next()).isFalse(); + } + } + + @Test + public void singleUseAsync() throws Exception { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); + final AtomicInteger rowCount = new AtomicInteger(); + ApiFuture res; + try (AsyncResultSet rs = client.singleUse().executeQueryAsync(SELECT1)) { + res = + rs.setCallback( + executor, + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + while (true) { + switch (resultSet.tryNext()) { + case OK: + rowCount.incrementAndGet(); + break; + case DONE: + return CallbackResponse.DONE; + case NOT_READY: + return CallbackResponse.CONTINUE; + } + } + } + }); + } + res.get(); + assertThat(rowCount.get()).isEqualTo(1); + } + + @Test + public void singleUseBound() { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); + try (ResultSet rs = + client + .singleUse(TimestampBound.ofExactStaleness(15L, TimeUnit.SECONDS)) + .executeQuery(SELECT1)) { + assertThat(rs.next()).isTrue(); + assertThat(rs.getLong(0)).isEqualTo(1L); + assertThat(rs.next()).isFalse(); + } + } + + @Test + public void singleUseBoundIsNonBlocking() { + mockSpanner.freeze(); + DatabaseClient client = + spannerWithEmptySessionPool.getDatabaseClient( + DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); + try (ResultSet rs = + client + .singleUse(TimestampBound.ofExactStaleness(15L, TimeUnit.SECONDS)) + .executeQuery(SELECT1)) { + mockSpanner.unfreeze(); + assertThat(rs.next()).isTrue(); + assertThat(rs.getLong(0)).isEqualTo(1L); + assertThat(rs.next()).isFalse(); + } + } + + @Test + public void singleUseBoundAsync() throws Exception { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); + final AtomicInteger rowCount = new AtomicInteger(); + ApiFuture res; + try (AsyncResultSet rs = + client + .singleUse(TimestampBound.ofExactStaleness(15L, TimeUnit.SECONDS)) + .executeQueryAsync(SELECT1)) { + res = + rs.setCallback( + executor, + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + while (true) { + switch (resultSet.tryNext()) { + case OK: + rowCount.incrementAndGet(); + break; + case DONE: + return CallbackResponse.DONE; + case NOT_READY: + return CallbackResponse.CONTINUE; + } + } + } + }); + } + res.get(); + assertThat(rowCount.get()).isEqualTo(1); + } + + @Test + public void singleUseTransaction() { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); + try (ResultSet rs = client.singleUseReadOnlyTransaction().executeQuery(SELECT1)) { + assertThat(rs.next()).isTrue(); + assertThat(rs.getLong(0)).isEqualTo(1L); + assertThat(rs.next()).isFalse(); + } + } + + @Test + public void singleUseTransactionIsNonBlocking() { + mockSpanner.freeze(); + DatabaseClient client = + spannerWithEmptySessionPool.getDatabaseClient( + DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); + try (ResultSet rs = client.singleUseReadOnlyTransaction().executeQuery(SELECT1)) { + mockSpanner.unfreeze(); + assertThat(rs.next()).isTrue(); + assertThat(rs.getLong(0)).isEqualTo(1L); + assertThat(rs.next()).isFalse(); + } + } + + @Test + public void singleUseTransactionBound() { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); + try (ResultSet rs = + client + .singleUseReadOnlyTransaction(TimestampBound.ofExactStaleness(15L, TimeUnit.SECONDS)) + .executeQuery(SELECT1)) { + assertThat(rs.next()).isTrue(); + assertThat(rs.getLong(0)).isEqualTo(1L); + assertThat(rs.next()).isFalse(); + } + } + + @Test + public void singleUseTransactionBoundIsNonBlocking() { + mockSpanner.freeze(); + DatabaseClient client = + spannerWithEmptySessionPool.getDatabaseClient( + DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); + try (ResultSet rs = + client + .singleUseReadOnlyTransaction(TimestampBound.ofExactStaleness(15L, TimeUnit.SECONDS)) + .executeQuery(SELECT1)) { + mockSpanner.unfreeze(); + assertThat(rs.next()).isTrue(); + assertThat(rs.getLong(0)).isEqualTo(1L); + assertThat(rs.next()).isFalse(); + } + } + + @Test + public void readOnlyTransaction() { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); + try (ReadOnlyTransaction tx = client.readOnlyTransaction()) { + try (ResultSet rs = tx.executeQuery(SELECT1)) { + assertThat(rs.next()).isTrue(); + assertThat(rs.getLong(0)).isEqualTo(1L); + assertThat(rs.next()).isFalse(); + } + } + } + + @Test + public void readOnlyTransactionIsNonBlocking() { + mockSpanner.freeze(); + DatabaseClient client = + spannerWithEmptySessionPool.getDatabaseClient( + DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); + try (ReadOnlyTransaction tx = client.readOnlyTransaction()) { + try (ResultSet rs = tx.executeQuery(SELECT1)) { + mockSpanner.unfreeze(); + assertThat(rs.next()).isTrue(); + assertThat(rs.getLong(0)).isEqualTo(1L); + assertThat(rs.next()).isFalse(); + } + } + } + + @Test + public void readOnlyTransactionBound() { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); + try (ReadOnlyTransaction tx = + client.readOnlyTransaction(TimestampBound.ofExactStaleness(15L, TimeUnit.SECONDS))) { + try (ResultSet rs = tx.executeQuery(SELECT1)) { + assertThat(rs.next()).isTrue(); + assertThat(rs.getLong(0)).isEqualTo(1L); + assertThat(rs.next()).isFalse(); + } + } + } + + @Test + public void readOnlyTransactionBoundIsNonBlocking() { + mockSpanner.freeze(); + DatabaseClient client = + spannerWithEmptySessionPool.getDatabaseClient( + DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); + try (ReadOnlyTransaction tx = + client.readOnlyTransaction(TimestampBound.ofExactStaleness(15L, TimeUnit.SECONDS))) { + try (ResultSet rs = tx.executeQuery(SELECT1)) { + mockSpanner.unfreeze(); + assertThat(rs.next()).isTrue(); + assertThat(rs.getLong(0)).isEqualTo(1L); + assertThat(rs.next()).isFalse(); + } + } + } + + @Test + public void readWriteTransaction() { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); + TransactionRunner runner = client.readWriteTransaction(); + runner.run( + new TransactionCallable() { + @Override + public Void run(TransactionContext transaction) throws Exception { + transaction.executeUpdate(UPDATE_STATEMENT); + return null; + } + }); + } + + @Test + public void readWriteTransactionIsNonBlocking() { + mockSpanner.freeze(); + DatabaseClient client = + spannerWithEmptySessionPool.getDatabaseClient( + DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); + TransactionRunner runner = client.readWriteTransaction(); + // The runner.run(...) method cannot be made non-blocking, as it returns the result of the + // transaction. + mockSpanner.unfreeze(); + runner.run( + new TransactionCallable() { + @Override + public Void run(TransactionContext transaction) throws Exception { + transaction.executeUpdate(UPDATE_STATEMENT); + return null; + } + }); + } + + @Test + public void runAsync() throws Exception { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); + ExecutorService executor = Executors.newSingleThreadExecutor(); + AsyncRunner runner = client.runAsync(); + ApiFuture fut = + runner.runAsync( + new AsyncWork() { + @Override + public ApiFuture doWorkAsync(TransactionContext txn) { + return ApiFutures.immediateFuture(txn.executeUpdate(UPDATE_STATEMENT)); + } + }, + executor); + assertThat(fut.get()).isEqualTo(UPDATE_COUNT); + executor.shutdown(); + } + + @Test + public void runAsyncIsNonBlocking() throws Exception { + mockSpanner.freeze(); + DatabaseClient client = + spannerWithEmptySessionPool.getDatabaseClient( + DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); + ExecutorService executor = Executors.newSingleThreadExecutor(); + AsyncRunner runner = client.runAsync(); + ApiFuture fut = + runner.runAsync( + new AsyncWork() { + @Override + public ApiFuture doWorkAsync(TransactionContext txn) { + return ApiFutures.immediateFuture(txn.executeUpdate(UPDATE_STATEMENT)); + } + }, + executor); + mockSpanner.unfreeze(); + assertThat(fut.get()).isEqualTo(UPDATE_COUNT); + executor.shutdown(); + } + + @Test + public void runAsyncWithException() throws Exception { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); + ExecutorService executor = Executors.newSingleThreadExecutor(); + AsyncRunner runner = client.runAsync(); + ApiFuture fut = + runner.runAsync( + new AsyncWork() { + @Override + public ApiFuture doWorkAsync(TransactionContext txn) { + return ApiFutures.immediateFuture(txn.executeUpdate(INVALID_UPDATE_STATEMENT)); + } + }, + executor); + try { + fut.get(); + fail("missing expected exception"); + } catch (ExecutionException e) { + assertThat(e.getCause()).isInstanceOf(SpannerException.class); + SpannerException se = (SpannerException) e.getCause(); + assertThat(se.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + } + executor.shutdown(); + } + + @Test + public void transactionManager() throws Exception { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); + try (TransactionManager txManager = client.transactionManager()) { + while (true) { + TransactionContext tx = txManager.begin(); + try { + tx.executeUpdate(UPDATE_STATEMENT); + txManager.commit(); + break; + } catch (AbortedException e) { + Thread.sleep(e.getRetryDelayInMillis() / 1000); + tx = txManager.resetForRetry(); + } + } + } + } + + @Test + public void transactionManagerIsNonBlocking() throws Exception { + mockSpanner.freeze(); + DatabaseClient client = + spannerWithEmptySessionPool.getDatabaseClient( + DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); + try (TransactionManager txManager = client.transactionManager()) { + while (true) { + mockSpanner.unfreeze(); + TransactionContext tx = txManager.begin(); + try { + tx.executeUpdate(UPDATE_STATEMENT); + txManager.commit(); + break; + } catch (AbortedException e) { + Thread.sleep(e.getRetryDelayInMillis() / 1000); + tx = txManager.resetForRetry(); + } + } + } + } + + @Test + public void transactionManagerExecuteQueryAsync() throws Exception { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); + final AtomicInteger rowCount = new AtomicInteger(); + try (TransactionManager txManager = client.transactionManager()) { + while (true) { + TransactionContext tx = txManager.begin(); + try { + try (AsyncResultSet rs = tx.executeQueryAsync(SELECT1)) { + rs.setCallback( + executor, + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + try { + while (true) { + switch (resultSet.tryNext()) { + case OK: + rowCount.incrementAndGet(); + break; + case DONE: + return CallbackResponse.DONE; + case NOT_READY: + return CallbackResponse.CONTINUE; + } + } + } catch (Throwable t) { + return CallbackResponse.DONE; + } + } + }); + } + txManager.commit(); + break; + } catch (AbortedException e) { + Thread.sleep(e.getRetryDelayInMillis() / 1000); + tx = txManager.resetForRetry(); + } + } + } + assertThat(rowCount.get()).isEqualTo(1); + } + /** * Test that the update statement can be executed as a partitioned transaction that returns a * lower bound update count. @@ -169,7 +613,7 @@ public void testExecutePartitionedDmlAborted() { * A valid query that returns a {@link ResultSet} should not be accepted by a partitioned dml * transaction. */ - @Test(expected = IllegalArgumentException.class) + @Test(expected = SpannerException.class) public void testExecutePartitionedDmlWithQuery() { DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); @@ -185,7 +629,7 @@ public void testExecutePartitionedDmlWithException() { } @Test - public void testPartitionedDmlDoesNotTimeout() throws Exception { + public void testPartitionedDmlDoesNotTimeout() { mockSpanner.setExecuteSqlExecutionTime(SimulatedExecutionTime.ofMinimumAndRandomTime(10, 0)); final RetrySettings retrySettings = RetrySettings.newBuilder() @@ -218,7 +662,7 @@ public void testPartitionedDmlDoesNotTimeout() throws Exception { .run( new TransactionCallable() { @Override - public Void run(TransactionContext transaction) throws Exception { + public Void run(TransactionContext transaction) { transaction.executeUpdate(UPDATE_STATEMENT); return null; } @@ -233,21 +677,23 @@ public Void run(TransactionContext transaction) throws Exception { } @Test - public void testPartitionedDmlWithLowerTimeout() throws Exception { - mockSpanner.setExecuteSqlExecutionTime(SimulatedExecutionTime.ofMinimumAndRandomTime(1000, 0)); + public void testPartitionedDmlWithLowerTimeout() { + mockSpanner.setExecuteStreamingSqlExecutionTime( + SimulatedExecutionTime.ofMinimumAndRandomTime(1000, 0)); SpannerOptions.Builder builder = SpannerOptions.newBuilder() .setProjectId(TEST_PROJECT) .setChannelProvider(channelProvider) .setCredentials(NoCredentials.getInstance()); // Set PDML timeout value. - builder.setPartitionedDmlTimeout(Duration.ofMillis(100L)); + builder.setPartitionedDmlTimeout(Duration.ofMillis(10L)); try (Spanner spanner = builder.build().getService()) { DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); - assertThat(spanner.getOptions().getPartitionedDmlTimeout()) - .isEqualTo(Duration.ofMillis(100L)); + assertThat(spanner.getOptions().getPartitionedDmlTimeout()).isEqualTo(Duration.ofMillis(10L)); // PDML should timeout with these settings. + mockSpanner.setExecuteSqlExecutionTime( + SimulatedExecutionTime.ofMinimumAndRandomTime(1000, 0)); try { client.executePartitionedUpdate(UPDATE_STATEMENT); fail("expected DEADLINE_EXCEEDED"); @@ -265,7 +711,7 @@ public void testPartitionedDmlWithLowerTimeout() throws Exception { .run( new TransactionCallable() { @Override - public Long run(TransactionContext transaction) throws Exception { + public Long run(TransactionContext transaction) { return transaction.executeUpdate(UPDATE_STATEMENT); } }); @@ -274,8 +720,9 @@ public Long run(TransactionContext transaction) throws Exception { } @Test - public void testPartitionedDmlWithHigherTimeout() throws Exception { - mockSpanner.setExecuteSqlExecutionTime(SimulatedExecutionTime.ofMinimumAndRandomTime(100, 0)); + public void testPartitionedDmlWithHigherTimeout() { + mockSpanner.setExecuteStreamingSqlExecutionTime( + SimulatedExecutionTime.ofMinimumAndRandomTime(100, 0)); SpannerOptions.Builder builder = SpannerOptions.newBuilder() .setProjectId(TEST_PROJECT) @@ -307,13 +754,14 @@ public void testPartitionedDmlWithHigherTimeout() throws Exception { long updateCount = client.executePartitionedUpdate(UPDATE_STATEMENT); // Normal DML should timeout as it should use the ExecuteSQL RPC settings. + mockSpanner.setExecuteSqlExecutionTime(SimulatedExecutionTime.ofMinimumAndRandomTime(100, 0)); try { client .readWriteTransaction() .run( new TransactionCallable() { @Override - public Long run(TransactionContext transaction) throws Exception { + public Long run(TransactionContext transaction) { return transaction.executeUpdate(UPDATE_STATEMENT); } }); @@ -326,7 +774,7 @@ public Long run(TransactionContext transaction) throws Exception { } @Test - public void testPartitionedDmlRetriesOnUnavailable() throws Exception { + public void testPartitionedDmlRetriesOnUnavailable() { mockSpanner.setExecuteSqlExecutionTime( SimulatedExecutionTime.ofException(Status.UNAVAILABLE.asRuntimeException())); SpannerOptions.Builder builder = @@ -387,7 +835,7 @@ public void testDatabaseOrInstanceDoesNotExistOnPrepareSession() throws Exceptio .run( new TransactionCallable() { @Override - public Void run(TransactionContext transaction) throws Exception { + public Void run(TransactionContext transaction) { return null; } }); @@ -440,7 +888,7 @@ public void testDatabaseOrInstanceDoesNotExistOnInitialization() throws Exceptio } @Test - public void testDatabaseOrInstanceDoesNotExistOnCreate() throws Exception { + public void testDatabaseOrInstanceDoesNotExistOnCreate() { StatusRuntimeException[] exceptions = new StatusRuntimeException[] { SpannerExceptionFactoryTest.newStatusResourceNotFoundException( @@ -466,6 +914,7 @@ public void testDatabaseOrInstanceDoesNotExistOnCreate() throws Exception { DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); // The create session failure should propagate to the client and not retry. try (ResultSet rs = dbClient.singleUse().executeQuery(SELECT1)) { + rs.next(); fail("missing expected exception"); } catch (DatabaseNotFoundException | InstanceNotFoundException e) { // The server should only receive one BatchCreateSessions request. @@ -578,7 +1027,7 @@ private void testExceptionOnPrepareSession(StatusRuntimeException exception) .run( new TransactionCallable() { @Override - public Void run(TransactionContext transaction) throws Exception { + public Void run(TransactionContext transaction) { return null; } }); @@ -594,7 +1043,7 @@ public Void run(TransactionContext transaction) throws Exception { .run( new TransactionCallable() { @Override - public Void run(TransactionContext transaction) throws Exception { + public Void run(TransactionContext transaction) { return null; } }); @@ -665,7 +1114,7 @@ public void testDatabaseOrInstanceIsDeletedAndThenRecreated() throws Exception { .run( new TransactionCallable() { @Override - public Void run(TransactionContext transaction) throws Exception { + public Void run(TransactionContext transaction) { return null; } }); @@ -690,7 +1139,7 @@ public Void run(TransactionContext transaction) throws Exception { .run( new TransactionCallable() { @Override - public Void run(TransactionContext transaction) throws Exception { + public Void run(TransactionContext transaction) { return null; } }); @@ -737,7 +1186,7 @@ public void testAllowNestedTransactions() throws InterruptedException { .run( new TransactionCallable() { @Override - public Long run(TransactionContext transaction) throws Exception { + public Long run(TransactionContext transaction) { assertThat(client.pool.getNumberOfSessionsInPool()).isEqualTo(minSessions - 1); return transaction.executeUpdate(UPDATE_STATEMENT); } @@ -772,7 +1221,7 @@ public void testNestedTransactionsUsingTwoDatabases() throws InterruptedExceptio .run( new TransactionCallable() { @Override - public Long run(TransactionContext transaction) throws Exception { + public Long run(TransactionContext transaction) { // Client1 should have 1 session checked out. // Client2 should have 0 sessions checked out. assertThat(client1.pool.getNumberOfSessionsInPool()).isEqualTo(minSessions - 1); @@ -783,7 +1232,7 @@ public Long run(TransactionContext transaction) throws Exception { .run( new TransactionCallable() { @Override - public Long run(TransactionContext transaction) throws Exception { + public Long run(TransactionContext transaction) { // Both clients should now have 1 session checked out. assertThat(client1.pool.getNumberOfSessionsInPool()) .isEqualTo(minSessions - 1); @@ -929,6 +1378,54 @@ public void testBackendPartitionQueryOptions() { } } + @Test + public void testAsyncQuery() throws Exception { + final int EXPECTED_ROW_COUNT = 10; + RandomResultSetGenerator generator = new RandomResultSetGenerator(EXPECTED_ROW_COUNT); + com.google.spanner.v1.ResultSet resultSet = generator.generate(); + mockSpanner.putStatementResult( + StatementResult.query(Statement.of("SELECT * FROM RANDOM"), resultSet)); + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); + ExecutorService executor = Executors.newSingleThreadExecutor(); + ApiFuture resultSetClosed; + final SettableFuture finished = SettableFuture.create(); + final List receivedResults = new ArrayList<>(); + try (AsyncResultSet rs = + client.singleUse().executeQueryAsync(Statement.of("SELECT * FROM RANDOM"))) { + resultSetClosed = + rs.setCallback( + executor, + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + try { + while (true) { + switch (rs.tryNext()) { + case DONE: + finished.set(true); + return CallbackResponse.DONE; + case NOT_READY: + return CallbackResponse.CONTINUE; + case OK: + receivedResults.add(resultSet.getCurrentRowAsStruct()); + break; + default: + throw new IllegalStateException("Unknown cursor state"); + } + } + } catch (Throwable t) { + finished.setException(t); + return CallbackResponse.DONE; + } + } + }); + } + assertThat(finished.get()).isTrue(); + assertThat(receivedResults.size()).isEqualTo(EXPECTED_ROW_COUNT); + resultSetClosed.get(); + } + @Test public void testClientIdReusedOnDatabaseNotFound() { mockSpanner.setBatchCreateSessionsExecutionTime( @@ -960,4 +1457,30 @@ public void testClientIdReusedOnDatabaseNotFound() { } } } + + @Test + public void testBatchCreateSessionsPermissionDenied() { + mockSpanner.setBatchCreateSessionsExecutionTime( + SimulatedExecutionTime.ofStickyException( + Status.PERMISSION_DENIED.withDescription("Not permitted").asRuntimeException())); + try (Spanner spanner = + SpannerOptions.newBuilder() + .setProjectId("my-project") + .setChannelProvider(channelProvider) + .setCredentials(NoCredentials.getInstance()) + .build() + .getService()) { + DatabaseId databaseId = DatabaseId.of("my-project", "my-instance", "my-database"); + DatabaseClient client = spanner.getDatabaseClient(databaseId); + // The following call is non-blocking and will not generate an exception. + ResultSet rs = client.singleUse().executeQuery(SELECT1); + try { + // Actually trying to get any results will cause an exception. + rs.next(); + fail("missing PERMISSION_DENIED exception"); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.PERMISSION_DENIED); + } + } + } } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseIdTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseIdTest.java index a740f40424..7db3c6cef4 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseIdTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseIdTest.java @@ -17,17 +17,15 @@ package com.google.cloud.spanner; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Unit tests for {@link com.google.cloud.spanner.DatabaseId}. */ @RunWith(JUnit4.class) public class DatabaseIdTest { - @Rule public ExpectedException expectedException = ExpectedException.none(); @Test public void basics() { @@ -44,8 +42,11 @@ public void basics() { @Test public void badName() { - expectedException.expect(IllegalArgumentException.class); - expectedException.expectMessage("projects"); // Expect conforming pattern in output. - DatabaseId.of("bad name"); + try { + DatabaseId.of("bad name"); + fail("Expected exception"); + } catch (IllegalArgumentException ex) { + assertThat(ex.getMessage()).contains("projects"); + } } } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseTest.java index 14b93d7413..9557615007 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseTest.java @@ -28,9 +28,7 @@ import com.google.cloud.spanner.DatabaseInfo.State; import java.util.Arrays; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.mockito.Mock; @@ -44,7 +42,6 @@ public class DatabaseTest { private static final String NAME = "projects/test-project/instances/test-instance/databases/database-1"; - @Rule public ExpectedException expectedException = ExpectedException.none(); @Mock DatabaseAdminClient dbClient; @Before @@ -54,7 +51,7 @@ public void setUp() { .thenAnswer( new Answer() { @Override - public Backup.Builder answer(InvocationOnMock invocation) throws Throwable { + public Backup.Builder answer(InvocationOnMock invocation) { return new Backup.Builder(dbClient, (BackupId) invocation.getArguments()[0]); } }); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/GrpcResultSetTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/GrpcResultSetTest.java index da3e81a9bb..2ee73e75d3 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/GrpcResultSetTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/GrpcResultSetTest.java @@ -16,9 +16,9 @@ package com.google.cloud.spanner; -import static com.google.cloud.spanner.SpannerMatchers.isSpannerException; import static com.google.common.testing.SerializableTester.reserialize; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; import com.google.cloud.ByteArray; import com.google.cloud.Date; @@ -40,16 +40,13 @@ import java.util.Map; import javax.annotation.Nullable; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Unit tests for {@link com.google.cloud.spanner.SpannerImpl.GrpcResultSet}. */ @RunWith(JUnit4.class) public class GrpcResultSetTest { - @Rule public ExpectedException expectedException = ExpectedException.none(); private AbstractResultSet.GrpcResultSet resultSet; private SpannerRpc.ResultStreamConsumer consumer; @@ -107,16 +104,24 @@ public void metadataFailure() { SpannerException t = SpannerExceptionFactory.newSpannerException(ErrorCode.DEADLINE_EXCEEDED, "outatime"); consumer.onError(t); - expectedException.expect(isSpannerException(ErrorCode.DEADLINE_EXCEEDED)); - expectedException.expectMessage("outatime"); - resultSet.next(); + try { + resultSet.next(); + fail("Expected exception"); + } catch (SpannerException ex) { + assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.DEADLINE_EXCEEDED); + assertThat(ex.getMessage()).contains("outatime"); + } } @Test public void noMetadata() { consumer.onCompleted(); - expectedException.expect(isSpannerException(ErrorCode.INTERNAL)); - resultSet.next(); + try { + resultSet.next(); + fail("Expected exception"); + } catch (SpannerException ex) { + assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.INTERNAL); + } } @Test @@ -195,8 +200,12 @@ public void multiResponseChunkingStreamClosed() { .setChunkedValue(true) .build()); consumer.onCompleted(); - expectedException.expect(isSpannerException(ErrorCode.INTERNAL)); - resultSet.next(); + try { + resultSet.next(); + fail("Expected exception"); + } catch (SpannerException ex) { + assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.INTERNAL); + } } @Test @@ -555,7 +564,7 @@ private static ResultSetMetadata makeMetadata(Type rowType) { } @Test - public void serialization() throws Exception { + public void serialization() { Type structType = Type.struct( Arrays.asList( @@ -596,7 +605,7 @@ public void serialization() throws Exception { } @Test - public void nestedStructSerialization() throws Exception { + public void nestedStructSerialization() { Type structType = Type.struct( Arrays.asList( diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InstanceAdminGaxTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InstanceAdminGaxTest.java index 66ae5ab0bd..5f3063bc7b 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InstanceAdminGaxTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InstanceAdminGaxTest.java @@ -224,12 +224,12 @@ public void setUp() throws Exception { RetrySettings retrySettingsWithLowTimeout = RetrySettings.newBuilder() .setInitialRetryDelay(Duration.ofMillis(1L)) - .setMaxRetryDelay(Duration.ofMillis(1L)) + .setMaxRetryDelay(Duration.ofMillis(1000L)) .setInitialRpcTimeout(Duration.ofMillis(20L)) .setMaxRpcTimeout(Duration.ofMillis(200L)) - .setRetryDelayMultiplier(1.3) + .setRetryDelayMultiplier(1000.0d) .setMaxAttempts(10) - .setTotalTimeout(Duration.ofMillis(200L)) + .setTotalTimeout(Duration.ofMillis(20000L)) .build(); RetrySettings retrySettingsWithHighTimeout = RetrySettings.newBuilder() @@ -300,7 +300,7 @@ public Void apply(Builder input) { } @After - public void tearDown() throws Exception { + public void tearDown() { spanner.close(); } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InstanceConfigIdTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InstanceConfigIdTest.java index 3266333d37..4110cbe676 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InstanceConfigIdTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InstanceConfigIdTest.java @@ -17,17 +17,16 @@ package com.google.cloud.spanner; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Unit tests for {@link InstanceConfigId}. */ @RunWith(JUnit4.class) public class InstanceConfigIdTest { - @Rule public ExpectedException expectedException = ExpectedException.none(); @Test public void basic() { @@ -43,7 +42,11 @@ public void basic() { @Test public void badName() { - expectedException.expect(IllegalArgumentException.class); - InstanceConfigId.of("bad name"); + try { + InstanceConfigId.of("bad name"); + fail("Expected exception"); + } catch (IllegalArgumentException e) { + assertNotNull(e.getMessage()); + } } } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InstanceIdTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InstanceIdTest.java index 7d6ff2a2fe..2e921257f9 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InstanceIdTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/InstanceIdTest.java @@ -17,17 +17,16 @@ package com.google.cloud.spanner; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertNotNull; -import org.junit.Rule; +import org.junit.Assert; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Unit tests for {@link InstanceId}. */ @RunWith(JUnit4.class) public class InstanceIdTest { - @Rule public ExpectedException expectedException = ExpectedException.none(); @Test public void basic() { @@ -43,7 +42,11 @@ public void basic() { @Test public void badName() { - expectedException.expect(IllegalArgumentException.class); - InstanceId.of("bad name"); + try { + InstanceId.of("bad name"); + Assert.fail("Expected exception"); + } catch (IllegalArgumentException e) { + assertNotNull(e.getMessage()); + } } } 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 f643c6021e..b67f970273 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 @@ -162,17 +162,22 @@ private void initializeInstance(InstanceId instanceId) { } private void cleanUpInstance() { - if (isOwnedInstance) { - // Delete the instance, which implicitly drops all databases in it. - try { - logger.log(Level.FINE, "Deleting test instance {0}", testHelper.getInstanceId()); - instanceAdminClient.deleteInstance(testHelper.getInstanceId().getInstance()); - logger.log(Level.INFO, "Deleted test instance {0}", testHelper.getInstanceId()); - } catch (SpannerException e) { - logger.log(Level.SEVERE, "Failed to delete test instance " + testHelper.getInstanceId(), e); + try { + if (isOwnedInstance) { + // Delete the instance, which implicitly drops all databases in it. + try { + logger.log(Level.FINE, "Deleting test instance {0}", testHelper.getInstanceId()); + instanceAdminClient.deleteInstance(testHelper.getInstanceId().getInstance()); + logger.log(Level.INFO, "Deleted test instance {0}", testHelper.getInstanceId()); + } catch (SpannerException e) { + logger.log( + Level.SEVERE, "Failed to delete test instance " + testHelper.getInstanceId(), e); + } + } else { + testHelper.cleanUp(); } - } else { - testHelper.cleanUp(); + } finally { + testHelper.getClient().close(); } } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/IntegrationTestWithClosedSessionsEnv.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/IntegrationTestWithClosedSessionsEnv.java index 6b22ba77c3..edbc7976c0 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/IntegrationTestWithClosedSessionsEnv.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/IntegrationTestWithClosedSessionsEnv.java @@ -17,6 +17,7 @@ package com.google.cloud.spanner; import com.google.cloud.spanner.SessionPool.PooledSession; +import com.google.cloud.spanner.SessionPool.PooledSessionFuture; import com.google.cloud.spanner.testing.RemoteSpannerHelper; /** @@ -73,30 +74,30 @@ public void setAllowSessionReplacing(boolean allow) { } @Override - PooledSession getReadSession() { - PooledSession session = super.getReadSession(); + PooledSessionFuture getReadSession() { + PooledSessionFuture session = super.getReadSession(); if (invalidateNextSession) { - session.delegate.close(); - session.setAllowReplacing(false); - awaitDeleted(session.delegate); - session.setAllowReplacing(allowReplacing); + session.get().delegate.close(); + session.get().setAllowReplacing(false); + awaitDeleted(session.get().delegate); + session.get().setAllowReplacing(allowReplacing); invalidateNextSession = false; } - session.setAllowReplacing(allowReplacing); + session.get().setAllowReplacing(allowReplacing); return session; } @Override - PooledSession getReadWriteSession() { - PooledSession session = super.getReadWriteSession(); + PooledSessionFuture getReadWriteSession() { + PooledSessionFuture session = super.getReadWriteSession(); if (invalidateNextSession) { - session.delegate.close(); - session.setAllowReplacing(false); - awaitDeleted(session.delegate); - session.setAllowReplacing(allowReplacing); + session.get().delegate.close(); + session.get().setAllowReplacing(false); + awaitDeleted(session.get().delegate); + session.get().setAllowReplacing(allowReplacing); invalidateNextSession = false; } - session.setAllowReplacing(allowReplacing); + session.get().setAllowReplacing(allowReplacing); return session; } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/KeyRangeTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/KeyRangeTest.java index ba8c8eeed5..c89d08ef26 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/KeyRangeTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/KeyRangeTest.java @@ -20,18 +20,16 @@ import static com.google.cloud.spanner.KeyRange.Endpoint.OPEN; import static com.google.common.testing.SerializableTester.reserializeAndAssert; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; import com.google.common.testing.EqualsTester; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Unit tests for {@link com.google.cloud.spanner.KeyRange}. */ @RunWith(JUnit4.class) public class KeyRangeTest { - @Rule public ExpectedException expectedException = ExpectedException.none(); @Test public void basics() { @@ -104,16 +102,22 @@ public void toBuilder() { @Test public void builderRequiresStart() { - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("start(Key)"); - KeyRange.newBuilder().setEnd(Key.of("z")).build(); + try { + KeyRange.newBuilder().setEnd(Key.of("z")).build(); + fail("Expected exception"); + } catch (IllegalStateException ex) { + assertThat(ex.getMessage()).contains("start(Key)"); + } } @Test public void builderRequiresEnd() { - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("end(Key)"); - KeyRange.newBuilder().setStart(Key.of("a")).build(); + try { + KeyRange.newBuilder().setStart(Key.of("a")).build(); + fail("Expected exception"); + } catch (IllegalStateException ex) { + assertThat(ex.getMessage()).contains("end(Key)"); + } } @Test @@ -126,7 +130,7 @@ public void testToString() { } @Test - public void serialization() throws Exception { + public void serialization() { reserializeAndAssert(KeyRange.closedOpen(Key.of(1), Key.of(2))); reserializeAndAssert(KeyRange.closedClosed(Key.of(1), Key.of(2))); reserializeAndAssert(KeyRange.openOpen(Key.of(1), Key.of(2))); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/KeySetTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/KeySetTest.java index 2712c84613..7ed74283f3 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/KeySetTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/KeySetTest.java @@ -264,7 +264,7 @@ public void serializationMultiWithAll() { } @Test - public void javaSerialization() throws Exception { + public void javaSerialization() { reserializeAndAssert( KeySet.all() .toBuilder() diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/KeyTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/KeyTest.java index 81f3957c28..a135e30888 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/KeyTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/KeyTest.java @@ -179,7 +179,7 @@ public void equalsAndHashCode() { } @Test - public void serialization() throws Exception { + public void serialization() { reserializeAndAssert(Key.of()); reserializeAndAssert(Key.of(new Object[] {null})); reserializeAndAssert(Key.of(true)); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockDatabaseAdminServiceImpl.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockDatabaseAdminServiceImpl.java index ff0e7b482f..25e162039b 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockDatabaseAdminServiceImpl.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockDatabaseAdminServiceImpl.java @@ -79,8 +79,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.locks.ReadWriteLock; -import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.concurrent.CountDownLatch; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -201,7 +200,7 @@ private CreateDatabaseCallable(String operationName, String name) { } @Override - public Database call() throws Exception { + public Database call() { MockDatabase db = databases.get(name); db.state = State.READY; Database proto = db.toProto(); @@ -434,7 +433,7 @@ private com.google.rpc.Status fromException(Exception e) { private static final String EXPIRE_TIME_MASK = "expire_time"; private static final Random RND = new Random(); private final Queue exceptions = new ConcurrentLinkedQueue<>(); - private final ReadWriteLock freezeLock = new ReentrantReadWriteLock(); + private volatile CountDownLatch freezeLock = new CountDownLatch(0); private final ConcurrentMap databases = new ConcurrentHashMap<>(); private final ConcurrentMap backups = new ConcurrentHashMap<>(); private final ConcurrentMap> filterMatches = new ConcurrentHashMap<>(); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockDatabaseAdminServiceImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockDatabaseAdminServiceImplTest.java index 4364bfe26a..c73b79d6e6 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockDatabaseAdminServiceImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockDatabaseAdminServiceImplTest.java @@ -193,7 +193,7 @@ public void setUp() throws IOException { } @After - public void tearDown() throws Exception { + public void tearDown() { client.close(); } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java index 069242d5a7..cae41510fc 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java @@ -19,9 +19,11 @@ import com.google.api.gax.grpc.testing.MockGrpcService; import com.google.cloud.ByteArray; import com.google.cloud.Date; +import com.google.cloud.spanner.AbstractResultSet.GrpcStruct; import com.google.cloud.spanner.TransactionRunnerImpl.TransactionContextImpl; import com.google.common.base.Optional; import com.google.common.base.Preconditions; +import com.google.common.base.Stopwatch; import com.google.common.base.Throwables; import com.google.common.util.concurrent.Uninterruptibles; import com.google.protobuf.AbstractMessage; @@ -79,6 +81,7 @@ import java.util.Collection; import java.util.Collections; import java.util.Comparator; +import java.util.Deque; import java.util.Iterator; import java.util.LinkedList; import java.util.List; @@ -88,14 +91,15 @@ import java.util.Random; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.locks.ReadWriteLock; -import java.util.concurrent.locks.ReentrantReadWriteLock; import org.threeten.bp.Instant; /** @@ -232,7 +236,7 @@ private enum StatementResultType { private final StatementResultType type; private final Statement statement; private final Long updateCount; - private final ResultSet resultSet; + private final Deque resultSets; private final StatusRuntimeException exception; /** Creates a {@link StatementResult} for a query that returns a {@link ResultSet}. */ @@ -240,6 +244,15 @@ public static StatementResult query(Statement statement, ResultSet resultSet) { return new StatementResult(statement, resultSet); } + /** + * Creates a {@link StatementResult} for a query that returns a {@link ResultSet} the first + * time, and a different {@link ResultSet} for all subsequent calls. + */ + public static StatementResult queryAndThen( + Statement statement, ResultSet resultSet, ResultSet next) { + return new StatementResult(statement, resultSet); + } + /** Creates a {@link StatementResult} for a read request. */ public static StatementResult read( String table, KeySet keySet, Iterable columns, ResultSet resultSet) { @@ -256,6 +269,25 @@ public static StatementResult exception(Statement statement, StatusRuntimeExcept return new StatementResult(statement, exception); } + private static class KeepLastElementDeque extends LinkedList { + private static KeepLastElementDeque singleton(E item) { + return new KeepLastElementDeque(Collections.singleton(item)); + } + + private static KeepLastElementDeque of(E first, E second) { + return new KeepLastElementDeque(Arrays.asList(first, second)); + } + + private KeepLastElementDeque(Collection coll) { + super(coll); + } + + @Override + public E pop() { + return this.size() == 1 ? super.peek() : super.pop(); + } + } + /** * Creates a {@link Statement} for a read statement. This {@link Statement} can be used to mock * a result for a read request. @@ -275,6 +307,7 @@ public static Statement createReadStatement( builder.append(", "); } builder.append(col); + first = false; } builder.append(" FROM ").append(table); if (keySet.isAll()) { @@ -302,14 +335,24 @@ private static boolean isValidKeySet(KeySet keySet) { private StatementResult(Statement statement, Long updateCount) { this.statement = Preconditions.checkNotNull(statement); this.updateCount = Preconditions.checkNotNull(updateCount); - this.resultSet = null; + this.resultSets = null; this.exception = null; this.type = StatementResultType.UPDATE_COUNT; } private StatementResult(Statement statement, ResultSet resultSet) { this.statement = Preconditions.checkNotNull(statement); - this.resultSet = Preconditions.checkNotNull(resultSet); + this.resultSets = KeepLastElementDeque.singleton(Preconditions.checkNotNull(resultSet)); + this.updateCount = null; + this.exception = null; + this.type = StatementResultType.RESULT_SET; + } + + private StatementResult(Statement statement, ResultSet resultSet, ResultSet andThen) { + this.statement = Preconditions.checkNotNull(statement); + this.resultSets = + KeepLastElementDeque.of( + Preconditions.checkNotNull(resultSet), Preconditions.checkNotNull(andThen)); this.updateCount = null; this.exception = null; this.type = StatementResultType.RESULT_SET; @@ -318,7 +361,7 @@ private StatementResult(Statement statement, ResultSet resultSet) { private StatementResult( String table, KeySet keySet, Iterable columns, ResultSet resultSet) { this.statement = createReadStatement(table, keySet, columns); - this.resultSet = Preconditions.checkNotNull(resultSet); + this.resultSets = KeepLastElementDeque.singleton(Preconditions.checkNotNull(resultSet)); this.updateCount = null; this.exception = null; this.type = StatementResultType.RESULT_SET; @@ -327,7 +370,7 @@ private StatementResult( private StatementResult(Statement statement, StatusRuntimeException exception) { this.statement = Preconditions.checkNotNull(statement); this.exception = Preconditions.checkNotNull(exception); - this.resultSet = null; + this.resultSets = null; this.updateCount = null; this.type = StatementResultType.EXCEPTION; } @@ -340,7 +383,7 @@ private ResultSet getResultSet() { Preconditions.checkState( type == StatementResultType.RESULT_SET, "This statement result does not contain a result set"); - return resultSet; + return resultSets.pop(); } private Long getUpdateCount() { @@ -394,6 +437,11 @@ public static SimulatedExecutionTime ofStickyException(Exception exception) { return new SimulatedExecutionTime(0, 0, Arrays.asList(exception), true); } + public static SimulatedExecutionTime stickyDatabaseNotFoundException(String name) { + return ofStickyException( + SpannerExceptionFactoryTest.newStatusDatabaseNotFoundException(name)); + } + public static SimulatedExecutionTime ofExceptions(Collection exceptions) { return new SimulatedExecutionTime(0, 0, exceptions, false); } @@ -421,19 +469,15 @@ private SimulatedExecutionTime( void simulateExecutionTime( Queue globalExceptions, boolean stickyGlobalExceptions, - ReadWriteLock freezeLock) { - try { - freezeLock.readLock().lock(); - checkException(globalExceptions, stickyGlobalExceptions); - checkException(this.exceptions, stickyException); - if (minimumExecutionTime > 0 || randomExecutionTime > 0) { - Uninterruptibles.sleepUninterruptibly( - (randomExecutionTime == 0 ? 0 : RANDOM.nextInt(randomExecutionTime)) - + minimumExecutionTime, - TimeUnit.MILLISECONDS); - } - } finally { - freezeLock.readLock().unlock(); + CountDownLatch freezeLock) { + Uninterruptibles.awaitUninterruptibly(freezeLock); + checkException(globalExceptions, stickyGlobalExceptions); + checkException(this.exceptions, stickyException); + if (minimumExecutionTime > 0 || randomExecutionTime > 0) { + Uninterruptibles.sleepUninterruptibly( + (randomExecutionTime == 0 ? 0 : RANDOM.nextInt(randomExecutionTime)) + + minimumExecutionTime, + TimeUnit.MILLISECONDS); } } @@ -451,22 +495,23 @@ private static void checkException(Queue exceptions, boolean keepExce private final Random random = new Random(); private double abortProbability = 0.0010D; - private final Queue requests = new ConcurrentLinkedQueue<>(); - private final ReadWriteLock freezeLock = new ReentrantReadWriteLock(); - private final Queue exceptions = new ConcurrentLinkedQueue<>(); + private final Object lock = new Object(); + private Deque requests = new ConcurrentLinkedDeque<>(); + private volatile CountDownLatch freezeLock = new CountDownLatch(0); + private Queue exceptions = new ConcurrentLinkedQueue<>(); private boolean stickyGlobalExceptions = false; - private final ConcurrentMap statementResults = - new ConcurrentHashMap<>(); - private final ConcurrentMap sessions = new ConcurrentHashMap<>(); + private ConcurrentMap statementResults = new ConcurrentHashMap<>(); + private ConcurrentMap statementGetCounts = new ConcurrentHashMap<>(); + private ConcurrentMap sessions = new ConcurrentHashMap<>(); private ConcurrentMap sessionLastUsed = new ConcurrentHashMap<>(); - private final ConcurrentMap transactions = new ConcurrentHashMap<>(); - private final ConcurrentMap isPartitionedDmlTransaction = + private ConcurrentMap transactions = new ConcurrentHashMap<>(); + private ConcurrentMap isPartitionedDmlTransaction = new ConcurrentHashMap<>(); - private final ConcurrentMap abortedTransactions = new ConcurrentHashMap<>(); + private ConcurrentMap abortedTransactions = new ConcurrentHashMap<>(); private final AtomicBoolean abortNextTransaction = new AtomicBoolean(); private final AtomicBoolean abortNextStatement = new AtomicBoolean(); - private final ConcurrentMap transactionCounters = new ConcurrentHashMap<>(); - private final ConcurrentMap> partitionTokens = new ConcurrentHashMap<>(); + private ConcurrentMap transactionCounters = new ConcurrentHashMap<>(); + private ConcurrentMap> partitionTokens = new ConcurrentHashMap<>(); private ConcurrentMap transactionLastUsed = new ConcurrentHashMap<>(); private int maxNumSessionsInOneBatch = 100; private int maxTotalSessions = Integer.MAX_VALUE; @@ -532,11 +577,29 @@ private Timestamp getCurrentGoogleTimestamp() { */ public void putStatementResult(StatementResult result) { Preconditions.checkNotNull(result); - statementResults.put(result.statement, result); + synchronized (lock) { + statementResults.put(result.statement, result); + } + } + + public void putStatementResults(StatementResult... results) { + synchronized (lock) { + for (StatementResult result : results) { + statementResults.put(result.statement, result); + } + } } private StatementResult getResult(Statement statement) { - StatementResult res = statementResults.get(statement); + StatementResult res; + synchronized (lock) { + res = statementResults.get(statement); + if (statementGetCounts.containsKey(statement)) { + statementGetCounts.put(statement, statementGetCounts.get(statement) + 1L); + } else { + statementGetCounts.put(statement, 1L); + } + } if (res == null) { throw Status.INTERNAL .withDescription( @@ -593,11 +656,11 @@ public void abortAllTransactions() { } public void freeze() { - freezeLock.writeLock().lock(); + freezeLock = new CountDownLatch(1); } public void unfreeze() { - freezeLock.writeLock().unlock(); + freezeLock.countDown(); } public void setMaxSessionsInOneBatch(int max) { @@ -935,6 +998,7 @@ public void executeBatchDml( status = com.google.rpc.Status.newBuilder() .setCode(res.getException().getStatus().getCode().value()) + .setMessage(res.getException().getMessage()) .build(); break resultLoop; case RESULT_SET: @@ -994,10 +1058,27 @@ public void executeStreamingSql( } sessionLastUsed.put(session.getName(), Instant.now()); try { + Statement statement = + buildStatement(request.getSql(), request.getParamTypesMap(), request.getParams()); + ByteString transactionId = getTransactionId(session, request.getTransaction()); + boolean isPartitioned = isPartitionedDmlTransaction(transactionId); + if (isPartitioned) { + StatementResult firstRes = getResult(statement); + switch (firstRes.getType()) { + case EXCEPTION: + throw firstRes.getException(); + case UPDATE_COUNT: + returnPartialResultSet( + session, 0L, !isPartitioned, responseObserver, request.getTransaction(), false); + break; + case RESULT_SET: + default: + break; + } + } executeStreamingSqlExecutionTime.simulateExecutionTime( exceptions, stickyGlobalExceptions, freezeLock); // Get or start transaction - ByteString transactionId = getTransactionId(session, request.getTransaction()); if (!request.getPartitionToken().isEmpty()) { List tokens = partitionTokens.get(partitionKey(session.getName(), transactionId)); @@ -1009,8 +1090,6 @@ public void executeStreamingSql( } } simulateAbort(session, transactionId); - Statement statement = - buildStatement(request.getSql(), request.getParamTypesMap(), request.getParams()); StatementResult res = getResult(statement); switch (res.getType()) { case EXCEPTION: @@ -1020,7 +1099,6 @@ public void executeStreamingSql( res.getResultSet(), transactionId, request.getTransaction(), responseObserver); break; case UPDATE_COUNT: - boolean isPartitioned = isPartitionedDmlTransaction(transactionId); if (isPartitioned) { commitTransaction(transactionId); } @@ -1041,6 +1119,7 @@ public void executeStreamingSql( } } + @SuppressWarnings("unchecked") private Statement buildStatement( String sql, Map paramTypes, com.google.protobuf.Struct params) { Statement.Builder builder = Statement.newBuilder(sql); @@ -1049,7 +1128,37 @@ private Statement buildStatement( if (value.getKindCase() == KindCase.NULL_VALUE) { switch (entry.getValue().getCode()) { case ARRAY: - throw new IllegalArgumentException("Array parameters not (yet) supported"); + switch (entry.getValue().getArrayElementType().getCode()) { + case BOOL: + builder.bind(entry.getKey()).toBoolArray((Iterable) null); + break; + case BYTES: + builder.bind(entry.getKey()).toBytesArray(null); + break; + case DATE: + builder.bind(entry.getKey()).toDateArray(null); + break; + case FLOAT64: + builder.bind(entry.getKey()).toFloat64Array((Iterable) null); + break; + case INT64: + builder.bind(entry.getKey()).toInt64Array((Iterable) null); + break; + case STRING: + builder.bind(entry.getKey()).toStringArray(null); + break; + case TIMESTAMP: + builder.bind(entry.getKey()).toTimestampArray(null); + break; + case STRUCT: + case TYPE_CODE_UNSPECIFIED: + case UNRECOGNIZED: + default: + throw new IllegalArgumentException( + "Unknown or invalid array parameter type: " + + entry.getValue().getArrayElementType().getCode()); + } + break; case BOOL: builder.bind(entry.getKey()).to((Boolean) null); break; @@ -1083,7 +1192,72 @@ private Statement buildStatement( } else { switch (entry.getValue().getCode()) { case ARRAY: - throw new IllegalArgumentException("Array parameters not (yet) supported"); + switch (entry.getValue().getArrayElementType().getCode()) { + case BOOL: + builder + .bind(entry.getKey()) + .toBoolArray( + (Iterable) + GrpcStruct.decodeArrayValue( + com.google.cloud.spanner.Type.bool(), value.getListValue())); + break; + case BYTES: + builder + .bind(entry.getKey()) + .toBytesArray( + (Iterable) + GrpcStruct.decodeArrayValue( + com.google.cloud.spanner.Type.bytes(), value.getListValue())); + break; + case DATE: + builder + .bind(entry.getKey()) + .toDateArray( + (Iterable) + GrpcStruct.decodeArrayValue( + com.google.cloud.spanner.Type.date(), value.getListValue())); + break; + case FLOAT64: + builder + .bind(entry.getKey()) + .toFloat64Array( + (Iterable) + GrpcStruct.decodeArrayValue( + com.google.cloud.spanner.Type.float64(), value.getListValue())); + break; + case INT64: + builder + .bind(entry.getKey()) + .toInt64Array( + (Iterable) + GrpcStruct.decodeArrayValue( + com.google.cloud.spanner.Type.int64(), value.getListValue())); + break; + case STRING: + builder + .bind(entry.getKey()) + .toStringArray( + (Iterable) + GrpcStruct.decodeArrayValue( + com.google.cloud.spanner.Type.string(), value.getListValue())); + break; + case TIMESTAMP: + builder + .bind(entry.getKey()) + .toTimestampArray( + (Iterable) + GrpcStruct.decodeArrayValue( + com.google.cloud.spanner.Type.timestamp(), value.getListValue())); + break; + case STRUCT: + case TYPE_CODE_UNSPECIFIED: + case UNRECOGNIZED: + default: + throw new IllegalArgumentException( + "Unknown or invalid array parameter type: " + + entry.getValue().getArrayElementType().getCode()); + } + break; case BOOL: builder.bind(entry.getKey()).to(value.getBoolValue()); break; @@ -1105,6 +1279,9 @@ private Statement buildStatement( case STRUCT: throw new IllegalArgumentException("Struct parameters not (yet) supported"); case TIMESTAMP: + builder + .bind(entry.getKey()) + .to(com.google.cloud.Timestamp.parseTimestamp(value.getStringValue())); break; case TYPE_CODE_UNSPECIFIED: case UNRECOGNIZED: @@ -1186,12 +1363,12 @@ public Iterator iterator() { return request.getColumnsList().iterator(); } }; - StatementResult res = - statementResults.get( - StatementResult.createReadStatement( - request.getTable(), - request.getKeySet().getAll() ? KeySet.all() : KeySet.singleKey(Key.of()), - cols)); + Statement statement = + StatementResult.createReadStatement( + request.getTable(), + request.getKeySet().getAll() ? KeySet.all() : KeySet.singleKey(Key.of()), + cols); + StatementResult res = getResult(statement); returnResultSet( res.getResultSet(), transactionId, request.getTransaction(), responseObserver); responseObserver.onCompleted(); @@ -1236,12 +1413,17 @@ public Iterator iterator() { return request.getColumnsList().iterator(); } }; - StatementResult res = - statementResults.get( - StatementResult.createReadStatement( - request.getTable(), - request.getKeySet().getAll() ? KeySet.all() : KeySet.singleKey(Key.of()), - cols)); + Statement statement = + StatementResult.createReadStatement( + request.getTable(), + request.getKeySet().getAll() ? KeySet.all() : KeySet.singleKey(Key.of()), + cols); + StatementResult res = getResult(statement); + if (res == null) { + throw Status.NOT_FOUND + .withDescription("No result found for " + statement.toString()) + .asRuntimeException(); + } returnPartialResultSet( res.getResultSet(), transactionId, request.getTransaction(), responseObserver); } catch (StatusRuntimeException e) { @@ -1281,6 +1463,16 @@ private void returnPartialResultSet( boolean exact, StreamObserver responseObserver, TransactionSelector transaction) { + returnPartialResultSet(session, updateCount, exact, responseObserver, transaction, true); + } + + private void returnPartialResultSet( + Session session, + Long updateCount, + boolean exact, + StreamObserver responseObserver, + TransactionSelector transaction, + boolean complete) { Field field = Field.newBuilder() .setName("UPDATE_COUNT") @@ -1307,7 +1499,9 @@ private void returnPartialResultSet( .setStats(ResultSetStats.newBuilder().setRowCountLowerBound(updateCount).build()) .build()); } - responseObserver.onCompleted(); + if (complete) { + responseObserver.onCompleted(); + } } private boolean isPartitionedDmlTransaction(ByteString transactionId) { @@ -1470,7 +1664,10 @@ private void ensureMostRecentTransaction(Session session, ByteString transaction throw Status.FAILED_PRECONDITION .withDescription( String.format( - "This transaction has been invalidated by a later transaction in the same session.", + "This transaction has been invalidated by a later transaction in the same session.\nTransaction id: " + + id + + "\nExpected: " + + counter.get(), session.getName())) .asRuntimeException(); } @@ -1636,6 +1833,37 @@ public List getRequests() { return new ArrayList<>(this.requests); } + public Iterable> getRequestTypes() { + List> res = new LinkedList<>(); + for (AbstractMessage m : this.requests) { + res.add(m.getClass()); + } + return res; + } + + public int countRequestsOfType(Class type) { + int c = 0; + for (AbstractMessage m : this.requests) { + if (m.getClass().equals(type)) { + c++; + } + } + return c; + } + + public void waitForLastRequestToBe(Class type, long timeoutMillis) + throws InterruptedException, TimeoutException { + Stopwatch watch = Stopwatch.createStarted(); + while (!(this.requests.peekLast() != null + && this.requests.peekLast().getClass().equals(type))) { + Thread.sleep(10L); + if (watch.elapsed(TimeUnit.MILLISECONDS) > timeoutMillis) { + throw new TimeoutException( + "Timeout while waiting for last request to become " + type.getName()); + } + } + } + @Override public void addResponse(AbstractMessage response) { throw new UnsupportedOperationException(); @@ -1646,6 +1874,10 @@ public void addException(Exception exception) { exceptions.add(exception); } + public void clearExceptions() { + exceptions.clear(); + } + public void setStickyGlobalExceptions(boolean sticky) { this.stickyGlobalExceptions = sticky; } @@ -1658,18 +1890,21 @@ public ServerServiceDefinition getServiceDefinition() { /** Removes all sessions and transactions. Mocked results are not removed. */ @Override public void reset() { - requests.clear(); - sessions.clear(); + requests = new ConcurrentLinkedDeque<>(); + exceptions = new ConcurrentLinkedQueue<>(); + statementGetCounts = new ConcurrentHashMap<>(); + sessions = new ConcurrentHashMap<>(); + sessionLastUsed = new ConcurrentHashMap<>(); + transactions = new ConcurrentHashMap<>(); + isPartitionedDmlTransaction = new ConcurrentHashMap<>(); + abortedTransactions = new ConcurrentHashMap<>(); + transactionCounters = new ConcurrentHashMap<>(); + partitionTokens = new ConcurrentHashMap<>(); + transactionLastUsed = new ConcurrentHashMap<>(); + numSessionsCreated.set(0); - sessionLastUsed.clear(); - transactions.clear(); - isPartitionedDmlTransaction.clear(); - abortedTransactions.clear(); - transactionCounters.clear(); - partitionTokens.clear(); - transactionLastUsed.clear(); - exceptions.clear(); stickyGlobalExceptions = false; + freezeLock.countDown(); } public void removeAllExecutionTimes() { diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerTestUtil.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerTestUtil.java new file mode 100644 index 0000000000..cc6784b679 --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerTestUtil.java @@ -0,0 +1,151 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.spanner; + +import com.google.cloud.spanner.Type.StructField; +import com.google.common.collect.ContiguousSet; +import com.google.protobuf.ListValue; +import com.google.spanner.v1.ResultSetMetadata; +import com.google.spanner.v1.StructType; +import com.google.spanner.v1.StructType.Field; +import com.google.spanner.v1.TypeCode; +import java.util.Arrays; + +public class MockSpannerTestUtil { + static final Statement SELECT1 = Statement.of("SELECT 1 AS COL1"); + private static final ResultSetMetadata SELECT1_METADATA = + ResultSetMetadata.newBuilder() + .setRowType( + StructType.newBuilder() + .addFields( + Field.newBuilder() + .setName("COL1") + .setType( + com.google.spanner.v1.Type.newBuilder() + .setCode(TypeCode.INT64) + .build()) + .build()) + .build()) + .build(); + static final com.google.spanner.v1.ResultSet SELECT1_RESULTSET = + com.google.spanner.v1.ResultSet.newBuilder() + .addRows( + ListValue.newBuilder() + .addValues(com.google.protobuf.Value.newBuilder().setStringValue("1").build()) + .build()) + .setMetadata(SELECT1_METADATA) + .build(); + + static final String TEST_PROJECT = "my-project"; + static final String TEST_INSTANCE = "my-instance"; + static final String TEST_DATABASE = "my-database"; + + static final Statement UPDATE_STATEMENT = Statement.of("UPDATE FOO SET BAR=1 WHERE BAZ=2"); + static final Statement INVALID_UPDATE_STATEMENT = + Statement.of("UPDATE NON_EXISTENT_TABLE SET BAR=1 WHERE BAZ=2"); + static final Statement UPDATE_ABORTED_STATEMENT = + Statement.of("UPDATE FOO SET BAR=1 WHERE BAZ=2 AND THIS_WILL_ABORT=TRUE"); + static final long UPDATE_COUNT = 1L; + + static final String READ_TABLE_NAME = "TestTable"; + static final String EMPTY_READ_TABLE_NAME = "EmptyTestTable"; + static final Iterable READ_COLUMN_NAMES = Arrays.asList("Key", "Value"); + static final Statement READ_ONE_KEY_VALUE_STATEMENT = + Statement.of("SELECT Key, Value FROM TestTable WHERE ID=1"); + static final Statement READ_MULTIPLE_KEY_VALUE_STATEMENT = + Statement.of("SELECT Key, Value FROM TestTable WHERE 1=1"); + static final Statement READ_ONE_EMPTY_KEY_VALUE_STATEMENT = + Statement.of("SELECT Key, Value FROM EmptyTestTable WHERE ID=1"); + static final Statement READ_ALL_EMPTY_KEY_VALUE_STATEMENT = + Statement.of("SELECT Key, Value FROM EmptyTestTable WHERE 1=1"); + static final ResultSetMetadata READ_KEY_VALUE_METADATA = + ResultSetMetadata.newBuilder() + .setRowType( + StructType.newBuilder() + .addFields( + Field.newBuilder() + .setName("Key") + .setType( + com.google.spanner.v1.Type.newBuilder() + .setCode(TypeCode.STRING) + .build()) + .build()) + .addFields( + Field.newBuilder() + .setName("Value") + .setType( + com.google.spanner.v1.Type.newBuilder() + .setCode(TypeCode.STRING) + .build()) + .build()) + .build()) + .build(); + static final Type READ_TABLE_TYPE = + Type.struct(StructField.of("Key", Type.string()), StructField.of("Value", Type.string())); + static final com.google.spanner.v1.ResultSet EMPTY_KEY_VALUE_RESULTSET = + com.google.spanner.v1.ResultSet.newBuilder() + .addRows(ListValue.newBuilder().build()) + .setMetadata(READ_KEY_VALUE_METADATA) + .build(); + static final com.google.spanner.v1.ResultSet READ_ONE_KEY_VALUE_RESULTSET = + com.google.spanner.v1.ResultSet.newBuilder() + .addRows( + ListValue.newBuilder() + .addValues(com.google.protobuf.Value.newBuilder().setStringValue("k1").build()) + .addValues(com.google.protobuf.Value.newBuilder().setStringValue("v1").build()) + .build()) + .setMetadata(READ_KEY_VALUE_METADATA) + .build(); + static final com.google.spanner.v1.ResultSet READ_MULTIPLE_KEY_VALUE_RESULTSET = + generateKeyValueResultSet(ContiguousSet.closed(1, 3)); + + static com.google.spanner.v1.ResultSet generateKeyValueResultSet(Iterable rows) { + com.google.spanner.v1.ResultSet.Builder builder = com.google.spanner.v1.ResultSet.newBuilder(); + for (Integer row : rows) { + builder.addRows( + ListValue.newBuilder() + .addValues(com.google.protobuf.Value.newBuilder().setStringValue("k" + row).build()) + .addValues(com.google.protobuf.Value.newBuilder().setStringValue("v" + row).build()) + .build()); + } + return builder.setMetadata(READ_KEY_VALUE_METADATA).build(); + } + + static final ResultSetMetadata READ_FIRST_NAME_SINGERS_METADATA = + ResultSetMetadata.newBuilder() + .setRowType( + StructType.newBuilder() + .addFields( + Field.newBuilder() + .setName("FirstName") + .setType( + com.google.spanner.v1.Type.newBuilder() + .setCode(TypeCode.STRING) + .build()) + .build()) + .build()) + .build(); + static final com.google.spanner.v1.ResultSet READ_FIRST_NAME_SINGERS_RESULTSET = + com.google.spanner.v1.ResultSet.newBuilder() + .addRows( + ListValue.newBuilder() + .addValues( + com.google.protobuf.Value.newBuilder().setStringValue("FirstName").build()) + .build()) + .setMetadata(READ_FIRST_NAME_SINGERS_METADATA) + .build(); +} diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MutationTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MutationTest.java index f857f08f0a..12edae4479 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MutationTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MutationTest.java @@ -18,6 +18,7 @@ import static com.google.common.testing.SerializableTester.reserializeAndAssert; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; import com.google.cloud.ByteArray; import com.google.cloud.Date; @@ -30,16 +31,13 @@ import java.util.List; import org.hamcrest.Matcher; import org.hamcrest.MatcherAssert; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Unit tests for {@link com.google.cloud.spanner.Mutation}. */ @RunWith(JUnit4.class) public class MutationTest { - @Rule public ExpectedException expectedException = ExpectedException.none(); @Test public void insertEmpty() { @@ -123,16 +121,22 @@ public void replace() { @Test public void duplicateColumn() { - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("Duplicate column"); - Mutation.newInsertBuilder("T1").set("C1").to(true).set("C1").to(false).build(); + try { + Mutation.newInsertBuilder("T1").set("C1").to(true).set("C1").to(false).build(); + fail("Expected exception"); + } catch (IllegalStateException ex) { + assertThat(ex.getMessage()).contains("Duplicate column"); + } } @Test public void duplicateColumnCaseInsensitive() { - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("Duplicate column"); - Mutation.newInsertBuilder("T1").set("C1").to(true).set("c1").to(false).build(); + try { + Mutation.newInsertBuilder("T1").set("C1").to(true).set("c1").to(false).build(); + fail("Expected exception"); + } catch (IllegalStateException ex) { + assertThat(ex.getMessage()).contains("Duplicate column"); + } } @Test @@ -149,30 +153,36 @@ public void asMap() { public void unfinishedBindingV1() { Mutation.WriteBuilder b = Mutation.newInsertBuilder("T1"); b.set("C1"); - - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("Incomplete binding for column C1"); - b.build(); + try { + b.build(); + fail("Expected exception"); + } catch (IllegalStateException ex) { + assertThat(ex.getMessage()).contains("Incomplete binding for column C1"); + } } @Test public void unfinishedBindingV2() { Mutation.WriteBuilder b = Mutation.newInsertBuilder("T1"); b.set("C1"); - - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("Incomplete binding for column C1"); - b.set("C2"); + try { + b.set("C2"); + fail("Expected exception"); + } catch (IllegalStateException ex) { + assertThat(ex.getMessage()).contains("Incomplete binding for column C1"); + } } @Test public void notInBinding() { ValueBinder binder = Mutation.newInsertBuilder("T1").set("C1"); binder.to(1234); - - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("No binding currently active"); - binder.to(5678); + try { + binder.to(5678); + fail("Expected exception"); + } catch (IllegalStateException ex) { + assertThat(ex.getMessage()).contains("No binding currently active"); + } } @Test @@ -403,7 +413,7 @@ public void toProtoCoalescingDeleteChanges() { } @Test - public void javaSerialization() throws Exception { + public void javaSerialization() { reserializeAndAssert(appendAllTypes(Mutation.newInsertBuilder("test")).build()); reserializeAndAssert(appendAllTypes(Mutation.newUpdateBuilder("test")).build()); reserializeAndAssert(appendAllTypes(Mutation.newReplaceBuilder("test")).build()); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/OperationTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/OperationTest.java index 8f0da8c6ff..169bf481d0 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/OperationTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/OperationTest.java @@ -16,9 +16,9 @@ package com.google.cloud.spanner; -import static com.google.cloud.spanner.SpannerMatchers.isSpannerException; import static com.google.common.truth.Truth.assertThat; import static com.google.longrunning.Operation.newBuilder; +import static org.junit.Assert.fail; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; @@ -31,9 +31,7 @@ import com.google.rpc.Status; import com.google.spanner.admin.database.v1.CreateDatabaseMetadata; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.mockito.Mock; @@ -49,8 +47,6 @@ public class OperationTest { @Mock private DatabaseAdminClient dbClient; @Mock private ApiClock clock; - @Rule public ExpectedException expectedException = ExpectedException.none(); - private class ParserImpl implements Operation.Parser { @Override @@ -91,8 +87,12 @@ public void failedOperation() { assertThat(op.isDone()).isTrue(); assertThat(op.isSuccessful()).isFalse(); assertThat(op.getMetadata()).isNull(); - expectedException.expect(isSpannerException(ErrorCode.NOT_FOUND)); - op.getResult(); + try { + op.getResult(); + fail("Expected exception"); + } catch (SpannerException ex) { + assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.NOT_FOUND); + } } @Test @@ -149,7 +149,7 @@ public void reload() { } @Test - public void waitForCompletes() throws Exception { + public void waitForCompletes() { com.google.longrunning.Operation proto = newBuilder().setName("op1").setDone(false).build(); Operation op = Operation.create(rpc, proto, new ParserImpl()); com.google.spanner.admin.database.v1.Database db = @@ -174,15 +174,18 @@ public void waitForCompletes() throws Exception { } @Test - public void waitForTimesout() throws Exception { + public void waitForTimesout() { com.google.longrunning.Operation proto = newBuilder().setName("op1").setDone(false).build(); Operation op = Operation.create(rpc, proto, new ParserImpl(), clock); when(rpc.getOperation("op1")).thenReturn(proto); when(clock.nanoTime()).thenReturn(0L, 50_000_000L, 100_000_000L, 150_000_000L); - - expectedException.expect(isSpannerException(ErrorCode.DEADLINE_EXCEEDED)); - op.waitFor( - RetryOption.totalTimeout(Duration.ofMillis(100L)), - RetryOption.initialRetryDelay(Duration.ZERO)); + try { + op.waitFor( + RetryOption.totalTimeout(Duration.ofMillis(100L)), + RetryOption.initialRetryDelay(Duration.ZERO)); + fail("Expected exception"); + } catch (SpannerException ex) { + assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.DEADLINE_EXCEEDED); + } } } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/OptionsTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/OptionsTest.java index 70544a661e..0931eba174 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/OptionsTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/OptionsTest.java @@ -17,40 +17,55 @@ package com.google.cloud.spanner; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Unit tests for {@link Options}. */ @RunWith(JUnit4.class) public class OptionsTest { - @Rule public ExpectedException expectedException = ExpectedException.none(); @Test public void negativeLimitsNotAllowed() { - expectedException.expect(IllegalArgumentException.class); - Options.limit(-1); + try { + Options.limit(-1); + fail("Expected exception"); + } catch (IllegalArgumentException ex) { + assertNotNull(ex.getMessage()); + } } @Test public void zeroLimitNotAllowed() { - expectedException.expect(IllegalArgumentException.class); - Options.limit(0); + try { + Options.limit(0); + fail("Expected exception"); + } catch (IllegalArgumentException ex) { + assertNotNull(ex.getMessage()); + } } @Test public void negativePrefetchChunksNotAllowed() { - expectedException.expect(IllegalArgumentException.class); - Options.prefetchChunks(-1); + try { + Options.prefetchChunks(-1); + fail("Expected exception"); + } catch (IllegalArgumentException ex) { + assertNotNull(ex.getMessage()); + } } @Test public void zeroPrefetchChunksNotAllowed() { - expectedException.expect(IllegalArgumentException.class); - Options.prefetchChunks(0); + try { + Options.prefetchChunks(0); + fail("Expected exception"); + } catch (IllegalArgumentException ex) { + assertNotNull(ex.getMessage()); + } } @Test diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/PartitionOptionsTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/PartitionOptionsTest.java index 4eb5ba5bfa..e8c8dd001f 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/PartitionOptionsTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/PartitionOptionsTest.java @@ -18,18 +18,17 @@ import static com.google.common.testing.SerializableTester.reserializeAndAssert; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; import com.google.common.testing.EqualsTester; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Unit tests for {@link com.google.cloud.spanner.PartitionOptions}. */ @RunWith(JUnit4.class) public class PartitionOptionsTest { - @Rule public ExpectedException expectedException = ExpectedException.none(); @Test public void builder() { @@ -86,13 +85,21 @@ public void equalAndHashCode() { @Test public void invalidDesiredBytesPerBatch() { - expectedException.expect(IllegalArgumentException.class); - PartitionOptions.newBuilder().setPartitionSizeBytes(-1).build(); + try { + PartitionOptions.newBuilder().setPartitionSizeBytes(-1).build(); + fail("Expected exception"); + } catch (IllegalArgumentException ex) { + assertNotNull(ex.getMessage()); + } } @Test public void invalidMaxPartitionCount() { - expectedException.expect(IllegalArgumentException.class); - PartitionOptions.newBuilder().setMaxPartitions(-1).build(); + try { + PartitionOptions.newBuilder().setMaxPartitions(-1).build(); + fail("Expected exception"); + } catch (IllegalArgumentException ex) { + assertNotNull(ex.getMessage()); + } } } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RandomResultSetGenerator.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RandomResultSetGenerator.java new file mode 100644 index 0000000000..63bc234a41 --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RandomResultSetGenerator.java @@ -0,0 +1,166 @@ +/* + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.api.client.util.Base64; +import com.google.cloud.Date; +import com.google.cloud.Timestamp; +import com.google.protobuf.ListValue; +import com.google.protobuf.NullValue; +import com.google.protobuf.Value; +import com.google.protobuf.util.Timestamps; +import com.google.spanner.v1.ResultSet; +import com.google.spanner.v1.ResultSetMetadata; +import com.google.spanner.v1.StructType; +import com.google.spanner.v1.StructType.Field; +import com.google.spanner.v1.Type; +import com.google.spanner.v1.TypeCode; +import java.util.Random; + +public class RandomResultSetGenerator { + private static final Type TYPES[] = + new Type[] { + Type.newBuilder().setCode(TypeCode.BOOL).build(), + Type.newBuilder().setCode(TypeCode.INT64).build(), + Type.newBuilder().setCode(TypeCode.FLOAT64).build(), + Type.newBuilder().setCode(TypeCode.STRING).build(), + Type.newBuilder().setCode(TypeCode.BYTES).build(), + Type.newBuilder().setCode(TypeCode.DATE).build(), + Type.newBuilder().setCode(TypeCode.TIMESTAMP).build(), + Type.newBuilder() + .setCode(TypeCode.ARRAY) + .setArrayElementType(Type.newBuilder().setCode(TypeCode.BOOL)) + .build(), + Type.newBuilder() + .setCode(TypeCode.ARRAY) + .setArrayElementType(Type.newBuilder().setCode(TypeCode.INT64)) + .build(), + Type.newBuilder() + .setCode(TypeCode.ARRAY) + .setArrayElementType(Type.newBuilder().setCode(TypeCode.FLOAT64)) + .build(), + Type.newBuilder() + .setCode(TypeCode.ARRAY) + .setArrayElementType(Type.newBuilder().setCode(TypeCode.STRING)) + .build(), + Type.newBuilder() + .setCode(TypeCode.ARRAY) + .setArrayElementType(Type.newBuilder().setCode(TypeCode.BYTES)) + .build(), + Type.newBuilder() + .setCode(TypeCode.ARRAY) + .setArrayElementType(Type.newBuilder().setCode(TypeCode.DATE)) + .build(), + Type.newBuilder() + .setCode(TypeCode.ARRAY) + .setArrayElementType(Type.newBuilder().setCode(TypeCode.TIMESTAMP)) + .build(), + }; + + private static final ResultSetMetadata generateMetadata() { + StructType.Builder rowTypeBuilder = StructType.newBuilder(); + for (int col = 0; col < TYPES.length; col++) { + rowTypeBuilder.addFields(Field.newBuilder().setName("COL" + col).setType(TYPES[col])).build(); + } + ResultSetMetadata.Builder builder = ResultSetMetadata.newBuilder(); + builder.setRowType(rowTypeBuilder.build()); + return builder.build(); + } + + private static final ResultSetMetadata METADATA = generateMetadata(); + + private final int rowCount; + private final Random random = new Random(); + + public RandomResultSetGenerator(int rowCount) { + this.rowCount = rowCount; + } + + public ResultSet generate() { + ResultSet.Builder builder = ResultSet.newBuilder(); + for (int row = 0; row < rowCount; row++) { + ListValue.Builder rowBuilder = ListValue.newBuilder(); + for (int col = 0; col < TYPES.length; col++) { + Value.Builder valueBuilder = Value.newBuilder(); + setRandomValue(valueBuilder, TYPES[col]); + rowBuilder.addValues(valueBuilder.build()); + } + builder.addRows(rowBuilder.build()); + } + builder.setMetadata(METADATA); + return builder.build(); + } + + private void setRandomValue(Value.Builder builder, Type type) { + if (randomNull()) { + builder.setNullValue(NullValue.NULL_VALUE); + } else { + switch (type.getCode()) { + case ARRAY: + int length = random.nextInt(20) + 1; + ListValue.Builder arrayBuilder = ListValue.newBuilder(); + for (int i = 0; i < length; i++) { + Value.Builder valueBuilder = Value.newBuilder(); + setRandomValue(valueBuilder, type.getArrayElementType()); + arrayBuilder.addValues(valueBuilder.build()); + } + builder.setListValue(arrayBuilder.build()); + break; + case BOOL: + builder.setBoolValue(random.nextBoolean()); + break; + case STRING: + case BYTES: + byte[] bytes = new byte[random.nextInt(200)]; + random.nextBytes(bytes); + builder.setStringValue(Base64.encodeBase64String(bytes)); + break; + case DATE: + Date date = + Date.fromYearMonthDay( + random.nextInt(2019) + 1, random.nextInt(11) + 1, random.nextInt(28) + 1); + builder.setStringValue(date.toString()); + break; + case FLOAT64: + builder.setNumberValue(random.nextDouble()); + break; + case INT64: + builder.setStringValue(String.valueOf(random.nextLong())); + break; + case TIMESTAMP: + com.google.protobuf.Timestamp ts = + Timestamps.add( + Timestamps.EPOCH, + com.google.protobuf.Duration.newBuilder() + .setSeconds(random.nextInt(100_000_000)) + .setNanos(random.nextInt(1000_000_000)) + .build()); + builder.setStringValue(Timestamp.fromProto(ts).toString()); + break; + case STRUCT: + case TYPE_CODE_UNSPECIFIED: + case UNRECOGNIZED: + default: + throw new IllegalArgumentException("Unknown or unsupported type: " + type.getCode()); + } + } + } + + private boolean randomNull() { + return random.nextInt(10) == 0; + } +} diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReadAsyncTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReadAsyncTest.java new file mode 100644 index 0000000000..13e4c47d08 --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ReadAsyncTest.java @@ -0,0 +1,510 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.spanner; + +import static com.google.cloud.spanner.MockSpannerTestUtil.*; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import com.google.api.core.ApiFunction; +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutures; +import com.google.api.core.SettableApiFuture; +import com.google.api.gax.grpc.testing.LocalChannelProvider; +import com.google.cloud.NoCredentials; +import com.google.cloud.spanner.AsyncResultSet.CallbackResponse; +import com.google.cloud.spanner.AsyncResultSet.ReadyCallback; +import com.google.cloud.spanner.MockSpannerServiceImpl.SimulatedExecutionTime; +import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult; +import com.google.common.base.Function; +import com.google.common.collect.ContiguousSet; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import io.grpc.Server; +import io.grpc.Status; +import io.grpc.inprocess.InProcessServerBuilder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Deque; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.SynchronousQueue; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class ReadAsyncTest { + private static MockSpannerServiceImpl mockSpanner; + private static Server server; + private static LocalChannelProvider channelProvider; + + private static ExecutorService executor; + private Spanner spanner; + private DatabaseClient client; + + @BeforeClass + public static void setup() throws Exception { + mockSpanner = new MockSpannerServiceImpl(); + mockSpanner.putStatementResult( + StatementResult.query(READ_ONE_KEY_VALUE_STATEMENT, READ_ONE_KEY_VALUE_RESULTSET)); + mockSpanner.putStatementResult( + StatementResult.query(READ_ONE_EMPTY_KEY_VALUE_STATEMENT, EMPTY_KEY_VALUE_RESULTSET)); + mockSpanner.putStatementResult( + StatementResult.query( + READ_MULTIPLE_KEY_VALUE_STATEMENT, READ_MULTIPLE_KEY_VALUE_RESULTSET)); + + String uniqueName = InProcessServerBuilder.generateName(); + server = + InProcessServerBuilder.forName(uniqueName) + .scheduledExecutorService(new ScheduledThreadPoolExecutor(1)) + .addService(mockSpanner) + .build() + .start(); + channelProvider = LocalChannelProvider.create(uniqueName); + executor = Executors.newScheduledThreadPool(8); + } + + @AfterClass + public static void teardown() throws Exception { + executor.shutdown(); + server.shutdown(); + server.awaitTermination(); + } + + @Before + public void before() { + spanner = + SpannerOptions.newBuilder() + .setProjectId(TEST_PROJECT) + .setChannelProvider(channelProvider) + .setCredentials(NoCredentials.getInstance()) + .setSessionPoolOption( + SessionPoolOptions.newBuilder().setFailOnSessionLeak().setMinSessions(0).build()) + .build() + .getService(); + client = spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); + } + + @After + public void after() { + spanner.close(); + mockSpanner.removeAllExecutionTimes(); + } + + @Test + public void readAsyncPropagatesError() throws Exception { + ApiFuture result; + try (AsyncResultSet resultSet = + client + .singleUse(TimestampBound.strong()) + .readAsync(EMPTY_READ_TABLE_NAME, KeySet.singleKey(Key.of("k99")), READ_COLUMN_NAMES)) { + result = + resultSet.setCallback( + executor, + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + throw SpannerExceptionFactory.newSpannerException( + ErrorCode.CANCELLED, "Don't want the data"); + } + }); + } + try { + result.get(); + fail("missing expected exception"); + } catch (ExecutionException e) { + assertThat(e.getCause()).isInstanceOf(SpannerException.class); + SpannerException se = (SpannerException) e.getCause(); + assertThat(se.getErrorCode()).isEqualTo(ErrorCode.CANCELLED); + assertThat(se.getMessage()).contains("Don't want the data"); + } + } + + @Test + public void emptyReadAsync() throws Exception { + ApiFuture result; + try (AsyncResultSet resultSet = + client + .singleUse(TimestampBound.strong()) + .readAsync(EMPTY_READ_TABLE_NAME, KeySet.singleKey(Key.of("k99")), READ_COLUMN_NAMES)) { + result = + resultSet.setCallback( + executor, + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + while (true) { + switch (resultSet.tryNext()) { + case OK: + fail("received unexpected data"); + case NOT_READY: + return CallbackResponse.CONTINUE; + case DONE: + assertThat(resultSet.getType()).isEqualTo(READ_TABLE_TYPE); + return CallbackResponse.DONE; + } + } + } + }); + } + assertThat(result.get()).isNull(); + } + + @Test + public void pointReadAsync() throws Exception { + ApiFuture row = + client + .singleUse(TimestampBound.strong()) + .readRowAsync(READ_TABLE_NAME, Key.of("k1"), READ_COLUMN_NAMES); + assertThat(row.get()).isNotNull(); + assertThat(row.get().getString(0)).isEqualTo("k1"); + assertThat(row.get().getString(1)).isEqualTo("v1"); + } + + @Test + public void pointReadNotFound() throws Exception { + ApiFuture row = + client + .singleUse(TimestampBound.strong()) + .readRowAsync(EMPTY_READ_TABLE_NAME, Key.of("k999"), READ_COLUMN_NAMES); + assertThat(row.get()).isNull(); + } + + @Test + public void invalidDatabase() throws Exception { + mockSpanner.setBatchCreateSessionsExecutionTime( + SimulatedExecutionTime.stickyDatabaseNotFoundException("invalid-database")); + DatabaseClient invalidClient = + spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, "invalid-database")); + ApiFuture row = + invalidClient + .singleUse(TimestampBound.strong()) + .readRowAsync(READ_TABLE_NAME, Key.of("k99"), READ_COLUMN_NAMES); + try { + row.get(); + fail("missing expected exception"); + } catch (ExecutionException e) { + assertThat(e.getCause()).isInstanceOf(DatabaseNotFoundException.class); + } + } + + @Test + public void tableNotFound() throws Exception { + mockSpanner.setStreamingReadExecutionTime( + SimulatedExecutionTime.ofException( + Status.NOT_FOUND + .withDescription("Table not found: BadTableName") + .asRuntimeException())); + ApiFuture row = + client + .singleUse(TimestampBound.strong()) + .readRowAsync("BadTableName", Key.of("k1"), READ_COLUMN_NAMES); + try { + row.get(); + fail("missing expected exception"); + } catch (ExecutionException e) { + assertThat(e.getCause()).isInstanceOf(SpannerException.class); + SpannerException se = (SpannerException) e.getCause(); + assertThat(se.getErrorCode()).isEqualTo(ErrorCode.NOT_FOUND); + assertThat(se.getMessage()).contains("BadTableName"); + } + } + + /** + * Ending a read-only transaction before an asynchronous query that was executed on that + * transaction has finished fetching all rows should keep the session checked out of the pool + * until all the rows have been returned. The session is then automatically returned to the + * session. + */ + @Test + public void closeTransactionBeforeEndOfAsyncQuery() throws Exception { + final BlockingQueue results = new SynchronousQueue<>(); + final SettableApiFuture finished = SettableApiFuture.create(); + ApiFuture closed; + DatabaseClientImpl clientImpl = (DatabaseClientImpl) client; + + // There should currently not be any sessions checked out of the pool. + assertThat(clientImpl.pool.getNumberOfSessionsInUse()).isEqualTo(0); + + final CountDownLatch dataReceived = new CountDownLatch(1); + try (ReadOnlyTransaction tx = client.readOnlyTransaction()) { + try (AsyncResultSet rs = + tx.readAsync(READ_TABLE_NAME, KeySet.all(), READ_COLUMN_NAMES, Options.bufferRows(1))) { + closed = + rs.setCallback( + executor, + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + try { + while (true) { + switch (resultSet.tryNext()) { + case DONE: + finished.set(true); + return CallbackResponse.DONE; + case NOT_READY: + return CallbackResponse.CONTINUE; + case OK: + dataReceived.countDown(); + results.put(resultSet.getString(0)); + } + } + } catch (Throwable t) { + finished.setException(t); + return CallbackResponse.DONE; + } + } + }); + } + // Wait until at least one row has been fetched. At that moment there should be one session + // checked out. + dataReceived.await(); + assertThat(clientImpl.pool.getNumberOfSessionsInUse()).isEqualTo(1); + } + // The read-only transaction is now closed, but the ready callback will continue to receive + // data. As it tries to put the data into a synchronous queue and the underlying buffer can also + // only hold 1 row, the async result set has not yet finished. The read-only transaction will + // release the session back into the pool when all async statements have finished. The number of + // sessions in use is therefore still 1. + assertThat(clientImpl.pool.getNumberOfSessionsInUse()).isEqualTo(1); + List resultList = new ArrayList<>(); + do { + results.drainTo(resultList); + } while (!finished.isDone() || results.size() > 0); + assertThat(finished.get()).isTrue(); + assertThat(resultList).containsExactly("k1", "k2", "k3"); + // The session will be released back into the pool by the asynchronous result set when it has + // returned all rows. As this is done in the background, it could take a couple of milliseconds. + closed.get(); + assertThat(clientImpl.pool.getNumberOfSessionsInUse()).isEqualTo(0); + } + + @Test + public void readOnlyTransaction() throws Exception { + Statement statement1 = + Statement.of("SELECT * FROM TestTable WHERE Key IN ('k10', 'k11', 'k12')"); + Statement statement2 = Statement.of("SELECT * FROM TestTable WHERE Key IN ('k1', 'k2', 'k3"); + mockSpanner.putStatementResult( + StatementResult.query(statement1, generateKeyValueResultSet(ContiguousSet.closed(10, 12)))); + mockSpanner.putStatementResult( + StatementResult.query(statement2, generateKeyValueResultSet(ContiguousSet.closed(1, 3)))); + + ApiFuture> values1; + ApiFuture> values2; + try (ReadOnlyTransaction tx = client.readOnlyTransaction()) { + try (AsyncResultSet rs = tx.executeQueryAsync(statement1)) { + values1 = + rs.toListAsync( + new Function() { + @Override + public String apply(StructReader input) { + return input.getString("Value"); + } + }, + executor); + } + try (AsyncResultSet rs = tx.executeQueryAsync(statement2)) { + values2 = + rs.toListAsync( + new Function() { + @Override + public String apply(StructReader input) { + return input.getString("Value"); + } + }, + executor); + } + } + ApiFuture> allValues = + ApiFutures.transform( + ApiFutures.allAsList(Arrays.asList(values1, values2)), + new ApiFunction>, Iterable>() { + @Override + public Iterable apply(List> input) { + return Iterables.mergeSorted( + input, + new Comparator() { + @Override + public int compare(String o1, String o2) { + // Return in numerical order (i.e. without the preceding 'v'). + return Integer.valueOf(o1.substring(1)) + .compareTo(Integer.valueOf(o2.substring(1))); + } + }); + } + }, + executor); + assertThat(allValues.get()).containsExactly("v1", "v2", "v3", "v10", "v11", "v12"); + } + + @Test + public void pauseResume() throws Exception { + Statement unevenStatement = + Statement.of("SELECT * FROM TestTable WHERE MOD(CAST(SUBSTR(Key, 2) AS INT64), 2) = 1"); + Statement evenStatement = + Statement.of("SELECT * FROM TestTable WHERE MOD(CAST(SUBSTR(Key, 2) AS INT64), 2) = 0"); + mockSpanner.putStatementResult( + StatementResult.query( + unevenStatement, generateKeyValueResultSet(ImmutableSet.of(1, 3, 5, 7, 9)))); + mockSpanner.putStatementResult( + StatementResult.query( + evenStatement, generateKeyValueResultSet(ImmutableSet.of(2, 4, 6, 8, 10)))); + + final Object lock = new Object(); + ApiFuture evenFinished; + ApiFuture unevenFinished; + final CountDownLatch unevenReturnedFirstRow = new CountDownLatch(1); + final Deque allValues = new ConcurrentLinkedDeque<>(); + try (ReadOnlyTransaction tx = client.readOnlyTransaction()) { + try (AsyncResultSet evenRs = tx.executeQueryAsync(evenStatement); + AsyncResultSet unevenRs = tx.executeQueryAsync(unevenStatement)) { + unevenFinished = + unevenRs.setCallback( + executor, + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + while (true) { + switch (resultSet.tryNext()) { + case DONE: + return CallbackResponse.DONE; + case NOT_READY: + return CallbackResponse.CONTINUE; + case OK: + synchronized (lock) { + allValues.add(resultSet.getString("Value")); + } + unevenReturnedFirstRow.countDown(); + return CallbackResponse.PAUSE; + } + } + } + }); + evenFinished = + evenRs.setCallback( + executor, + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + try { + // Make sure the uneven result set has returned the first before we start the + // even + // results. + unevenReturnedFirstRow.await(); + while (true) { + switch (resultSet.tryNext()) { + case DONE: + return CallbackResponse.DONE; + case NOT_READY: + return CallbackResponse.CONTINUE; + case OK: + synchronized (lock) { + allValues.add(resultSet.getString("Value")); + } + return CallbackResponse.PAUSE; + } + } + } catch (InterruptedException e) { + throw SpannerExceptionFactory.propagateInterrupt(e); + } + } + }); + while (!(evenFinished.isDone() && unevenFinished.isDone())) { + synchronized (lock) { + if (allValues.peekLast() != null) { + if (Integer.valueOf(allValues.peekLast().substring(1)) % 2 == 1) { + evenRs.resume(); + } else { + unevenRs.resume(); + } + } + if (allValues.size() == 10) { + unevenRs.resume(); + evenRs.resume(); + } + } + } + } + } + assertThat(ApiFutures.allAsList(Arrays.asList(evenFinished, unevenFinished)).get()) + .containsExactly(null, null); + assertThat(allValues) + .containsExactly("v1", "v2", "v3", "v4", "v5", "v6", "v7", "v8", "v9", "v10"); + } + + @Test + public void cancel() throws Exception { + final List values = new LinkedList<>(); + final CountDownLatch receivedFirstRow = new CountDownLatch(1); + final CountDownLatch cancelled = new CountDownLatch(1); + final ApiFuture res; + try (AsyncResultSet rs = + client.singleUse().readAsync(READ_TABLE_NAME, KeySet.all(), READ_COLUMN_NAMES)) { + res = + rs.setCallback( + executor, + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + try { + while (true) { + switch (resultSet.tryNext()) { + case DONE: + return CallbackResponse.DONE; + case NOT_READY: + return CallbackResponse.CONTINUE; + case OK: + values.add(resultSet.getString("Value")); + receivedFirstRow.countDown(); + cancelled.await(); + break; + } + } + } catch (Throwable t) { + return CallbackResponse.DONE; + } + } + }); + receivedFirstRow.await(); + rs.cancel(); + } + cancelled.countDown(); + try { + res.get(); + fail("missing expected exception"); + } catch (ExecutionException e) { + assertThat(e.getCause()).isInstanceOf(SpannerException.class); + SpannerException se = (SpannerException) e.getCause(); + assertThat(se.getErrorCode()).isEqualTo(ErrorCode.CANCELLED); + assertThat(values).containsExactly("v1"); + } + } +} diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResultSetsTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResultSetsTest.java index 006e5a62b1..93b6d9f604 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResultSetsTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ResultSetsTest.java @@ -17,6 +17,7 @@ package com.google.cloud.spanner; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.fail; import com.google.cloud.ByteArray; @@ -27,9 +28,7 @@ import com.google.common.primitives.Longs; import java.util.Arrays; import java.util.List; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -37,8 +36,6 @@ @RunWith(JUnit4.class) public class ResultSetsTest { - @Rule public ExpectedException expected = ExpectedException.none(); - @Test public void resultSetIteration() { double doubleVal = 1.2; @@ -234,10 +231,13 @@ public void resultSetIterationWithStructColumns() { Struct value1 = Struct.newBuilder().set("g1").to("abc").build(); Struct struct1 = Struct.newBuilder().set("f1").to(value1).set("f2").to((Long) null).build(); - - expected.expect(UnsupportedOperationException.class); - expected.expectMessage("STRUCT-typed columns are not supported inside ResultSets."); - ResultSets.forRows(type, Arrays.asList(struct1)); + try { + ResultSets.forRows(type, Arrays.asList(struct1)); + fail("Expected exception"); + } catch (UnsupportedOperationException ex) { + assertThat(ex.getMessage()) + .contains("STRUCT-typed columns are not supported inside ResultSets."); + } } @Test @@ -306,8 +306,12 @@ public void closeResultSet() { Type.struct(Type.StructField.of("f1", Type.string())), Arrays.asList(Struct.newBuilder().set("f1").to("x").build())); rs.close(); - expected.expect(IllegalStateException.class); - rs.getCurrentRowAsStruct(); + try { + rs.getCurrentRowAsStruct(); + fail("Expected exception"); + } catch (IllegalStateException ex) { + assertNotNull(ex.getMessage()); + } } @Test @@ -316,7 +320,11 @@ public void exceptionIfNextIsNotCalled() { ResultSets.forRows( Type.struct(Type.StructField.of("f1", Type.string())), Arrays.asList(Struct.newBuilder().set("f1").to("x").build())); - expected.expect(IllegalStateException.class); - rs.getCurrentRowAsStruct(); + try { + rs.getCurrentRowAsStruct(); + fail("Expected exception"); + } catch (IllegalStateException ex) { + assertNotNull(ex.getMessage()); + } } } 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 ca3e621ff9..6c387f0d48 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 @@ -16,8 +16,8 @@ package com.google.cloud.spanner; -import static com.google.cloud.spanner.SpannerMatchers.isSpannerException; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -40,15 +40,13 @@ import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.mockito.Mockito; import org.mockito.internal.util.reflection.Whitebox; -/** Unit tests for {@link SpannerImpl.ResumableStreamIterator}. */ +/** Unit tests for {@link AbstractResultSet.ResumableStreamIterator}. */ @RunWith(JUnit4.class) public class ResumableStreamIteratorTest { interface Starter { @@ -112,8 +110,6 @@ public void close(@Nullable String message) { } } - @Rule public ExpectedException expectedException = ExpectedException.none(); - Starter starter = Mockito.mock(Starter.class); AbstractResultSet.ResumableStreamIterator resumableStreamIterator; @@ -237,8 +233,12 @@ public void nonRetryableError() { Iterator strings = stringIterator(resumableStreamIterator); assertThat(strings.next()).isEqualTo("a"); assertThat(strings.next()).isEqualTo("b"); - expectedException.expect(isSpannerException(ErrorCode.FAILED_PRECONDITION)); - assertThat(strings.next()).isNotEqualTo("X"); + try { + assertThat(strings.next()).isNotEqualTo("X"); + fail("Expected exception"); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.FAILED_PRECONDITION); + } } @Test @@ -341,8 +341,12 @@ public void bufferLimitMissingTokensUnsafeToRetry() { .thenThrow(new RetryableException(ErrorCode.UNAVAILABLE, "failed by test")); assertThat(consumeAtMost(3, resumableStreamIterator)).containsExactly("a", "b", "c").inOrder(); - expectedException.expect(isSpannerException(ErrorCode.UNAVAILABLE)); - resumableStreamIterator.next(); + try { + resumableStreamIterator.next(); + fail("Expected exception"); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.UNAVAILABLE); + } } @Test diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RetryOnInvalidatedSessionTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RetryOnInvalidatedSessionTest.java index d202f5d5eb..7380791eed 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RetryOnInvalidatedSessionTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RetryOnInvalidatedSessionTest.java @@ -19,6 +19,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; +import com.google.api.core.ApiFuture; import com.google.api.gax.core.NoCredentialsProvider; import com.google.api.gax.grpc.testing.LocalChannelProvider; import com.google.cloud.NoCredentials; @@ -28,7 +29,9 @@ import com.google.cloud.spanner.v1.SpannerClient; import com.google.cloud.spanner.v1.SpannerClient.ListSessionsPagedResponse; import com.google.cloud.spanner.v1.SpannerSettings; +import com.google.common.base.Function; import com.google.common.base.Stopwatch; +import com.google.common.collect.ImmutableList; import com.google.protobuf.ListValue; import com.google.spanner.v1.ResultSetMetadata; import com.google.spanner.v1.StructType; @@ -41,6 +44,9 @@ import java.util.Arrays; import java.util.Collection; import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import org.junit.After; import org.junit.AfterClass; @@ -54,6 +60,14 @@ @RunWith(Parameterized.class) public class RetryOnInvalidatedSessionTest { + private static final class ToLongTransformer implements Function { + @Override + public Long apply(StructReader input) { + return input.getLong(0); + } + } + + private static final ToLongTransformer TO_LONG = new ToLongTransformer(); @Parameter(0) public boolean failOnInvalidatedSession; @@ -138,6 +152,7 @@ public static Collection data() { private static SpannerClient spannerClient; private static Spanner spanner; private static DatabaseClient client; + private static ExecutorService executor; @BeforeClass public static void startStaticServer() throws IOException { @@ -166,6 +181,7 @@ public static void startStaticServer() throws IOException { .setCredentialsProvider(NoCredentialsProvider.create()) .build(); spannerClient = SpannerClient.create(settings); + executor = Executors.newSingleThreadExecutor(); } @AfterClass @@ -173,13 +189,16 @@ public static void stopServer() throws InterruptedException { spannerClient.close(); server.shutdown(); server.awaitTermination(); + executor.shutdown(); } @Before - public void setUp() throws IOException { + public void setUp() { mockSpanner.reset(); SessionPoolOptions.Builder builder = - SessionPoolOptions.newBuilder().setWriteSessionsFraction(WRITE_SESSIONS_FRACTION); + SessionPoolOptions.newBuilder() + .setWriteSessionsFraction(WRITE_SESSIONS_FRACTION) + .setFailOnSessionLeak(); if (failOnInvalidatedSession) { builder.setFailIfSessionNotFound(); } @@ -195,7 +214,7 @@ public void setUp() throws IOException { } @After - public void tearDown() throws Exception { + public void tearDown() { spanner.close(); } @@ -253,6 +272,20 @@ public void singleUseSelect() throws InterruptedException { } } + @Test + public void singleUseSelectAsync() throws Exception { + invalidateSessionPool(); + ApiFuture> list; + try (AsyncResultSet rs = client.singleUse().executeQueryAsync(SELECT1AND2)) { + list = rs.toListAsync(TO_LONG, executor); + assertThat(list.get()).containsExactly(1L, 2L); + assertThat(failOnInvalidatedSession).isFalse(); + } catch (ExecutionException e) { + assertThat(e.getCause()).isInstanceOf(SessionNotFoundException.class); + assertThat(failOnInvalidatedSession).isTrue(); + } + } + @Test public void singleUseRead() throws InterruptedException { invalidateSessionPool(); @@ -573,7 +606,7 @@ public void readWriteTransactionReadOnlySessionInPool() throws InterruptedExcept runner.run( new TransactionCallable() { @Override - public Integer run(TransactionContext transaction) throws Exception { + public Integer run(TransactionContext transaction) { int count = 0; try (ResultSet rs = transaction.executeQuery(SELECT1AND2)) { while (rs.next()) { @@ -596,7 +629,7 @@ public void readWriteTransactionSelect() throws InterruptedException { runner.run( new TransactionCallable() { @Override - public Integer run(TransactionContext transaction) throws Exception { + public Integer run(TransactionContext transaction) { int count = 0; try (ResultSet rs = transaction.executeQuery(SELECT1AND2)) { while (rs.next()) { @@ -623,7 +656,7 @@ public void readWriteTransactionRead() throws InterruptedException { runner.run( new TransactionCallable() { @Override - public Integer run(TransactionContext transaction) throws Exception { + public Integer run(TransactionContext transaction) { int count = 0; try (ResultSet rs = transaction.read("FOO", KeySet.all(), Arrays.asList("BAR"))) { while (rs.next()) { @@ -650,7 +683,7 @@ public void readWriteTransactionReadUsingIndex() throws InterruptedException { runner.run( new TransactionCallable() { @Override - public Integer run(TransactionContext transaction) throws Exception { + public Integer run(TransactionContext transaction) { int count = 0; try (ResultSet rs = transaction.readUsingIndex( @@ -679,7 +712,7 @@ public void readWriteTransactionReadRow() throws InterruptedException { runner.run( new TransactionCallable() { @Override - public Struct run(TransactionContext transaction) throws Exception { + public Struct run(TransactionContext transaction) { return transaction.readRow("FOO", Key.of(), Arrays.asList("BAR")); } }); @@ -700,7 +733,7 @@ public void readWriteTransactionReadRowUsingIndex() throws InterruptedException runner.run( new TransactionCallable() { @Override - public Struct run(TransactionContext transaction) throws Exception { + public Struct run(TransactionContext transaction) { return transaction.readRowUsingIndex( "FOO", "IDX", Key.of(), Arrays.asList("BAR")); } @@ -722,7 +755,7 @@ public void readWriteTransactionUpdate() throws InterruptedException { runner.run( new TransactionCallable() { @Override - public Long run(TransactionContext transaction) throws Exception { + public Long run(TransactionContext transaction) { return transaction.executeUpdate(UPDATE_STATEMENT); } }); @@ -743,7 +776,7 @@ public void readWriteTransactionBatchUpdate() throws InterruptedException { runner.run( new TransactionCallable() { @Override - public long[] run(TransactionContext transaction) throws Exception { + public long[] run(TransactionContext transaction) { return transaction.batchUpdate(Arrays.asList(UPDATE_STATEMENT)); } }); @@ -764,7 +797,7 @@ public void readWriteTransactionBuffer() throws InterruptedException { runner.run( new TransactionCallable() { @Override - public Void run(TransactionContext transaction) throws Exception { + public Void run(TransactionContext transaction) { transaction.buffer(Mutation.newInsertBuilder("FOO").set("BAR").to(1L).build()); return null; } @@ -777,7 +810,7 @@ public Void run(TransactionContext transaction) throws Exception { } @Test - public void readWriteTransactionSelectInvalidatedDuringTransaction() throws InterruptedException { + public void readWriteTransactionSelectInvalidatedDuringTransaction() { try { TransactionRunner runner = client.readWriteTransaction(); int attempts = @@ -814,7 +847,7 @@ public Integer run(TransactionContext transaction) throws Exception { } @Test - public void readWriteTransactionReadInvalidatedDuringTransaction() throws InterruptedException { + public void readWriteTransactionReadInvalidatedDuringTransaction() { try { TransactionRunner runner = client.readWriteTransaction(); int attempts = @@ -851,8 +884,7 @@ public Integer run(TransactionContext transaction) throws Exception { } @Test - public void readWriteTransactionReadUsingIndexInvalidatedDuringTransaction() - throws InterruptedException { + public void readWriteTransactionReadUsingIndexInvalidatedDuringTransaction() { try { TransactionRunner runner = client.readWriteTransaction(); int attempts = @@ -893,8 +925,7 @@ public Integer run(TransactionContext transaction) throws Exception { } @Test - public void readWriteTransactionReadRowInvalidatedDuringTransaction() - throws InterruptedException { + public void readWriteTransactionReadRowInvalidatedDuringTransaction() { try { TransactionRunner runner = client.readWriteTransaction(); int attempts = @@ -922,8 +953,7 @@ public Integer run(TransactionContext transaction) throws Exception { } @Test - public void readWriteTransactionReadRowUsingIndexInvalidatedDuringTransaction() - throws InterruptedException { + public void readWriteTransactionReadRowUsingIndexInvalidatedDuringTransaction() { try { TransactionRunner runner = client.readWriteTransaction(); int attempts = diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionClientTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionClientTest.java index fe68020705..39dfee6a3f 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionClientTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionClientTest.java @@ -151,8 +151,7 @@ public void batchCreateAndCloseSessions() { .then( new Answer>() { @Override - public List answer(InvocationOnMock invocation) - throws Throwable { + public List answer(InvocationOnMock invocation) { Map options = invocation.getArgumentAt(3, Map.class); Long channelHint = (Long) options.get(SpannerRpc.Option.CHANNEL_HINT); usedChannels.add(channelHint); @@ -215,8 +214,7 @@ public void batchCreateSessionsDistributesMultipleRequestsOverChannels() { .then( new Answer>() { @Override - public List answer(InvocationOnMock invocation) - throws Throwable { + public List answer(InvocationOnMock invocation) { Map options = invocation.getArgumentAt(3, Map.class); Long channelHint = (Long) options.get(SpannerRpc.Option.CHANNEL_HINT); usedChannelHintss.add(channelHint); @@ -300,8 +298,7 @@ public void batchCreateSessionsWithExceptions() { .then( new Answer>() { @Override - public List answer(InvocationOnMock invocation) - throws Throwable { + public List answer(InvocationOnMock invocation) { Map options = invocation.getArgumentAt(3, Map.class); Long channelHint = (Long) options.get(SpannerRpc.Option.CHANNEL_HINT); if (errorOnChannels.contains(channelHint)) { @@ -368,8 +365,7 @@ public void batchCreateSessionsServerReturnsLessSessionsPerBatch() { .then( new Answer>() { @Override - public List answer(InvocationOnMock invocation) - throws Throwable { + public List answer(InvocationOnMock invocation) { int sessionCount = invocation.getArgumentAt(1, Integer.class); List res = new ArrayList<>(); for (int i = 1; i <= Math.min(MAX_SESSIONS_PER_BATCH, sessionCount); i++) { diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionImplTest.java index f3f205a529..c756a7898a 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionImplTest.java @@ -17,10 +17,12 @@ package com.google.cloud.spanner; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import com.google.api.core.ApiFutures; import com.google.api.core.NanoClock; import com.google.api.gax.retrying.RetrySettings; import com.google.cloud.Timestamp; @@ -40,6 +42,7 @@ import com.google.spanner.v1.ResultSetMetadata; import com.google.spanner.v1.Session; import com.google.spanner.v1.Transaction; +import io.opencensus.trace.Span; import java.text.ParseException; import java.util.Arrays; import java.util.Calendar; @@ -50,9 +53,7 @@ import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.mockito.ArgumentCaptor; @@ -63,11 +64,9 @@ import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; -/** Unit tests for {@link com.google.cloud.spanner.SpannerImpl.SessionImpl}. */ +/** Unit tests for {@link com.google.cloud.spanner.SessionImpl}. */ @RunWith(JUnit4.class) public class SessionImplTest { - @Rule public ExpectedException expectedException = ExpectedException.none(); - @Mock private SpannerRpc rpc; @Mock private SpannerOptions spannerOptions; private com.google.cloud.spanner.Session session; @@ -101,16 +100,17 @@ public void setUp() { .thenReturn(sessionProto); Transaction txn = Transaction.newBuilder().setId(ByteString.copyFromUtf8("TEST")).build(); Mockito.when( - rpc.beginTransaction( + rpc.beginTransactionAsync( Mockito.any(BeginTransactionRequest.class), Mockito.any(Map.class))) - .thenReturn(txn); + .thenReturn(ApiFutures.immediateFuture(txn)); CommitResponse commitResponse = CommitResponse.newBuilder() .setCommitTimestamp(com.google.protobuf.Timestamp.getDefaultInstance()) .build(); - Mockito.when(rpc.commit(Mockito.any(CommitRequest.class), Mockito.any(Map.class))) - .thenReturn(commitResponse); + Mockito.when(rpc.commitAsync(Mockito.any(CommitRequest.class), Mockito.any(Map.class))) + .thenReturn(ApiFutures.immediateFuture(commitResponse)); session = spanner.getSessionClient(db).createSession(); + ((SessionImpl) session).setCurrentSpan(mock(Span.class)); // We expect the same options, "options", on all calls on "session". options = optionsCaptor.getValue(); } @@ -127,7 +127,7 @@ public Void run(TransactionContext transaction) throws SpannerException { .run( new TransactionCallable() { @Override - public Void run(TransactionContext transaction) throws Exception { + public Void run(TransactionContext transaction) { return null; } }); @@ -245,50 +245,60 @@ private static long utcTimeSeconds(int year, int month, int day, int hour, int m public void newSingleUseContextClosesOldSingleUseContext() { ReadContext ctx = session.singleUse(TimestampBound.strong()); session.singleUse(TimestampBound.strong()); - - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("invalidated"); - ctx.read("Dummy", KeySet.all(), Arrays.asList("C")); + try { + ctx.read("Dummy", KeySet.all(), Arrays.asList("C")); + fail("Expected exception"); + } catch (IllegalStateException ex) { + assertThat(ex.getMessage()).contains("invalidated"); + } } @Test public void newSingleUseContextClosesOldSingleUseReadOnlyTransactionContext() { ReadContext ctx = session.singleUseReadOnlyTransaction(TimestampBound.strong()); session.singleUse(TimestampBound.strong()); - - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("invalidated"); - ctx.read("Dummy", KeySet.all(), Arrays.asList("C")); + try { + ctx.read("Dummy", KeySet.all(), Arrays.asList("C")); + fail("Expected exception"); + } catch (IllegalStateException ex) { + assertThat(ex.getMessage()).contains("invalidated"); + } } @Test public void newSingleUseContextClosesOldMultiUseReadOnlyTransactionContext() { ReadContext ctx = session.singleUseReadOnlyTransaction(TimestampBound.strong()); session.singleUse(TimestampBound.strong()); - - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("invalidated"); - ctx.read("Dummy", KeySet.all(), Arrays.asList("C")); + try { + ctx.read("Dummy", KeySet.all(), Arrays.asList("C")); + fail("Expected exception"); + } catch (IllegalStateException ex) { + assertThat(ex.getMessage()).contains("invalidated"); + } } @Test public void newSingleUseReadOnlyTransactionContextClosesOldSingleUseContext() { ReadContext ctx = session.singleUse(TimestampBound.strong()); session.singleUseReadOnlyTransaction(TimestampBound.strong()); - - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("invalidated"); - ctx.read("Dummy", KeySet.all(), Arrays.asList("C")); + try { + ctx.read("Dummy", KeySet.all(), Arrays.asList("C")); + fail("Expected exception"); + } catch (IllegalStateException ex) { + assertThat(ex.getMessage()).contains("invalidated"); + } } @Test public void newMultiUseReadOnlyTransactionContextClosesOldSingleUseContext() { ReadContext ctx = session.singleUse(TimestampBound.strong()); session.readOnlyTransaction(TimestampBound.strong()); - - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("invalidated"); - ctx.read("Dummy", KeySet.all(), Arrays.asList("C")); + try { + ctx.read("Dummy", KeySet.all(), Arrays.asList("C")); + fail("Expected exception"); + } catch (IllegalStateException ex) { + assertThat(ex.getMessage()).contains("invalidated"); + } } @Test @@ -301,10 +311,12 @@ public void writeClosesOldSingleUseContext() throws ParseException { .setCommitTimestamp(Timestamps.parse("2015-10-01T10:54:20.021Z")) .build()); session.writeAtLeastOnce(Arrays.asList()); - - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("invalidated"); - ctx.read("Dummy", KeySet.all(), Arrays.asList("C")); + try { + ctx.read("Dummy", KeySet.all(), Arrays.asList("C")); + fail("Expected exception"); + } catch (IllegalStateException ex) { + assertThat(ex.getMessage()).contains("invalidated"); + } } @Test @@ -313,10 +325,12 @@ public void transactionClosesOldSingleUseContext() { // Note that we don't even run the transaction - just preparing the runner is sufficient. session.readWriteTransaction(); - - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("invalidated"); - ctx.read("Dummy", KeySet.all(), Arrays.asList("C")); + try { + ctx.read("Dummy", KeySet.all(), Arrays.asList("C")); + fail("Expected exception"); + } catch (IllegalStateException ex) { + assertThat(ex.getMessage()).contains("invalidated"); + } } @Test @@ -324,18 +338,20 @@ public void singleUseContextClosesTransaction() { TransactionRunner runner = session.readWriteTransaction(); session.singleUse(TimestampBound.strong()); - - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("invalidated"); - runner.run( - new TransactionRunner.TransactionCallable() { - @Nullable - @Override - public Void run(TransactionContext transaction) throws SpannerException { - fail("Unexpected call to transaction body"); - return null; - } - }); + try { + runner.run( + new TransactionRunner.TransactionCallable() { + @Nullable + @Override + public Void run(TransactionContext transaction) throws SpannerException { + fail("Unexpected call to transaction body"); + return null; + } + }); + fail("Expected exception"); + } catch (IllegalStateException ex) { + assertThat(ex.getMessage()).contains("invalidated"); + } } @Test @@ -345,10 +361,12 @@ public void prepareClosesOldSingleUseContext() { Mockito.when(rpc.beginTransaction(Mockito.any(), Mockito.eq(options))) .thenReturn(Transaction.newBuilder().setId(ByteString.copyFromUtf8("t1")).build()); session.prepareReadWriteTransaction(); - - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("invalidated"); - ctx.read("Dummy", KeySet.all(), Arrays.asList("C")); + try { + ctx.read("Dummy", KeySet.all(), Arrays.asList("C")); + fail("Expected exception"); + } catch (IllegalStateException ex) { + assertThat(ex.getMessage()).contains("invalidated"); + } } private static ResultSetMetadata newMetadata(Type type) { @@ -370,8 +388,12 @@ public void singleUseReadOnlyTransactionDoesntReturnTransactionMetadata() { // be better for the read to fail with an INTERNAL error, but we can't do that until txn // metadata is returned for failed reads (e.g., table-not-found) as well as successful ones. // TODO(user): Fix this. - expectedException.expect(IllegalStateException.class); - txn.getReadTimestamp(); + try { + txn.getReadTimestamp(); + fail("Expected exception"); + } catch (IllegalStateException ex) { + assertNotNull(ex.getMessage()); + } } @Test @@ -386,9 +408,12 @@ public void singleUseReadOnlyTransactionReturnsEmptyTransactionMetadata() { mockRead(resultSet); ReadOnlyTransaction txn = session.singleUseReadOnlyTransaction(TimestampBound.strong()); - - expectedException.expect(SpannerMatchers.isSpannerException(ErrorCode.INTERNAL)); - txn.readRow("Dummy", Key.of(), Arrays.asList("C")); + try { + txn.readRow("Dummy", Key.of(), Arrays.asList("C")); + fail("Expected exception"); + } catch (SpannerException ex) { + assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.INTERNAL); + } } private static class NoOpStreamingCall implements SpannerRpc.StreamingCall { @@ -406,7 +431,7 @@ private void mockRead(final PartialResultSet myResultSet) { .then( new Answer() { @Override - public SpannerRpc.StreamingCall answer(InvocationOnMock invocation) throws Throwable { + public SpannerRpc.StreamingCall answer(InvocationOnMock invocation) { consumer.getValue().onPartialResultSet(myResultSet); consumer.getValue().onCompleted(); return new NoOpStreamingCall(); @@ -426,9 +451,12 @@ public void multiUseReadOnlyTransactionReturnsEmptyTransactionMetadata() { mockRead(resultSet); ReadOnlyTransaction txn = session.readOnlyTransaction(TimestampBound.strong()); - - expectedException.expect(SpannerMatchers.isSpannerException(ErrorCode.INTERNAL)); - txn.readRow("Dummy", Key.of(), Arrays.asList("C")); + try { + txn.readRow("Dummy", Key.of(), Arrays.asList("C")); + fail("Expected exception"); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INTERNAL); + } } @Test @@ -443,9 +471,12 @@ public void multiUseReadOnlyTransactionReturnsMissingTimestamp() { mockRead(resultSet); ReadOnlyTransaction txn = session.readOnlyTransaction(TimestampBound.strong()); - - expectedException.expect(SpannerMatchers.isSpannerException(ErrorCode.INTERNAL)); - txn.readRow("Dummy", Key.of(), Arrays.asList("C")); + try { + txn.readRow("Dummy", Key.of(), Arrays.asList("C")); + fail("Expected exception"); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INTERNAL); + } } @Test @@ -461,8 +492,11 @@ public void multiUseReadOnlyTransactionReturnsMissingTransactionId() throws Pars mockRead(resultSet); ReadOnlyTransaction txn = session.readOnlyTransaction(TimestampBound.strong()); - - expectedException.expect(SpannerMatchers.isSpannerException(ErrorCode.INTERNAL)); - txn.readRow("Dummy", Key.of(), Arrays.asList("C")); + try { + txn.readRow("Dummy", Key.of(), Arrays.asList("C")); + fail("Expected exception"); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INTERNAL); + } } } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolIntegrationTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolIntegrationTest.java index 5492957106..b7ddb19223 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolIntegrationTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolIntegrationTest.java @@ -28,10 +28,8 @@ import org.junit.Before; import org.junit.BeforeClass; import org.junit.ClassRule; -import org.junit.Rule; import org.junit.Test; import org.junit.experimental.categories.Category; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -48,7 +46,6 @@ public class SessionPoolIntegrationTest { private static final String TABLE_NAME = "TestTable"; private static Database db; - @Rule public ExpectedException expectedException = ExpectedException.none(); private SessionPool pool; @BeforeClass @@ -77,7 +74,7 @@ public static void setUpDatabase() { } @Before - public void setUp() throws Exception { + public void setUp() { SessionPoolOptions options = SessionPoolOptions.newBuilder().setMinSessions(1).setMaxSessions(2).build(); pool = diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolLeakTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolLeakTest.java index dbafb9dd01..2dc31bb28a 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolLeakTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolLeakTest.java @@ -72,7 +72,7 @@ public static void stopServer() throws InterruptedException { } @Before - public void setUp() throws Exception { + public void setUp() { mockSpanner.reset(); mockSpanner.removeAllExecutionTimes(); SpannerOptions.Builder builder = @@ -95,7 +95,7 @@ public void setUp() throws Exception { } @After - public void tearDown() throws Exception { + public void tearDown() { spanner.close(); } @@ -135,7 +135,7 @@ private void readWriteTransactionTest( .run( new TransactionCallable() { @Override - public Void run(TransactionContext transaction) throws Exception { + public Void run(TransactionContext transaction) { return null; } }); @@ -161,7 +161,7 @@ public void run() { } @Test - public void testTransactionManagerExceptionOnBegin() throws Exception { + public void testTransactionManagerExceptionOnBegin() { transactionManagerTest( new Runnable() { @Override 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 8d1b780432..0e72b2b9bc 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 @@ -25,6 +25,7 @@ import com.google.cloud.spanner.SessionClient.SessionConsumer; import com.google.cloud.spanner.SessionPool.PooledSession; +import com.google.cloud.spanner.SessionPool.PooledSessionFuture; import com.google.cloud.spanner.SessionPool.SessionConsumerImpl; import com.google.common.base.Function; import java.util.ArrayList; @@ -56,7 +57,7 @@ public class SessionPoolMaintainerTest extends BaseSessionPoolTest { private Map pingedSessions = new HashMap<>(); @Before - public void setUp() throws Exception { + public void setUp() { initMocks(this); when(client.getOptions()).thenReturn(spannerOptions); when(client.getSessionClient(db)).thenReturn(sessionClient); @@ -78,7 +79,7 @@ private void setupMockSessionCreation() { doAnswer( new Answer() { @Override - public Void answer(final InvocationOnMock invocation) throws Throwable { + public Void answer(final InvocationOnMock invocation) { executor.submit( new Runnable() { @Override @@ -107,7 +108,7 @@ private SessionImpl setupMockSession(final SessionImpl session) { .thenAnswer( new Answer() { @Override - public ResultSet answer(InvocationOnMock invocation) throws Throwable { + public ResultSet answer(InvocationOnMock invocation) { Integer currentValue = pingedSessions.get(session.getName()); if (currentValue == null) { currentValue = 0; @@ -200,7 +201,7 @@ public void testKeepAlive() throws Exception { assertThat(pingedSessions).containsExactly(session1.getName(), 2, session2.getName(), 3); // Update the last use date and release the session to the pool and do another maintenance // cycle. - ((PooledSession) session6).markUsed(); + ((PooledSessionFuture) session6).get().markUsed(); session6.close(); runMaintainanceLoop(clock, pool, 3); assertThat(pingedSessions).containsExactly(session1.getName(), 2, session2.getName(), 3); @@ -261,9 +262,9 @@ public void testIdleSessions() throws Exception { // Now check out three sessions so the pool will create an additional session. The pool will // only keep 2 sessions alive, as that is the setting for MinSessions. - Session session3 = pool.getReadSession(); - Session session4 = pool.getReadSession(); - Session session5 = pool.getReadSession(); + Session session3 = pool.getReadSession().get(); + Session session4 = pool.getReadSession().get(); + Session session5 = pool.getReadSession().get(); // Note that session2 was now the first session in the pool as it was the last to receive a // ping. assertThat(session3.getName()).isEqualTo(session2.getName()); @@ -278,9 +279,9 @@ public void testIdleSessions() throws Exception { assertThat(pool.totalSessions()).isEqualTo(2); // Check out three sessions again and keep one session checked out. - Session session6 = pool.getReadSession(); - Session session7 = pool.getReadSession(); - Session session8 = pool.getReadSession(); + Session session6 = pool.getReadSession().get(); + Session session7 = pool.getReadSession().get(); + Session session8 = pool.getReadSession().get(); session8.close(); session7.close(); // Now advance the clock to idle sessions. This should remove session8 from the pool. @@ -292,9 +293,9 @@ public void testIdleSessions() throws Exception { // Check out three sessions and keep them all checked out. No sessions should be removed from // the pool. - Session session9 = pool.getReadSession(); - Session session10 = pool.getReadSession(); - Session session11 = pool.getReadSession(); + Session session9 = pool.getReadSession().get(); + Session session10 = pool.getReadSession().get(); + Session session11 = pool.getReadSession().get(); runMaintainanceLoop(clock, pool, loopsToIdleSessions); assertThat(idledSessions).containsExactly(session5, session8); assertThat(pool.totalSessions()).isEqualTo(3); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolOptionsTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolOptionsTest.java index b529a2737f..9cdabfac68 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolOptionsTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolOptionsTest.java @@ -16,13 +16,13 @@ package com.google.cloud.spanner; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; import java.util.ArrayList; import java.util.Collection; import java.util.List; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameter; @@ -31,7 +31,6 @@ /** Unit tests for {@link com.google.cloud.spanner.SessionPoolOptions} */ @RunWith(Parameterized.class) public class SessionPoolOptionsTest { - @Rule public ExpectedException expectedException = ExpectedException.none(); @Parameter public int minSessions; @Parameter(1) @@ -49,17 +48,23 @@ public static Collection data() { @Test public void setMinMaxSessions() { - if (minSessions > maxSessions) { - expectedException.expect(IllegalArgumentException.class); + try { + SessionPoolOptions options = + SessionPoolOptions.newBuilder() + .setMinSessions(minSessions) + .setMaxSessions(maxSessions) + .build(); + if (minSessions > maxSessions) { + fail("Expected exception"); + } + assertThat(minSessions).isEqualTo(options.getMinSessions()); + assertThat(maxSessions).isEqualTo(options.getMaxSessions()); + } catch (IllegalArgumentException ex) { + if (minSessions <= maxSessions) { + throw ex; + } + assertNotNull(ex.getMessage()); } - SessionPoolOptions options = - SessionPoolOptions.newBuilder() - .setMinSessions(minSessions) - .setMaxSessions(maxSessions) - .build(); - - assertThat(minSessions).isEqualTo(options.getMinSessions()); - assertThat(maxSessions).isEqualTo(options.getMaxSessions()); } /** 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 b059e4f861..b806f5fad6 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 @@ -17,7 +17,7 @@ package com.google.cloud.spanner; import static com.google.common.truth.Truth.assertThat; -import static org.mockito.Mockito.any; +import static org.mockito.Matchers.any; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -26,9 +26,11 @@ import com.google.api.core.ApiFutures; import com.google.cloud.spanner.SessionClient.SessionConsumer; import com.google.cloud.spanner.SessionPool.PooledSession; +import com.google.cloud.spanner.SessionPool.PooledSessionFuture; import com.google.cloud.spanner.SessionPool.SessionConsumerImpl; import com.google.common.base.Function; import com.google.common.util.concurrent.Uninterruptibles; +import com.google.protobuf.ByteString; import com.google.protobuf.Empty; import java.util.ArrayList; import java.util.Collection; @@ -39,6 +41,8 @@ import java.util.Random; import java.util.Set; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import org.junit.Test; @@ -66,6 +70,7 @@ public class SessionPoolStressTest extends BaseSessionPoolTest { DatabaseId db = DatabaseId.of("projects/p/instances/i/databases/unused"); SessionPool pool; SessionPoolOptions options; + ExecutorService createExecutor = Executors.newSingleThreadExecutor(); Object lock = new Object(); Random random = new Random(); FakeClock clock = new FakeClock(); @@ -97,43 +102,31 @@ private void setupSpanner(DatabaseId db) { SessionClient sessionClient = mock(SessionClient.class); when(mockSpanner.getSessionClient(db)).thenReturn(sessionClient); when(mockSpanner.getOptions()).thenReturn(spannerOptions); - when(sessionClient.createSession()) - .thenAnswer( - new Answer() { - - @Override - public Session answer(InvocationOnMock invocation) throws Throwable { - synchronized (lock) { - SessionImpl session = mockSession(); - setupSession(session); - - sessions.put(session.getName(), false); - if (sessions.size() > maxAliveSessions) { - maxAliveSessions = sessions.size(); - } - return session; - } - } - }); doAnswer( new Answer() { @Override - public Void answer(InvocationOnMock invocation) throws Throwable { - int sessionCount = invocation.getArgumentAt(0, Integer.class); - for (int s = 0; s < sessionCount; s++) { - synchronized (lock) { - SessionImpl session = mockSession(); - setupSession(session); - - sessions.put(session.getName(), false); - if (sessions.size() > maxAliveSessions) { - maxAliveSessions = sessions.size(); - } - SessionConsumerImpl consumer = - invocation.getArgumentAt(2, SessionConsumerImpl.class); - consumer.onSessionReady(session); - } - } + public Void answer(final InvocationOnMock invocation) { + createExecutor.submit( + new Runnable() { + @Override + public void run() { + int sessionCount = invocation.getArgumentAt(0, Integer.class); + for (int s = 0; s < sessionCount; s++) { + SessionImpl session; + synchronized (lock) { + session = mockSession(); + setupSession(session); + sessions.put(session.getName(), false); + if (sessions.size() > maxAliveSessions) { + maxAliveSessions = sessions.size(); + } + } + SessionConsumerImpl consumer = + invocation.getArgumentAt(2, SessionConsumerImpl.class); + consumer.onSessionReady(session); + } + } + }); return null; } }) @@ -151,7 +144,7 @@ private void setupSession(final SessionImpl session) { new Answer() { @Override - public ResultSet answer(InvocationOnMock invocation) throws Throwable { + public ResultSet answer(InvocationOnMock invocation) { resetTransaction(session); return mockResult; } @@ -161,7 +154,7 @@ public ResultSet answer(InvocationOnMock invocation) throws Throwable { new Answer>() { @Override - public ApiFuture answer(InvocationOnMock invocation) throws Throwable { + public ApiFuture answer(InvocationOnMock invocation) { synchronized (lock) { if (expiredSessions.contains(session.getName())) { return ApiFutures.immediateFailedFuture( @@ -184,41 +177,48 @@ public ApiFuture answer(InvocationOnMock invocation) throws Throwable { doAnswer( new Answer() { @Override - public Void answer(InvocationOnMock invocation) throws Throwable { + public Void answer(InvocationOnMock invocation) { if (random.nextInt(100) < 10) { expireSession(session); throw SpannerExceptionFactoryTest.newSessionNotFoundException(session.getName()); } + String name = session.getName(); synchronized (lock) { - if (sessions.put(session.getName(), true)) { + if (sessions.put(name, true)) { setFailed(); } + session.readyTransactionId = ByteString.copyFromUtf8("foo"); } return null; } }) .when(session) .prepareReadWriteTransaction(); + when(session.hasReadyTransaction()).thenCallRealMethod(); } private void expireSession(Session session) { + String name = session.getName(); synchronized (lock) { - sessions.remove(session.getName()); - expiredSessions.add(session.getName()); + sessions.remove(name); + expiredSessions.add(name); } } private void assertWritePrepared(Session session) { + String name = session.getName(); synchronized (lock) { - if (!sessions.get(session.getName())) { + if (!sessions.containsKey(name) || !sessions.get(name)) { setFailed(); } } } - private void resetTransaction(Session session) { + private void resetTransaction(SessionImpl session) { + String name = session.getName(); synchronized (lock) { - sessions.put(session.getName(), false); + session.readyTransactionId = null; + sessions.put(name, false); } } @@ -264,8 +264,9 @@ public void stressTest() throws Exception { new Function() { @Override public Void apply(PooledSession pooled) { + String name = pooled.getName(); synchronized (lock) { - sessions.remove(pooled.getName()); + sessions.remove(name); return null; } } @@ -279,16 +280,18 @@ public void run() { Uninterruptibles.awaitUninterruptibly(releaseThreads); for (int j = 0; j < numOperationsPerThread; j++) { try { - Session session = null; + PooledSessionFuture session = null; if (random.nextInt(10) < writeOperationFraction) { session = pool.getReadWriteSession(); - assertWritePrepared(session); + PooledSession sess = session.get(); + assertWritePrepared(sess); } else { session = pool.getReadSession(); + session.get(); } Uninterruptibles.sleepUninterruptibly( random.nextInt(5), TimeUnit.MILLISECONDS); - resetTransaction(session); + resetTransaction(session.get().delegate); session.close(); } catch (SpannerException e) { if (e.getErrorCode() != ErrorCode.RESOURCE_EXHAUSTED || shouldBlock) { 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 395a60ade3..d5ea648bbd 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 @@ -22,8 +22,8 @@ import static com.google.cloud.spanner.MetricRegistryConstants.NUM_WRITE_SESSIONS; import static com.google.cloud.spanner.MetricRegistryConstants.SPANNER_LABEL_KEYS; import static com.google.cloud.spanner.MetricRegistryConstants.SPANNER_LABEL_KEYS_WITH_TYPE; -import static com.google.cloud.spanner.SpannerMatchers.isSpannerException; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.fail; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; @@ -47,6 +47,7 @@ import com.google.cloud.spanner.SessionClient.SessionConsumer; import com.google.cloud.spanner.SessionPool.Clock; import com.google.cloud.spanner.SessionPool.PooledSession; +import com.google.cloud.spanner.SessionPool.PooledSessionFuture; import com.google.cloud.spanner.SessionPool.SessionConsumerImpl; import com.google.cloud.spanner.SpannerImpl.ClosedException; import com.google.cloud.spanner.TransactionRunner.TransactionCallable; @@ -59,12 +60,14 @@ import com.google.protobuf.ByteString; import com.google.protobuf.Empty; import com.google.spanner.v1.CommitRequest; +import com.google.spanner.v1.CommitResponse; import com.google.spanner.v1.ExecuteBatchDmlRequest; import com.google.spanner.v1.ExecuteSqlRequest; import com.google.spanner.v1.ResultSetStats; import com.google.spanner.v1.RollbackRequest; import io.opencensus.metrics.LabelValue; import io.opencensus.metrics.MetricRegistry; +import io.opencensus.trace.Span; import java.io.PrintWriter; import java.io.StringWriter; import java.util.ArrayList; @@ -81,9 +84,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameter; @@ -96,7 +97,6 @@ /** Tests for SessionPool that mock out the underlying stub. */ @RunWith(Parameterized.class) public class SessionPoolTest extends BaseSessionPoolTest { - @Rule public ExpectedException expectedException = ExpectedException.none(); private final ExecutorService executor = Executors.newSingleThreadExecutor(); @Parameter public int minSessions; @@ -135,7 +135,7 @@ private SessionPool createPool( } @Before - public void setUp() throws Exception { + public void setUp() { initMocks(this); when(client.getOptions()).thenReturn(spannerOptions); when(client.getSessionClient(db)).thenReturn(sessionClient); @@ -153,7 +153,7 @@ private void setupMockSessionCreation() { doAnswer( new Answer() { @Override - public Void answer(final InvocationOnMock invocation) throws Throwable { + public Void answer(final InvocationOnMock invocation) { executor.submit( new Runnable() { @Override @@ -207,21 +207,21 @@ public void sessionCreation() { public void poolLifo() { setupMockSessionCreation(); pool = createPool(); - Session session1 = pool.getReadSession(); - Session session2 = pool.getReadSession(); + Session session1 = pool.getReadSession().get(); + Session session2 = pool.getReadSession().get(); assertThat(session1).isNotEqualTo(session2); session2.close(); session1.close(); - Session session3 = pool.getReadSession(); - Session session4 = pool.getReadSession(); + Session session3 = pool.getReadSession().get(); + Session session4 = pool.getReadSession().get(); assertThat(session3).isEqualTo(session1); assertThat(session4).isEqualTo(session2); session3.close(); session4.close(); - Session session5 = pool.getReadWriteSession(); - Session session6 = pool.getReadWriteSession(); + Session session5 = pool.getReadWriteSession().get(); + Session session6 = pool.getReadWriteSession().get(); assertThat(session5).isEqualTo(session4); assertThat(session6).isEqualTo(session3); session6.close(); @@ -244,7 +244,7 @@ public void poolClosureClosesLeakedSessions() throws Exception { doAnswer( new Answer() { @Override - public Void answer(final InvocationOnMock invocation) throws Throwable { + public Void answer(final InvocationOnMock invocation) { executor.submit( new Runnable() { @Override @@ -262,7 +262,7 @@ public void run() { pool = createPool(); Session session1 = pool.getReadSession(); // Leaked sessions - PooledSession leakedSession = pool.getReadSession(); + PooledSessionFuture leakedSession = pool.getReadSession(); // Clear the leaked exception to suppress logging of expected exceptions. leakedSession.clearLeakedException(); session1.close(); @@ -302,7 +302,7 @@ public void poolClosureFailsPendingReadWaiters() throws Exception { doAnswer( new Answer() { @Override - public Void answer(final InvocationOnMock invocation) throws Throwable { + public Void answer(final InvocationOnMock invocation) { executor.submit( new Runnable() { @Override @@ -318,7 +318,7 @@ public void run() { .doAnswer( new Answer() { @Override - public Void answer(final InvocationOnMock invocation) throws Throwable { + public Void answer(final InvocationOnMock invocation) { executor.submit( new Callable() { @Override @@ -338,7 +338,7 @@ public Void call() throws Exception { .asyncBatchCreateSessions(Mockito.eq(1), Mockito.anyBoolean(), any(SessionConsumer.class)); pool = createPool(); - PooledSession leakedSession = pool.getReadSession(); + PooledSessionFuture leakedSession = pool.getReadSession(); // Suppress expected leakedSession warning. leakedSession.clearLeakedException(); AtomicBoolean failed = new AtomicBoolean(false); @@ -347,7 +347,7 @@ public Void call() throws Exception { insideCreation.await(); pool.closeAsync(new SpannerImpl.ClosedException()); releaseCreation.countDown(); - latch.await(); + latch.await(5L, TimeUnit.SECONDS); assertThat(failed.get()).isTrue(); } @@ -360,7 +360,7 @@ public void poolClosureFailsPendingWriteWaiters() throws Exception { doAnswer( new Answer() { @Override - public Void answer(final InvocationOnMock invocation) throws Throwable { + public Void answer(final InvocationOnMock invocation) { executor.submit( new Runnable() { @Override @@ -376,7 +376,7 @@ public void run() { .doAnswer( new Answer() { @Override - public Void answer(final InvocationOnMock invocation) throws Throwable { + public Void answer(final InvocationOnMock invocation) { executor.submit( new Callable() { @Override @@ -396,7 +396,7 @@ public Void call() throws Exception { .asyncBatchCreateSessions(Mockito.eq(1), Mockito.anyBoolean(), any(SessionConsumer.class)); pool = createPool(); - PooledSession leakedSession = pool.getReadSession(); + PooledSessionFuture leakedSession = pool.getReadSession(); // Suppress expected leakedSession warning. leakedSession.clearLeakedException(); AtomicBoolean failed = new AtomicBoolean(false); @@ -416,7 +416,7 @@ public void poolClosesEvenIfCreationFails() throws Exception { doAnswer( new Answer() { @Override - public Void answer(final InvocationOnMock invocation) throws Throwable { + public Void answer(final InvocationOnMock invocation) { executor.submit( new Callable() { @Override @@ -452,7 +452,7 @@ public void poolClosesEvenIfPreparationFails() throws Exception { doAnswer( new Answer() { @Override - public Void answer(final InvocationOnMock invocation) throws Throwable { + public Void answer(final InvocationOnMock invocation) { executor.submit( new Runnable() { @Override @@ -492,12 +492,12 @@ public Session answer(InvocationOnMock invocation) throws Throwable { } @Test - public void poolClosureFailsNewRequests() throws Exception { + public void poolClosureFailsNewRequests() { final SessionImpl session = mockSession(); doAnswer( new Answer() { @Override - public Void answer(final InvocationOnMock invocation) throws Throwable { + public Void answer(final InvocationOnMock invocation) { executor.submit( new Runnable() { @Override @@ -513,12 +513,17 @@ public void run() { .when(sessionClient) .asyncBatchCreateSessions(Mockito.eq(1), Mockito.anyBoolean(), any(SessionConsumer.class)); pool = createPool(); - PooledSession leakedSession = pool.getReadSession(); + PooledSessionFuture leakedSession = pool.getReadSession(); + leakedSession.get(); // Suppress expected leakedSession warning. leakedSession.clearLeakedException(); pool.closeAsync(new SpannerImpl.ClosedException()); - expectedException.expect(IllegalStateException.class); - pool.getReadSession(); + try { + pool.getReadSession(); + fail("Expected exception"); + } catch (IllegalStateException ex) { + assertNotNull(ex.getMessage()); + } } @Test @@ -542,11 +547,11 @@ public void creationExceptionPropagatesToReadSession() { doAnswer( new Answer() { @Override - public Void answer(final InvocationOnMock invocation) throws Throwable { + public Void answer(final InvocationOnMock invocation) { executor.submit( new Callable() { @Override - public Void call() throws Exception { + public Void call() { SessionConsumerImpl consumer = invocation.getArgumentAt(2, SessionConsumerImpl.class); consumer.onSessionCreateFailure( @@ -560,8 +565,12 @@ public Void call() throws Exception { .when(sessionClient) .asyncBatchCreateSessions(Mockito.eq(1), Mockito.anyBoolean(), any(SessionConsumer.class)); pool = createPool(); - expectedException.expect(isSpannerException(ErrorCode.INTERNAL)); - pool.getReadSession(); + try { + pool.getReadSession().get(); + fail("Expected exception"); + } catch (SpannerException ex) { + assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.INTERNAL); + } } @Test @@ -569,11 +578,11 @@ public void creationExceptionPropagatesToReadWriteSession() { doAnswer( new Answer() { @Override - public Void answer(final InvocationOnMock invocation) throws Throwable { + public Void answer(final InvocationOnMock invocation) { executor.submit( new Callable() { @Override - public Void call() throws Exception { + public Void call() { SessionConsumerImpl consumer = invocation.getArgumentAt(2, SessionConsumerImpl.class); consumer.onSessionCreateFailure( @@ -587,8 +596,12 @@ public Void call() throws Exception { .when(sessionClient) .asyncBatchCreateSessions(Mockito.eq(1), Mockito.anyBoolean(), any(SessionConsumer.class)); pool = createPool(); - expectedException.expect(isSpannerException(ErrorCode.INTERNAL)); - pool.getReadWriteSession(); + try { + pool.getReadWriteSession().get(); + fail("Expected exception"); + } catch (SpannerException ex) { + assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.INTERNAL); + } } @Test @@ -597,7 +610,7 @@ public void prepareExceptionPropagatesToReadWriteSession() { doAnswer( new Answer() { @Override - public Void answer(final InvocationOnMock invocation) throws Throwable { + public Void answer(final InvocationOnMock invocation) { executor.submit( new Runnable() { @Override @@ -616,8 +629,12 @@ public void run() { .when(session) .prepareReadWriteTransaction(); pool = createPool(); - expectedException.expect(isSpannerException(ErrorCode.INTERNAL)); - pool.getReadWriteSession(); + try { + pool.getReadWriteSession().get(); + fail("Expected exception"); + } catch (SpannerException ex) { + assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.INTERNAL); + } } @Test @@ -626,7 +643,7 @@ public void getReadWriteSession() { doAnswer( new Answer() { @Override - public Void answer(final InvocationOnMock invocation) throws Throwable { + public Void answer(final InvocationOnMock invocation) { executor.submit( new Runnable() { @Override @@ -642,14 +659,15 @@ public void run() { .when(sessionClient) .asyncBatchCreateSessions(Mockito.eq(1), Mockito.anyBoolean(), any(SessionConsumer.class)); pool = createPool(); - try (Session session = pool.getReadWriteSession()) { + try (PooledSessionFuture session = pool.getReadWriteSession()) { assertThat(session).isNotNull(); + session.get(); verify(mockSession).prepareReadWriteTransaction(); } } @Test - public void getMultipleReadWriteSessions() { + public void getMultipleReadWriteSessions() throws Exception { SessionImpl mockSession1 = mockSession(); SessionImpl mockSession2 = mockSession(); final LinkedList sessions = @@ -657,7 +675,7 @@ public void getMultipleReadWriteSessions() { doAnswer( new Answer() { @Override - public Void answer(final InvocationOnMock invocation) throws Throwable { + public Void answer(final InvocationOnMock invocation) { executor.submit( new Runnable() { @Override @@ -673,8 +691,10 @@ public void run() { .when(sessionClient) .asyncBatchCreateSessions(Mockito.eq(1), Mockito.anyBoolean(), any(SessionConsumer.class)); pool = createPool(); - Session session1 = pool.getReadWriteSession(); - Session session2 = pool.getReadWriteSession(); + PooledSessionFuture session1 = pool.getReadWriteSession(); + PooledSessionFuture session2 = pool.getReadWriteSession(); + session1.get(); + session2.get(); verify(mockSession1).prepareReadWriteTransaction(); verify(mockSession2).prepareReadWriteTransaction(); session1.close(); @@ -688,7 +708,7 @@ public void getMultipleConcurrentReadWriteSessions() { doAnswer( new Answer() { @Override - public Void answer(final InvocationOnMock invocation) throws Throwable { + public Void answer(final InvocationOnMock invocation) { executor.submit( new Runnable() { @Override @@ -722,7 +742,7 @@ public void sessionIsPrePrepared() { new Answer() { @Override - public Void answer(InvocationOnMock arg0) throws Throwable { + public Void answer(InvocationOnMock arg0) { prepareLatch.countDown(); return null; } @@ -733,7 +753,7 @@ public Void answer(InvocationOnMock arg0) throws Throwable { new Answer() { @Override - public Void answer(InvocationOnMock arg0) throws Throwable { + public Void answer(InvocationOnMock arg0) { prepareLatch.countDown(); return null; } @@ -743,7 +763,7 @@ public Void answer(InvocationOnMock arg0) throws Throwable { doAnswer( new Answer() { @Override - public Void answer(final InvocationOnMock invocation) throws Throwable { + public Void answer(final InvocationOnMock invocation) { executor.submit( new Runnable() { @Override @@ -769,8 +789,8 @@ public void run() { pool = createPool(); // One of the sessions would be pre prepared. Uninterruptibles.awaitUninterruptibly(prepareLatch); - PooledSession readSession = pool.getReadSession(); - PooledSession writeSession = pool.getReadWriteSession(); + PooledSession readSession = pool.getReadSession().get(); + PooledSession writeSession = pool.getReadWriteSession().get(); verify(writeSession.delegate, times(1)).prepareReadWriteTransaction(); verify(readSession.delegate, never()).prepareReadWriteTransaction(); readSession.close(); @@ -784,7 +804,7 @@ public void getReadSessionFallsBackToWritePreparedSession() throws Exception { doAnswer( new Answer() { @Override - public Void answer(InvocationOnMock arg0) throws Throwable { + public Void answer(InvocationOnMock arg0) { prepareLatch.countDown(); return null; } @@ -794,7 +814,7 @@ public Void answer(InvocationOnMock arg0) throws Throwable { doAnswer( new Answer() { @Override - public Void answer(final InvocationOnMock invocation) throws Throwable { + public Void answer(final InvocationOnMock invocation) { executor.submit( new Runnable() { @Override @@ -819,7 +839,7 @@ public void run() { pool.getReadWriteSession().close(); prepareLatch.await(); // This session should also be write prepared. - PooledSession readSession = pool.getReadSession(); + PooledSession readSession = pool.getReadSession().get(); verify(readSession.delegate, times(2)).prepareReadWriteTransaction(); } @@ -834,7 +854,7 @@ public void failOnPoolExhaustion() { doAnswer( new Answer() { @Override - public Void answer(final InvocationOnMock invocation) throws Throwable { + public Void answer(final InvocationOnMock invocation) { executor.submit( new Runnable() { @Override @@ -851,8 +871,12 @@ public void run() { .asyncBatchCreateSessions(Mockito.eq(1), Mockito.anyBoolean(), any(SessionConsumer.class)); pool = createPool(); Session session1 = pool.getReadSession(); - expectedException.expect(isSpannerException(ErrorCode.RESOURCE_EXHAUSTED)); - pool.getReadSession(); + try { + pool.getReadSession(); + fail("Expected exception"); + } catch (SpannerException ex) { + assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.RESOURCE_EXHAUSTED); + } session1.close(); session1 = pool.getReadSession(); assertThat(session1).isNotNull(); @@ -871,7 +895,7 @@ public void poolWorksWhenSessionNotFound() { doAnswer( new Answer() { @Override - public Void answer(final InvocationOnMock invocation) throws Throwable { + public Void answer(final InvocationOnMock invocation) { executor.submit( new Runnable() { @Override @@ -887,7 +911,7 @@ public void run() { .when(sessionClient) .asyncBatchCreateSessions(Mockito.eq(1), Mockito.anyBoolean(), any(SessionConsumer.class)); pool = createPool(); - assertThat(pool.getReadWriteSession().delegate).isEqualTo(mockSession2); + assertThat(pool.getReadWriteSession().get().delegate).isEqualTo(mockSession2); } @Test @@ -907,7 +931,7 @@ public void idleSessionCleanup() throws Exception { doAnswer( new Answer() { @Override - public Void answer(final InvocationOnMock invocation) throws Throwable { + public Void answer(final InvocationOnMock invocation) { executor.submit( new Runnable() { @Override @@ -932,9 +956,14 @@ public void run() { pool.getReadSession().close(); runMaintainanceLoop(clock, pool, pool.poolMaintainer.numClosureCycles); assertThat(pool.numIdleSessionsRemoved()).isEqualTo(0L); - Session readSession1 = pool.getReadSession(); - Session readSession2 = pool.getReadSession(); - Session readSession3 = pool.getReadSession(); + PooledSessionFuture readSession1 = pool.getReadSession(); + PooledSessionFuture readSession2 = pool.getReadSession(); + PooledSessionFuture readSession3 = pool.getReadSession(); + // Wait until the sessions have actually been gotten in order to make sure they are in use in + // parallel. + readSession1.get(); + readSession2.get(); + readSession3.get(); readSession1.close(); readSession2.close(); readSession3.close(); @@ -967,7 +996,7 @@ public void keepAlive() throws Exception { doAnswer( new Answer() { @Override - public Void answer(final InvocationOnMock invocation) throws Throwable { + public Void answer(final InvocationOnMock invocation) { executor.submit( new Runnable() { @Override @@ -988,8 +1017,10 @@ public void run() { FakeClock clock = new FakeClock(); clock.currentTimeMillis = System.currentTimeMillis(); pool = createPool(clock); - Session session1 = pool.getReadSession(); - Session session2 = pool.getReadSession(); + PooledSessionFuture session1 = pool.getReadSession(); + PooledSessionFuture session2 = pool.getReadSession(); + session1.get(); + session2.get(); session1.close(); session2.close(); runMaintainanceLoop(clock, pool, pool.poolMaintainer.numKeepAliveCycles); @@ -1023,7 +1054,7 @@ public void testMaintainerKeepsWriteProportion() throws Exception { doAnswer( new Answer() { @Override - public Void answer(final InvocationOnMock invocation) throws Throwable { + public Void answer(final InvocationOnMock invocation) { executor.submit( new Runnable() { @Override @@ -1058,9 +1089,7 @@ public void run() { verify(session, times(options.getMinSessions())).singleUse(any(TimestampBound.class)); // Verify that all sessions are still in the pool, and that the write fraction is maintained. assertThat(pool.getNumberOfSessionsInPool()).isEqualTo(options.getMinSessions()); - assertThat( - pool.getNumberOfAvailableWritePreparedSessions() - + pool.getNumberOfSessionsBeingPrepared()) + assertThat(pool.getNumberOfWriteSessionsInPool()) .isEqualTo( (int) Math.ceil(pool.getNumberOfSessionsInPool() * options.getWriteSessionsFraction())); @@ -1118,7 +1147,8 @@ public void blockAndTimeoutOnPoolExhaustion() throws Exception { setupMockSessionCreation(); pool = createPool(); // Take the only session that can be in the pool. - Session checkedOutSession = pool.getReadSession(); + PooledSessionFuture checkedOutSession = pool.getReadSession(); + checkedOutSession.get(); final Boolean finWrite = write; ExecutorService executor = Executors.newFixedThreadPool(1); final CountDownLatch latch = new CountDownLatch(1); @@ -1127,8 +1157,8 @@ public void blockAndTimeoutOnPoolExhaustion() throws Exception { executor.submit( new Callable() { @Override - public Void call() throws Exception { - Session session; + public Void call() { + PooledSessionFuture session; latch.countDown(); if (finWrite) { session = pool.getReadWriteSession(); @@ -1183,7 +1213,7 @@ public void testSessionNotFoundSingleUse() { doAnswer( new Answer() { @Override - public Void answer(final InvocationOnMock invocation) throws Throwable { + public Void answer(final InvocationOnMock invocation) { executor.submit( new Runnable() { @Override @@ -1199,7 +1229,7 @@ public void run() { .doAnswer( new Answer() { @Override - public Void answer(final InvocationOnMock invocation) throws Throwable { + public Void answer(final InvocationOnMock invocation) { executor.submit( new Runnable() { @Override @@ -1239,7 +1269,7 @@ public void testSessionNotFoundReadOnlyTransaction() { doAnswer( new Answer() { @Override - public Void answer(final InvocationOnMock invocation) throws Throwable { + public Void answer(final InvocationOnMock invocation) { executor.submit( new Runnable() { @Override @@ -1255,7 +1285,7 @@ public void run() { .doAnswer( new Answer() { @Override - public Void answer(final InvocationOnMock invocation) throws Throwable { + public Void answer(final InvocationOnMock invocation) { executor.submit( new Runnable() { @Override @@ -1311,7 +1341,8 @@ public void testSessionNotFoundReadWriteTransaction() { .thenThrow(sessionNotFound); when(rpc.executeBatchDml(any(ExecuteBatchDmlRequest.class), any(Map.class))) .thenThrow(sessionNotFound); - when(rpc.commit(any(CommitRequest.class), any(Map.class))).thenThrow(sessionNotFound); + when(rpc.commitAsync(any(CommitRequest.class), any(Map.class))) + .thenReturn(ApiFutures.immediateFailedFuture(sessionNotFound)); doThrow(sessionNotFound).when(rpc).rollback(any(RollbackRequest.class), any(Map.class)); final SessionImpl closedSession = mock(SessionImpl.class); when(closedSession.getName()) @@ -1327,9 +1358,10 @@ public void testSessionNotFoundReadWriteTransaction() { when(closedSession.asyncClose()) .thenReturn(ApiFutures.immediateFuture(Empty.getDefaultInstance())); when(closedSession.newTransaction()).thenReturn(closedTransactionContext); - when(closedSession.beginTransaction()).thenThrow(sessionNotFound); + when(closedSession.beginTransactionAsync()).thenThrow(sessionNotFound); TransactionRunnerImpl closedTransactionRunner = new TransactionRunnerImpl(closedSession, rpc, 10); + closedTransactionRunner.setSpan(mock(Span.class)); when(closedSession.readWriteTransaction()).thenReturn(closedTransactionRunner); final SessionImpl openSession = mock(SessionImpl.class); @@ -1339,9 +1371,11 @@ public void testSessionNotFoundReadWriteTransaction() { .thenReturn("projects/dummy/instances/dummy/database/dummy/sessions/session-open"); final TransactionContextImpl openTransactionContext = mock(TransactionContextImpl.class); when(openSession.newTransaction()).thenReturn(openTransactionContext); - when(openSession.beginTransaction()).thenReturn(ByteString.copyFromUtf8("open-txn")); + when(openSession.beginTransactionAsync()) + .thenReturn(ApiFutures.immediateFuture(ByteString.copyFromUtf8("open-txn"))); TransactionRunnerImpl openTransactionRunner = new TransactionRunnerImpl(openSession, mock(SpannerRpc.class), 10); + openTransactionRunner.setSpan(mock(Span.class)); when(openSession.readWriteTransaction()).thenReturn(openTransactionRunner); ResultSet openResultSet = mock(ResultSet.class); @@ -1361,7 +1395,7 @@ public void testSessionNotFoundReadWriteTransaction() { doAnswer( new Answer() { @Override - public Void answer(final InvocationOnMock invocation) throws Throwable { + public Void answer(final InvocationOnMock invocation) { executor.submit( new Runnable() { @Override @@ -1377,7 +1411,7 @@ public void run() { .doAnswer( new Answer() { @Override - public Void answer(final InvocationOnMock invocation) throws Throwable { + public Void answer(final InvocationOnMock invocation) { executor.submit( new Runnable() { @Override @@ -1407,7 +1441,7 @@ public void run() { SessionPool pool = SessionPool.createPool( options, new TestExecutorFactory(), spanner.getSessionClient(db)); - try (PooledSession readWriteSession = pool.getReadWriteSession()) { + try (PooledSessionFuture readWriteSession = pool.getReadWriteSession()) { TransactionRunner runner = readWriteSession.readWriteTransaction(); try { runner.run( @@ -1415,7 +1449,7 @@ public void run() { private int callNumber = 0; @Override - public Integer run(TransactionContext transaction) throws Exception { + public Integer run(TransactionContext transaction) { callNumber++; if (hasPreparedTransaction) { // If the session had a prepared read/write transaction, that transaction will @@ -1497,7 +1531,7 @@ public void testSessionNotFoundOnPrepareTransaction() { doAnswer( new Answer() { @Override - public Void answer(final InvocationOnMock invocation) throws Throwable { + public Void answer(final InvocationOnMock invocation) { executor.submit( new Runnable() { @Override @@ -1513,7 +1547,7 @@ public void run() { .doAnswer( new Answer() { @Override - public Void answer(final InvocationOnMock invocation) throws Throwable { + public Void answer(final InvocationOnMock invocation) { executor.submit( new Runnable() { @Override @@ -1531,7 +1565,7 @@ public void run() { FakeClock clock = new FakeClock(); clock.currentTimeMillis = System.currentTimeMillis(); pool = createPool(clock); - PooledSession session = pool.getReadWriteSession(); + PooledSession session = pool.getReadWriteSession().get(); assertThat(session.delegate).isEqualTo(openSession); } @@ -1548,7 +1582,7 @@ public void testSessionNotFoundWrite() { doAnswer( new Answer() { @Override - public Void answer(final InvocationOnMock invocation) throws Throwable { + public Void answer(final InvocationOnMock invocation) { executor.submit( new Runnable() { @Override @@ -1564,7 +1598,7 @@ public void run() { .doAnswer( new Answer() { @Override - public Void answer(final InvocationOnMock invocation) throws Throwable { + public Void answer(final InvocationOnMock invocation) { executor.submit( new Runnable() { @Override @@ -1600,7 +1634,7 @@ public void testSessionNotFoundWriteAtLeastOnce() { doAnswer( new Answer() { @Override - public Void answer(final InvocationOnMock invocation) throws Throwable { + public Void answer(final InvocationOnMock invocation) { executor.submit( new Runnable() { @Override @@ -1616,7 +1650,7 @@ public void run() { .doAnswer( new Answer() { @Override - public Void answer(final InvocationOnMock invocation) throws Throwable { + public Void answer(final InvocationOnMock invocation) { executor.submit( new Runnable() { @Override @@ -1651,7 +1685,7 @@ public void testSessionNotFoundPartitionedUpdate() { doAnswer( new Answer() { @Override - public Void answer(final InvocationOnMock invocation) throws Throwable { + public Void answer(final InvocationOnMock invocation) { executor.submit( new Runnable() { @Override @@ -1667,7 +1701,7 @@ public void run() { .doAnswer( new Answer() { @Override - public Void answer(final InvocationOnMock invocation) throws Throwable { + public Void answer(final InvocationOnMock invocation) { executor.submit( new Runnable() { @Override @@ -1711,8 +1745,10 @@ public void testSessionMetrics() throws Exception { setupMockSessionCreation(); pool = createPool(clock, metricRegistry, labelValues); - Session session1 = pool.getReadSession(); - Session session2 = pool.getReadSession(); + PooledSessionFuture session1 = pool.getReadSession(); + PooledSessionFuture session2 = pool.getReadSession(); + session1.get(); + session2.get(); MetricsRecord record = metricRegistry.pollRecord(); assertThat(record.getMetrics().size()).isEqualTo(6); @@ -1846,7 +1882,8 @@ private void getSessionAsync(final CountDownLatch latch, final AtomicBoolean fai new Runnable() { @Override public void run() { - try (Session session = pool.getReadSession()) { + try (PooledSessionFuture future = pool.getReadSession()) { + PooledSession session = future.get(); failed.compareAndSet(false, session == null); Uninterruptibles.sleepUninterruptibly(10, TimeUnit.MILLISECONDS); } catch (Throwable e) { @@ -1864,7 +1901,8 @@ private void getReadWriteSessionAsync(final CountDownLatch latch, final AtomicBo new Runnable() { @Override public void run() { - try (Session session = pool.getReadWriteSession()) { + try (PooledSessionFuture future = pool.getReadWriteSession()) { + PooledSession session = future.get(); failed.compareAndSet(false, session == null); Uninterruptibles.sleepUninterruptibly(2, TimeUnit.MILLISECONDS); } catch (SpannerException e) { 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 eb1bb67e89..7dcc9b65e1 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 @@ -44,10 +44,8 @@ import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; -import org.junit.Rule; import org.junit.Test; import org.junit.experimental.categories.Category; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.threeten.bp.Duration; @@ -119,8 +117,6 @@ public class SpanTest { .withDescription("Non-retryable test exception.") .asRuntimeException(); - @Rule public ExpectedException expectedException = ExpectedException.none(); - @BeforeClass public static void startStaticServer() throws Exception { mockSpanner = new MockSpannerServiceImpl(); @@ -231,23 +227,29 @@ public void tearDown() { @Test public void singleUseNonRetryableErrorOnNext() { - expectedException.expect(SpannerMatchers.isSpannerException(ErrorCode.FAILED_PRECONDITION)); try (ResultSet rs = client.singleUse().executeQuery(SELECT1AND2)) { mockSpanner.addException(FAILED_PRECONDITION); while (rs.next()) { // Just consume the result set. + fail("Expected exception"); } + fail("Expected exception"); + } catch (SpannerException ex) { + assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.FAILED_PRECONDITION); } } @Test public void singleUseExecuteStreamingSqlTimeout() { - expectedException.expect(SpannerMatchers.isSpannerException(ErrorCode.DEADLINE_EXCEEDED)); try (ResultSet rs = clientWithTimeout.singleUse().executeQuery(SELECT1AND2)) { mockSpanner.setExecuteStreamingSqlExecutionTime(ONE_SECOND); while (rs.next()) { // Just consume the result set. + fail("Expected exception"); } + fail("Expected exception"); + } catch (SpannerException ex) { + assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.DEADLINE_EXCEEDED); } } @@ -290,7 +292,7 @@ public void transactionRunner() { runner.run( new TransactionCallable() { @Override - public Void run(TransactionContext transaction) throws Exception { + public Void run(TransactionContext transaction) { transaction.executeUpdate(UPDATE_STATEMENT); return null; } @@ -311,7 +313,7 @@ public void transactionRunnerWithError() { runner.run( new TransactionCallable() { @Override - public Void run(TransactionContext transaction) throws Exception { + public Void run(TransactionContext transaction) { transaction.executeUpdate(INVALID_UPDATE_STATEMENT); return null; } @@ -322,6 +324,7 @@ public Void run(TransactionContext transaction) throws Exception { } Map spans = failOnOverkillTraceComponent.getSpans(); + assertThat(spans.size()).isEqualTo(5); assertThat(spans).containsEntry("CloudSpanner.ReadWriteTransaction", true); assertThat(spans).containsEntry("CloudSpannerOperation.BatchCreateSessions", true); assertThat(spans).containsEntry("SessionPool.WaitForSession", true); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerExceptionFactoryTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerExceptionFactoryTest.java index bc7dd5498d..49cbfb905d 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerExceptionFactoryTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerExceptionFactoryTest.java @@ -52,6 +52,11 @@ static DatabaseNotFoundException newDatabaseNotFoundException(String name) { "Database", SpannerExceptionFactory.DATABASE_RESOURCE_TYPE, name); } + static StatusRuntimeException newStatusDatabaseNotFoundException(String name) { + return newStatusResourceNotFoundException( + "Database", SpannerExceptionFactory.DATABASE_RESOURCE_TYPE, name); + } + static InstanceNotFoundException newInstanceNotFoundException(String name) { return (InstanceNotFoundException) newResourceNotFoundException( diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerGaxRetryTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerGaxRetryTest.java index 54d7ad5a12..b98702f87c 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerGaxRetryTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerGaxRetryTest.java @@ -19,6 +19,7 @@ import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; import com.google.api.core.ApiFunction; @@ -45,9 +46,7 @@ import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.threeten.bp.Duration; @@ -101,8 +100,6 @@ public class SpannerGaxRetryTest { private Spanner spannerWithTimeout; private DatabaseClient clientWithTimeout; - @Rule public ExpectedException expectedException = ExpectedException.none(); - @BeforeClass public static void startStaticServer() throws IOException { mockSpanner = new MockSpannerServiceImpl(); @@ -189,7 +186,7 @@ public Void apply(Builder input) { } @After - public void tearDown() throws Exception { + public void tearDown() { spannerWithTimeout.close(); spanner.close(); } @@ -204,7 +201,7 @@ private void warmUpSessionPool(DatabaseClient client) { runner.run( new TransactionCallable() { @Override - public Long run(TransactionContext transaction) throws Exception { + public Long run(TransactionContext transaction) { return transaction.executeUpdate(UPDATE_STATEMENT); } }); @@ -224,10 +221,13 @@ public Long run(TransactionContext transaction) throws Exception { @Test public void singleUseTimeout() { - expectedException.expect(SpannerMatchers.isSpannerException(ErrorCode.DEADLINE_EXCEEDED)); mockSpanner.setBatchCreateSessionsExecutionTime(ONE_SECOND); try (ResultSet rs = clientWithTimeout.singleUse().executeQuery(SELECT1AND2)) { - while (rs.next()) {} + while (rs.next()) { + fail("Expected exception"); + } + } catch (SpannerException ex) { + assertEquals(ErrorCode.DEADLINE_EXCEEDED, ex.getErrorCode()); } } @@ -241,38 +241,50 @@ public void singleUseUnavailable() { @Test public void singleUseNonRetryableError() { - expectedException.expect(SpannerMatchers.isSpannerException(ErrorCode.FAILED_PRECONDITION)); mockSpanner.addException(FAILED_PRECONDITION); try (ResultSet rs = client.singleUse().executeQuery(SELECT1AND2)) { - while (rs.next()) {} + while (rs.next()) { + fail("Expected exception"); + } + } catch (SpannerException ex) { + assertEquals(ErrorCode.FAILED_PRECONDITION, ex.getErrorCode()); } } @Test public void singleUseNonRetryableErrorOnNext() { - expectedException.expect(SpannerMatchers.isSpannerException(ErrorCode.FAILED_PRECONDITION)); try (ResultSet rs = client.singleUse().executeQuery(SELECT1AND2)) { mockSpanner.addException(FAILED_PRECONDITION); - while (rs.next()) {} + while (rs.next()) { + fail("Expected exception"); + } + } catch (SpannerException ex) { + assertEquals(ErrorCode.FAILED_PRECONDITION, ex.getErrorCode()); } } @Test public void singleUseInternal() { - expectedException.expect(SpannerMatchers.isSpannerException(ErrorCode.INTERNAL)); mockSpanner.addException(new IllegalArgumentException()); try (ResultSet rs = client.singleUse().executeQuery(SELECT1AND2)) { - while (rs.next()) {} + while (rs.next()) { + fail("Expected exception"); + } + } catch (SpannerException ex) { + assertEquals(ErrorCode.INTERNAL, ex.getErrorCode()); } } @Test public void singleUseReadOnlyTransactionTimeout() { - expectedException.expect(SpannerMatchers.isSpannerException(ErrorCode.DEADLINE_EXCEEDED)); mockSpanner.setBatchCreateSessionsExecutionTime(ONE_SECOND); try (ResultSet rs = clientWithTimeout.singleUseReadOnlyTransaction().executeQuery(SELECT1AND2)) { - while (rs.next()) {} + while (rs.next()) { + fail("Expected exception"); + } + } catch (SpannerException ex) { + assertEquals(ErrorCode.DEADLINE_EXCEEDED, ex.getErrorCode()); } } @@ -286,10 +298,13 @@ public void singleUseReadOnlyTransactionUnavailable() { @Test public void singleUseExecuteStreamingSqlTimeout() { - expectedException.expect(SpannerMatchers.isSpannerException(ErrorCode.DEADLINE_EXCEEDED)); try (ResultSet rs = clientWithTimeout.singleUse().executeQuery(SELECT1AND2)) { mockSpanner.setExecuteStreamingSqlExecutionTime(ONE_SECOND); - while (rs.next()) {} + while (rs.next()) { + fail("Expected exception"); + } + } catch (SpannerException ex) { + assertEquals(ErrorCode.DEADLINE_EXCEEDED, ex.getErrorCode()); } } @@ -303,18 +318,20 @@ public void singleUseExecuteStreamingSqlUnavailable() { @Test public void readWriteTransactionTimeout() { - expectedException.expect(SpannerMatchers.isSpannerException(ErrorCode.DEADLINE_EXCEEDED)); mockSpanner.setBeginTransactionExecutionTime(ONE_SECOND); - TransactionRunner runner = clientWithTimeout.readWriteTransaction(); - long updateCount = - runner.run( - new TransactionCallable() { - @Override - public Long run(TransactionContext transaction) throws Exception { - return transaction.executeUpdate(UPDATE_STATEMENT); - } - }); - assertThat(updateCount, is(equalTo(UPDATE_COUNT))); + try { + TransactionRunner runner = clientWithTimeout.readWriteTransaction(); + runner.run( + new TransactionCallable() { + @Override + public Void run(TransactionContext transaction) throws Exception { + return null; + } + }); + fail("Expected exception"); + } catch (SpannerException ex) { + assertEquals(ErrorCode.DEADLINE_EXCEEDED, ex.getErrorCode()); + } } @Test @@ -326,7 +343,7 @@ public void readWriteTransactionUnavailable() { runner.run( new TransactionCallable() { @Override - public Long run(TransactionContext transaction) throws Exception { + public Long run(TransactionContext transaction) { return transaction.executeUpdate(UPDATE_STATEMENT); } }); @@ -341,7 +358,7 @@ public void readWriteTransactionStatementAborted() { runner.run( new TransactionCallable() { @Override - public Long run(TransactionContext transaction) throws Exception { + public Long run(TransactionContext transaction) { if (attempts.getAndIncrement() == 0) { mockSpanner.abortTransaction(transaction); } @@ -360,7 +377,7 @@ public void readWriteTransactionCommitAborted() { runner.run( new TransactionCallable() { @Override - public Long run(TransactionContext transaction) throws Exception { + public Long run(TransactionContext transaction) { long res = transaction.executeUpdate(UPDATE_STATEMENT); if (attempts.getAndIncrement() == 0) { mockSpanner.abortTransaction(transaction); @@ -391,7 +408,7 @@ public void readWriteTransactionUncheckedException() { runner.run( new TransactionCallable() { @Override - public Long run(TransactionContext transaction) throws Exception { + public Long run(TransactionContext transaction) { transaction.executeUpdate(UPDATE_STATEMENT); throw SpannerExceptionFactory.newSpannerException(ErrorCode.INVALID_ARGUMENT, "test"); } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerImplTest.java index f2546f838d..aafd88390c 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerImplTest.java @@ -168,7 +168,7 @@ public void getDbclientAfterCloseThrows() { } @Test - public void testSpannerClosed() throws InterruptedException { + public void testSpannerClosed() { SpannerOptions options = createSpannerOptions(); Spanner spanner1 = options.getService(); Spanner spanner2 = options.getService(); @@ -199,7 +199,7 @@ public void testSpannerClosed() throws InterruptedException { } @Test - public void testClientId() throws Exception { + public void testClientId() { // Create a unique database id to be sure it has not yet been used in the lifetime of this JVM. String dbName = String.format("projects/p1/instances/i1/databases/%s", UUID.randomUUID().toString()); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerMatchers.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerMatchers.java index 9662047867..4723497a47 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerMatchers.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerMatchers.java @@ -21,6 +21,7 @@ import com.google.protobuf.Message; import com.google.protobuf.TextFormat; import java.lang.reflect.InvocationTargetException; +import java.util.concurrent.ExecutionException; import org.hamcrest.BaseMatcher; import org.hamcrest.Description; import org.hamcrest.Matcher; @@ -47,6 +48,15 @@ public static Matcher isSpannerException(ErrorCode code return new SpannerExceptionMatcher<>(code); } + /** + * Returns a method that checks that a {@link Throwable} is an {@link ExecutionException} where + * the cause is a {@link SpannerException} with an error code to {@code code}. + */ + public static Matcher isExecutionExceptionWithSpannerCause( + ErrorCode code) { + return new ExecutionExceptionWithSpannerCauseMatcher<>(code); + } + private static class ProtoTextMatcher extends BaseMatcher { private final T expected; @@ -110,4 +120,31 @@ public void describeTo(Description description) { description.appendText("SpannerException[" + expectedCode + "]"); } } + + private static class ExecutionExceptionWithSpannerCauseMatcher + extends BaseMatcher { + private final ErrorCode expectedCode; + + ExecutionExceptionWithSpannerCauseMatcher(ErrorCode expectedCode) { + this.expectedCode = checkNotNull(expectedCode); + } + + @Override + public boolean matches(Object item) { + if (!(item instanceof ExecutionException)) { + return false; + } + ExecutionException ee = (ExecutionException) item; + if (!(ee.getCause() instanceof SpannerException)) { + return false; + } + SpannerException e = (SpannerException) ee.getCause(); + return e.getErrorCode() == expectedCode; + } + + @Override + public void describeTo(Description description) { + description.appendText("ExecutionException[SpannerException[" + expectedCode + "]]"); + } + } } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerOptionsTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerOptionsTest.java index f03def2e33..65636e80fa 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerOptionsTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerOptionsTest.java @@ -19,6 +19,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.fail; import com.google.api.gax.retrying.RetrySettings; import com.google.api.gax.rpc.ServerStreamingCallSettings; @@ -35,9 +36,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.mockito.Mockito; @@ -47,8 +46,6 @@ @RunWith(JUnit4.class) public class SpannerOptionsTest { - @Rule public ExpectedException thrown = ExpectedException.none(); - @Test public void defaultBuilder() { // We need to set the project id since in test environment we cannot obtain a default project @@ -84,7 +81,7 @@ public void builder() { @Test public void testSpannerDefaultRetrySettings() { - RetrySettings defaultRetrySettings = + RetrySettings witRetryPolicy1 = RetrySettings.newBuilder() .setInitialRetryDelay(Duration.ofMillis(250L)) .setRetryDelayMultiplier(1.3) @@ -94,21 +91,28 @@ public void testSpannerDefaultRetrySettings() { .setMaxRpcTimeout(Duration.ofMillis(3600000L)) .setTotalTimeout(Duration.ofMillis(3600000L)) .build(); - RetrySettings streamingRetrySettings = + RetrySettings witRetryPolicy2 = RetrySettings.newBuilder() .setInitialRetryDelay(Duration.ofMillis(250L)) .setRetryDelayMultiplier(1.3) .setMaxRetryDelay(Duration.ofMillis(32000L)) - .setInitialRpcTimeout(Duration.ofMillis(3600000L)) + .setInitialRpcTimeout(Duration.ofMillis(60000L)) .setRpcTimeoutMultiplier(1.0) - .setMaxRpcTimeout(Duration.ofMillis(3600000L)) - .setTotalTimeout(Duration.ofMillis(3600000L)) + .setMaxRpcTimeout(Duration.ofMillis(60000L)) + .setTotalTimeout(Duration.ofMillis(60000L)) .build(); - RetrySettings longRunningRetrySettings = + RetrySettings witRetryPolicy3 = RetrySettings.newBuilder() .setInitialRetryDelay(Duration.ofMillis(250L)) .setRetryDelayMultiplier(1.3) .setMaxRetryDelay(Duration.ofMillis(32000L)) + .setInitialRpcTimeout(Duration.ofMillis(30000L)) + .setRpcTimeoutMultiplier(1.0) + .setMaxRpcTimeout(Duration.ofMillis(30000L)) + .setTotalTimeout(Duration.ofMillis(30000L)) + .build(); + RetrySettings noRetry1 = + RetrySettings.newBuilder() .setInitialRpcTimeout(Duration.ofMillis(3600000L)) .setRpcTimeoutMultiplier(1.0) .setMaxRpcTimeout(Duration.ofMillis(3600000L)) @@ -116,33 +120,37 @@ public void testSpannerDefaultRetrySettings() { .build(); SpannerOptions options = SpannerOptions.newBuilder().setProjectId("test-project").build(); SpannerStubSettings stubSettings = options.getSpannerStubSettings(); - List> callsWithDefaultSettings = + List> callsWithRetry1 = + Arrays.asList(stubSettings.listSessionsSettings(), stubSettings.commitSettings()); + List> callsWithRetry2 = + Arrays.asList(stubSettings.batchCreateSessionsSettings()); + List> callsWithRetry3 = Arrays.asList( - stubSettings.beginTransactionSettings(), stubSettings.createSessionSettings(), + stubSettings.getSessionSettings(), stubSettings.deleteSessionSettings(), - stubSettings.executeBatchDmlSettings(), stubSettings.executeSqlSettings(), - stubSettings.getSessionSettings(), - stubSettings.listSessionsSettings(), - stubSettings.partitionQuerySettings(), - stubSettings.partitionReadSettings(), + stubSettings.executeBatchDmlSettings(), stubSettings.readSettings(), - stubSettings.rollbackSettings()); - List> callsWithStreamingSettings = + stubSettings.beginTransactionSettings(), + stubSettings.rollbackSettings(), + stubSettings.partitionQuerySettings(), + stubSettings.partitionReadSettings()); + List> callsWithNoRetry1 = Arrays.asList( stubSettings.executeStreamingSqlSettings(), stubSettings.streamingReadSettings()); - List> callsWithLongRunningSettings = - Arrays.asList(stubSettings.commitSettings()); - for (UnaryCallSettings callSettings : callsWithDefaultSettings) { - assertThat(callSettings.getRetrySettings()).isEqualTo(defaultRetrySettings); + for (UnaryCallSettings callSettings : callsWithRetry1) { + assertThat(callSettings.getRetrySettings()).isEqualTo(witRetryPolicy1); } - for (ServerStreamingCallSettings callSettings : callsWithStreamingSettings) { - assertThat(callSettings.getRetrySettings()).isEqualTo(streamingRetrySettings); + for (UnaryCallSettings callSettings : callsWithRetry2) { + assertThat(callSettings.getRetrySettings()).isEqualTo(witRetryPolicy2); + } + for (UnaryCallSettings callSettings : callsWithRetry3) { + assertThat(callSettings.getRetrySettings()).isEqualTo(witRetryPolicy3); } - for (UnaryCallSettings callSettings : callsWithLongRunningSettings) { - assertThat(callSettings.getRetrySettings()).isEqualTo(longRunningRetrySettings); + for (ServerStreamingCallSettings callSettings : callsWithNoRetry1) { + assertThat(callSettings.getRetrySettings()).isEqualTo(noRetry1); } } @@ -216,26 +224,54 @@ public void testSpannerCustomRetrySettings() { @Test public void testDatabaseAdminDefaultRetrySettings() { - RetrySettings defaultRetrySettings = + RetrySettings withRetryPolicy1 = RetrySettings.newBuilder() .setInitialRetryDelay(Duration.ofMillis(1000L)) .setRetryDelayMultiplier(1.3) .setMaxRetryDelay(Duration.ofMillis(32000L)) - .setInitialRpcTimeout(Duration.ofMillis(60000L)) + .setInitialRpcTimeout(Duration.ofMillis(3600000L)) .setRpcTimeoutMultiplier(1.0) - .setMaxRpcTimeout(Duration.ofMillis(60000L)) - .setTotalTimeout(Duration.ofMillis(600000L)) + .setMaxRpcTimeout(Duration.ofMillis(3600000L)) + .setTotalTimeout(Duration.ofMillis(3600000L)) + .build(); + RetrySettings withRetryPolicy2 = + RetrySettings.newBuilder() + .setInitialRetryDelay(Duration.ofMillis(1000L)) + .setRetryDelayMultiplier(1.3) + .setMaxRetryDelay(Duration.ofMillis(32000L)) + .setInitialRpcTimeout(Duration.ofMillis(30000L)) + .setRpcTimeoutMultiplier(1.0) + .setMaxRpcTimeout(Duration.ofMillis(30000L)) + .setTotalTimeout(Duration.ofMillis(30000L)) + .build(); + RetrySettings noRetryPolicy2 = + RetrySettings.newBuilder() + .setInitialRpcTimeout(Duration.ofMillis(30000L)) + .setRpcTimeoutMultiplier(1.0) + .setMaxRpcTimeout(Duration.ofMillis(30000L)) + .setTotalTimeout(Duration.ofMillis(30000L)) .build(); SpannerOptions options = SpannerOptions.newBuilder().setProjectId("test-project").build(); DatabaseAdminStubSettings stubSettings = options.getDatabaseAdminStubSettings(); - List> callsWithDefaultSettings = + List> callsWithRetryPolicy1 = Arrays.asList( stubSettings.dropDatabaseSettings(), - stubSettings.getDatabaseDdlSettings(), - stubSettings.getDatabaseSettings()); + stubSettings.getDatabaseSettings(), + stubSettings.getDatabaseDdlSettings()); + List> callsWithRetryPolicy2 = + Arrays.asList(stubSettings.getIamPolicySettings()); + List> callsWithNoRetry2 = + Arrays.asList( + stubSettings.setIamPolicySettings(), stubSettings.testIamPermissionsSettings()); - for (UnaryCallSettings callSettings : callsWithDefaultSettings) { - assertThat(callSettings.getRetrySettings()).isEqualTo(defaultRetrySettings); + for (UnaryCallSettings callSettings : callsWithRetryPolicy1) { + assertThat(callSettings.getRetrySettings()).isEqualTo(withRetryPolicy1); + } + for (UnaryCallSettings callSettings : callsWithRetryPolicy2) { + assertThat(callSettings.getRetrySettings()).isEqualTo(withRetryPolicy2); + } + for (UnaryCallSettings callSettings : callsWithNoRetry2) { + assertThat(callSettings.getRetrySettings()).isEqualTo(noRetryPolicy2); } } @@ -278,28 +314,68 @@ public void testDatabaseAdminCustomRetrySettings() { @Test public void testInstanceAdminDefaultRetrySettings() { - RetrySettings defaultRetrySettings = + RetrySettings withRetryPolicy1 = RetrySettings.newBuilder() .setInitialRetryDelay(Duration.ofMillis(1000L)) .setRetryDelayMultiplier(1.3) .setMaxRetryDelay(Duration.ofMillis(32000L)) - .setInitialRpcTimeout(Duration.ofMillis(60000L)) + .setInitialRpcTimeout(Duration.ofMillis(3600000L)) .setRpcTimeoutMultiplier(1.0) - .setMaxRpcTimeout(Duration.ofMillis(60000L)) - .setTotalTimeout(Duration.ofMillis(600000L)) + .setMaxRpcTimeout(Duration.ofMillis(3600000L)) + .setTotalTimeout(Duration.ofMillis(3600000L)) + .build(); + RetrySettings withRetryPolicy2 = + RetrySettings.newBuilder() + .setInitialRetryDelay(Duration.ofMillis(1000L)) + .setRetryDelayMultiplier(1.3) + .setMaxRetryDelay(Duration.ofMillis(32000L)) + .setInitialRpcTimeout(Duration.ofMillis(30000L)) + .setRpcTimeoutMultiplier(1.0) + .setMaxRpcTimeout(Duration.ofMillis(30000L)) + .setTotalTimeout(Duration.ofMillis(30000L)) + .build(); + RetrySettings noRetryPolicy1 = + RetrySettings.newBuilder() + .setInitialRpcTimeout(Duration.ofMillis(3600000L)) + .setRpcTimeoutMultiplier(1.0) + .setMaxRpcTimeout(Duration.ofMillis(3600000L)) + .setTotalTimeout(Duration.ofMillis(3600000L)) + .build(); + RetrySettings noRetryPolicy2 = + RetrySettings.newBuilder() + .setInitialRpcTimeout(Duration.ofMillis(30000L)) + .setRpcTimeoutMultiplier(1.0) + .setMaxRpcTimeout(Duration.ofMillis(30000L)) + .setTotalTimeout(Duration.ofMillis(30000L)) .build(); SpannerOptions options = SpannerOptions.newBuilder().setProjectId("test-project").build(); InstanceAdminStubSettings stubSettings = options.getInstanceAdminStubSettings(); - List> callsWithDefaultSettings = + List> callsWithRetryPolicy1 = Arrays.asList( stubSettings.getInstanceConfigSettings(), stubSettings.listInstanceConfigsSettings(), stubSettings.deleteInstanceSettings(), stubSettings.getInstanceSettings(), stubSettings.listInstancesSettings()); + List> callsWithRetryPolicy2 = + Arrays.asList(stubSettings.getIamPolicySettings()); + List> callsWithNoRetryPolicy1 = + Arrays.asList(stubSettings.createInstanceSettings(), stubSettings.updateInstanceSettings()); + List> callsWithNoRetryPolicy2 = + Arrays.asList( + stubSettings.setIamPolicySettings(), stubSettings.testIamPermissionsSettings()); - for (UnaryCallSettings callSettings : callsWithDefaultSettings) { - assertThat(callSettings.getRetrySettings()).isEqualTo(defaultRetrySettings); + for (UnaryCallSettings callSettings : callsWithRetryPolicy1) { + assertThat(callSettings.getRetrySettings()).isEqualTo(withRetryPolicy1); + } + for (UnaryCallSettings callSettings : callsWithRetryPolicy2) { + assertThat(callSettings.getRetrySettings()).isEqualTo(withRetryPolicy2); + } + for (UnaryCallSettings callSettings : callsWithNoRetryPolicy1) { + assertThat(callSettings.getRetrySettings()).isEqualTo(noRetryPolicy1); + } + for (UnaryCallSettings callSettings : callsWithNoRetryPolicy2) { + assertThat(callSettings.getRetrySettings()).isEqualTo(noRetryPolicy2); } } @@ -346,22 +422,34 @@ public void testInstanceAdminCustomRetrySettings() { @Test public void testInvalidTransport() { - thrown.expect(IllegalArgumentException.class); - SpannerOptions.newBuilder().setTransportOptions(Mockito.mock(TransportOptions.class)); + try { + SpannerOptions.newBuilder().setTransportOptions(Mockito.mock(TransportOptions.class)); + fail("Expected exception"); + } catch (IllegalArgumentException ex) { + assertThat(ex.getMessage()).isNotNull(); + } } @Test public void testInvalidSessionLabels() { - thrown.expect(NullPointerException.class); Map labels = new HashMap<>(); labels.put("env", null); - SpannerOptions.newBuilder().setSessionLabels(labels); + try { + SpannerOptions.newBuilder().setSessionLabels(labels); + fail("Expected exception"); + } catch (NullPointerException ex) { + assertThat(ex.getMessage()).isNotNull(); + } } @Test public void testNullSessionLabels() { - thrown.expect(NullPointerException.class); - SpannerOptions.newBuilder().setSessionLabels(null); + try { + SpannerOptions.newBuilder().setSessionLabels(null); + fail("Expected exception"); + } catch (NullPointerException ex) { + assertThat(ex.getMessage()).isNotNull(); + } } @Test @@ -496,4 +584,35 @@ public String getOptimizerVersion() { assertThat(options.getDefaultQueryOptions(DatabaseId.of("p", "i", "o"))) .isEqualTo(QueryOptions.newBuilder().setOptimizerVersion("2").build()); } + + @Test + public void testCompressorName() { + assertThat( + SpannerOptions.newBuilder() + .setProjectId("p") + .setCompressorName("gzip") + .build() + .getCompressorName()) + .isEqualTo("gzip"); + assertThat( + SpannerOptions.newBuilder() + .setProjectId("p") + .setCompressorName("identity") + .build() + .getCompressorName()) + .isEqualTo("identity"); + assertThat( + SpannerOptions.newBuilder() + .setProjectId("p") + .setCompressorName(null) + .build() + .getCompressorName()) + .isNull(); + try { + SpannerOptions.newBuilder().setCompressorName("foo"); + fail("missing expected exception"); + } catch (IllegalArgumentException e) { + // ignore, this is the expected exception. + } + } } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerOptionsThreadTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerOptionsThreadTest.java new file mode 100644 index 0000000000..8626f5cb5c --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerOptionsThreadTest.java @@ -0,0 +1,188 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.spanner; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.api.core.ApiFunction; +import com.google.cloud.NoCredentials; +import com.google.cloud.spanner.connection.AbstractMockServerTest; +import com.google.common.base.Stopwatch; +import com.google.spanner.admin.database.v1.ListDatabasesResponse; +import com.google.spanner.admin.instance.v1.ListInstancesResponse; +import io.grpc.ManagedChannelBuilder; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class SpannerOptionsThreadTest extends AbstractMockServerTest { + private static final int NUMBER_OF_TEST_RUNS = 2; + private static final int DEFAULT_NUM_CHANNELS_PER_GAPIC_CLIENT = 4; + private static final int NUM_GAPIC_CLIENTS = 4; + private static final int NUM_THREADS = + Math.max( + DEFAULT_NUM_CHANNELS_PER_GAPIC_CLIENT * NUM_GAPIC_CLIENTS, + Runtime.getRuntime().availableProcessors()); + private static final String SPANNER_THREAD_NAME = "Cloud-Spanner-TransportChannel"; + private static final String THREAD_PATTERN = "%s-[0-9]+"; + + private final DatabaseId dbId = DatabaseId.of("p", "i", "d"); + + @SuppressWarnings("rawtypes") + private SpannerOptions createOptions() { + return SpannerOptions.newBuilder() + .setProjectId("p") + // Set a custom channel configurator to allow http instead of https. + .setChannelConfigurator( + new ApiFunction() { + @Override + public ManagedChannelBuilder apply(ManagedChannelBuilder input) { + input.usePlaintext(); + return input; + } + }) + .setHost("http://localhost:" + getPort()) + .setCredentials(NoCredentials.getInstance()) + .build(); + } + + @Test + public void testCloseAllThreadsWhenClosingSpanner() throws InterruptedException { + int baseThreadCount = getNumberOfThreadsWithName(SPANNER_THREAD_NAME); + for (int i = 0; i < NUMBER_OF_TEST_RUNS; i++) { + waitForStartup(); + assertThat(getNumberOfThreadsWithName(SPANNER_THREAD_NAME)).isAtMost(baseThreadCount); + // Create Spanner instance. + // We make a copy of the options instance, as SpannerOptions caches any service object + // that has been handed out. + SpannerOptions options = createOptions(); + Spanner spanner = options.getService(); + // Get a database client and do a query. This should initiate threads for the Spanner service. + DatabaseClient client = spanner.getDatabaseClient(dbId); + List resultSets = new ArrayList<>(); + // SpannerStub affiliates a channel with a session, so we need to use multiple sessions + // to ensure we also hit multiple channels. + for (int i2 = 0; i2 < options.getSessionPoolOptions().getMaxSessions(); i2++) { + ResultSet rs = client.singleUse().executeQuery(SELECT_COUNT_STATEMENT); + // Execute ResultSet#next() to send the query to Spanner. + rs.next(); + // Delay closing the result set in order to force the use of multiple sessions. + // As each session is linked to one transport channel, using multiple different + // sessions should initialize multiple transport channels. + resultSets.add(rs); + // Check whether the number of expected threads has been reached. + if (getNumberOfThreadsWithName(SPANNER_THREAD_NAME) == NUM_THREADS + baseThreadCount) { + break; + } + } + for (ResultSet rs : resultSets) { + rs.close(); + } + // Check the number of threads after the query. Doing a request should initialize a thread + // pool for the underlying SpannerClient. + assertThat(getNumberOfThreadsWithName(SPANNER_THREAD_NAME)) + .isEqualTo(NUM_THREADS + baseThreadCount); + + // Then do a request to the InstanceAdmin service and check the number of threads. + // Doing a request should initialize a thread pool for the underlying InstanceAdminClient. + for (int i2 = 0; i2 < DEFAULT_NUM_CHANNELS_PER_GAPIC_CLIENT * 2; i2++) { + InstanceAdminClient instanceAdminClient = spanner.getInstanceAdminClient(); + mockInstanceAdmin.addResponse(ListInstancesResponse.getDefaultInstance()); + instanceAdminClient.listInstances(); + } + assertThat(getNumberOfThreadsWithName(SPANNER_THREAD_NAME)) + .isEqualTo(NUM_THREADS + baseThreadCount); + + // Then do a request to the DatabaseAdmin service and check the number of threads. + // Doing a request should initialize a thread pool for the underlying DatabaseAdminClient. + for (int i2 = 0; i2 < DEFAULT_NUM_CHANNELS_PER_GAPIC_CLIENT * 2; i2++) { + DatabaseAdminClient databaseAdminClient = spanner.getDatabaseAdminClient(); + mockDatabaseAdmin.addResponse(ListDatabasesResponse.getDefaultInstance()); + databaseAdminClient.listDatabases(dbId.getInstanceId().getInstance()); + } + assertThat(getNumberOfThreadsWithName(SPANNER_THREAD_NAME)) + .isEqualTo(NUM_THREADS + baseThreadCount); + + // Now close the Spanner instance and check whether the threads are shutdown or not. + spanner.close(); + // Wait a little to allow the threads to actually shutdown. + Stopwatch watch = Stopwatch.createStarted(); + while (getNumberOfThreadsWithName(SPANNER_THREAD_NAME) > baseThreadCount + && watch.elapsed(TimeUnit.SECONDS) < 2) { + Thread.sleep(50L); + } + assertThat(getNumberOfThreadsWithName(SPANNER_THREAD_NAME)).isAtMost(baseThreadCount); + } + } + + @Test + public void testMultipleSpannersFromSameSpannerOptions() throws InterruptedException { + waitForStartup(); + int baseThreadCount = getNumberOfThreadsWithName(SPANNER_THREAD_NAME); + SpannerOptions options = createOptions(); + try (Spanner spanner1 = options.getService()) { + // Having both in the try-with-resources block is not possible, as it is the same instance. + // One will be closed before the other, and the closing of the second instance would fail. + Spanner spanner2 = options.getService(); + assertThat(spanner1).isSameInstanceAs(spanner2); + DatabaseClient client1 = spanner1.getDatabaseClient(dbId); + DatabaseClient client2 = spanner2.getDatabaseClient(dbId); + assertThat(client1).isSameInstanceAs(client2); + try (ResultSet rs1 = client1.singleUse().executeQuery(SELECT_COUNT_STATEMENT); + ResultSet rs2 = client2.singleUse().executeQuery(SELECT_COUNT_STATEMENT)) { + while (rs1.next() && rs2.next()) { + // Do nothing, just consume the result sets. + } + } + } + Stopwatch watch = Stopwatch.createStarted(); + while (getNumberOfThreadsWithName(SPANNER_THREAD_NAME) > baseThreadCount + && watch.elapsed(TimeUnit.SECONDS) < 2) { + Thread.sleep(50L); + } + assertThat(getNumberOfThreadsWithName(SPANNER_THREAD_NAME)).isAtMost(baseThreadCount); + } + + private void waitForStartup() throws InterruptedException { + // Wait until the IT environment has already started all base worker threads. + int threadCount; + Stopwatch watch = Stopwatch.createStarted(); + do { + threadCount = getNumberOfThreadsWithName(SPANNER_THREAD_NAME); + Thread.sleep(100L); + } while (getNumberOfThreadsWithName(SPANNER_THREAD_NAME) > threadCount + && watch.elapsed(TimeUnit.SECONDS) < 5); + } + + private int getNumberOfThreadsWithName(String serviceName) { + Pattern pattern = Pattern.compile(String.format(THREAD_PATTERN, serviceName)); + Set threadSet = Thread.getAllStackTraces().keySet(); + int res = 0; + for (Thread thread : threadSet) { + if (pattern.matcher(thread.getName()).matches()) { + res++; + } + } + return res; + } +} diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerRetryHelperTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerRetryHelperTest.java index 8991691bff..415145120f 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerRetryHelperTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerRetryHelperTest.java @@ -20,6 +20,7 @@ import static org.junit.Assert.fail; import com.google.common.base.Stopwatch; +import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.google.protobuf.Duration; import com.google.rpc.RetryInfo; import io.grpc.Context; @@ -49,7 +50,7 @@ public void testCancelledContext() { final Callable callable = new Callable() { @Override - public Integer call() throws Exception { + public Integer call() { latch.countDown(); throw SpannerExceptionFactory.newSpannerException(ErrorCode.ABORTED, "test"); } @@ -84,12 +85,12 @@ public void run() { } @Test - public void testTimedoutContext() throws InterruptedException { + public void testTimedoutContext() { ScheduledExecutorService service = Executors.newScheduledThreadPool(1); final Callable callable = new Callable() { @Override - public Integer call() throws Exception { + public Integer call() { throw SpannerExceptionFactory.newSpannerException(ErrorCode.ABORTED, "test"); } }; @@ -119,7 +120,7 @@ public void noException() { Callable callable = new Callable() { @Override - public Integer call() throws Exception { + public Integer call() { return 1 + 1; } }; @@ -131,7 +132,7 @@ public void propagateUncheckedException() { Callable callable = new Callable() { @Override - public Integer call() throws Exception { + public Integer call() { throw new IllegalStateException("test"); } }; @@ -144,7 +145,7 @@ public void retryOnAborted() { Callable callable = new Callable() { @Override - public Integer call() throws Exception { + public Integer call() { if (attempts.getAndIncrement() == 0) { throw abortedWithRetryInfo((int) TimeUnit.MILLISECONDS.toNanos(1L)); } @@ -160,7 +161,7 @@ public void retryMultipleTimesOnAborted() { Callable callable = new Callable() { @Override - public Integer call() throws Exception { + public Integer call() { if (attempts.getAndIncrement() < 2) { throw abortedWithRetryInfo((int) TimeUnit.MILLISECONDS.toNanos(1)); } @@ -176,7 +177,7 @@ public void retryOnAbortedAndThenPropagateUnchecked() { Callable callable = new Callable() { @Override - public Integer call() throws Exception { + public Integer call() { if (attempts.getAndIncrement() == 0) { throw abortedWithRetryInfo((int) TimeUnit.MILLISECONDS.toNanos(1L)); } @@ -188,6 +189,24 @@ public Integer call() throws Exception { @Test public void testExceptionWithRetryInfo() { + // Workaround from https://bugs.java.com/bugdatabase/view_bug.do?bug_id=6435126. + // See also https://stackoverflow.com/questions/824110/accurate-sleep-for-java-on-windows + // Note that this is a daemon thread, so it will not prevent the JVM from shutting down. + new ThreadFactoryBuilder() + .setDaemon(true) + .build() + .newThread( + new Runnable() { + @Override + public void run() { + while (true) { + try { + Thread.sleep(Long.MAX_VALUE); + } catch (InterruptedException e) { + } + } + } + }); final int RETRY_DELAY_MILLIS = 100; Metadata.Key key = ProtoUtils.keyForProto(RetryInfo.getDefaultInstance()); Status status = Status.fromCodeValue(Status.Code.ABORTED.value()); @@ -208,7 +227,7 @@ public void testExceptionWithRetryInfo() { Callable callable = new Callable() { @Override - public Integer call() throws Exception { + public Integer call() { if (attempts.getAndIncrement() == 0) { throw e; } @@ -220,7 +239,8 @@ public Integer call() throws Exception { Stopwatch watch = Stopwatch.createStarted(); assertThat(SpannerRetryHelper.runTxWithRetriesOnAborted(callable)).isEqualTo(2); long elapsed = watch.elapsed(TimeUnit.MILLISECONDS); - assertThat(elapsed >= RETRY_DELAY_MILLIS).isTrue(); + // Allow 1ms difference as that should be the accuracy of the sleep method. + assertThat(elapsed).isAtLeast(RETRY_DELAY_MILLIS - 1); } private SpannerException abortedWithRetryInfo(int nanos) { diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/StatementTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/StatementTest.java index 116781c582..4e39d1271d 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/StatementTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/StatementTest.java @@ -18,20 +18,19 @@ import static com.google.common.testing.SerializableTester.reserializeAndAssert; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; import com.google.cloud.ByteArray; import com.google.common.collect.ImmutableMap; import com.google.common.testing.EqualsTester; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Unit tests for {@link com.google.cloud.spanner.Statement}. */ @RunWith(JUnit4.class) public class StatementTest { - @Rule public ExpectedException expectedException = ExpectedException.none(); @Test public void basic() { @@ -44,7 +43,7 @@ public void basic() { } @Test - public void serialization() throws Exception { + public void serialization() { Statement stmt = Statement.newBuilder("SELECT * FROM table WHERE ") .append("bool_field = @bool_field ") @@ -103,33 +102,47 @@ public void bindReplacement() { public void incompleteBinding() { Statement.Builder builder = Statement.newBuilder("SELECT @v"); builder.bind("v"); - expectedException.expect(IllegalStateException.class); - builder.build(); + try { + builder.build(); + fail("Expected exception"); + } catch (IllegalStateException ex) { + assertThat(ex.getMessage()).isNotNull(); + } } @Test public void bindingInProgress() { Statement.Builder builder = Statement.newBuilder("SELECT @v"); builder.bind("v"); - - expectedException.expect(IllegalStateException.class); - builder.bind("y"); + try { + builder.bind("y"); + fail("Expected exception"); + } catch (IllegalStateException ex) { + assertThat(ex.getMessage()).isNotNull(); + } } @Test public void alreadyBound() { ValueBinder binder = Statement.newBuilder("SELECT @v").bind("v"); binder.to("abc"); - - expectedException.expect(IllegalStateException.class); - binder.to("xyz"); + try { + binder.to("xyz"); + fail("Expected exception"); + } catch (IllegalStateException ex) { + assertThat(ex.getMessage()).isNotNull(); + } } @Test public void bindCommitTimestampFails() { ValueBinder binder = Statement.newBuilder("SELECT @v").bind("v"); - expectedException.expect(IllegalArgumentException.class); - binder.to(Value.COMMIT_TIMESTAMP); + try { + binder.to(Value.COMMIT_TIMESTAMP); + fail("Expected exception"); + } catch (IllegalArgumentException ex) { + assertNotNull(ex.getMessage()); + } } @Test diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/StructTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/StructTest.java index 503723df0b..61d2f900cb 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/StructTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/StructTest.java @@ -23,9 +23,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -33,8 +31,6 @@ @RunWith(JUnit4.class) public class StructTest { - @Rule public ExpectedException expectedException = ExpectedException.none(); - @Test public void builder() { // These tests are basic: AbstractStructReaderTypesTest already covers all type getters. diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TimestampBoundTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TimestampBoundTest.java index 231831128b..955485da5d 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TimestampBoundTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TimestampBoundTest.java @@ -19,6 +19,8 @@ import static com.google.common.testing.SerializableTester.reserializeAndAssert; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; import com.google.cloud.Timestamp; import com.google.cloud.spanner.TimestampBound.Mode; @@ -26,9 +28,7 @@ import com.google.spanner.v1.TransactionOptions; import java.util.concurrent.TimeUnit; import org.hamcrest.MatcherAssert; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -37,10 +37,9 @@ public class TimestampBoundTest { private static final long TEST_TIME_SECONDS = 1444662894L; private static final String TEST_TIME_ISO = "2015-10-12T15:14:54Z"; - @Rule public ExpectedException expectedException = ExpectedException.none(); @Test - public void serialization() throws Exception { + public void serialization() { reserializeAndAssert(TimestampBound.strong()); reserializeAndAssert(TimestampBound.ofExactStaleness(10, TimeUnit.NANOSECONDS)); reserializeAndAssert(TimestampBound.ofMaxStaleness(100, TimeUnit.DAYS)); @@ -90,8 +89,12 @@ public void exactStaleness() { @Test public void exactStalenessNegative() { - expectedException.expect(IllegalArgumentException.class); - TimestampBound.ofExactStaleness(-1, TimeUnit.SECONDS); + try { + TimestampBound.ofExactStaleness(-1, TimeUnit.SECONDS); + fail("Expected exception"); + } catch (IllegalArgumentException ex) { + assertNotNull(ex.getMessage()); + } } @Test @@ -119,8 +122,12 @@ public void stalenessSourceUnits() { @Test public void maxStalenessNegative() { - expectedException.expect(IllegalArgumentException.class); - TimestampBound.ofMaxStaleness(-1, TimeUnit.SECONDS); + try { + TimestampBound.ofMaxStaleness(-1, TimeUnit.SECONDS); + fail("Expected exception"); + } catch (IllegalArgumentException ex) { + assertNotNull(ex.getMessage()); + } } @Test diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerAbortedTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerAbortedTest.java index 8272ac4aa8..dec674bd6c 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerAbortedTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerAbortedTest.java @@ -167,7 +167,7 @@ public static void stopServer() throws InterruptedException { } @Before - public void setUp() throws Exception { + public void setUp() { mockSpanner.reset(); mockSpanner.removeAllExecutionTimes(); SpannerOptions.Builder builder = @@ -179,7 +179,7 @@ public void setUp() throws Exception { } @After - public void tearDown() throws Exception { + public void tearDown() { spanner.close(); } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerImplTest.java index cd94d82800..38aa66516e 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionManagerImplTest.java @@ -17,6 +17,7 @@ package com.google.cloud.spanner; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.fail; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; @@ -25,6 +26,7 @@ import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; +import com.google.api.core.ApiFuture; import com.google.api.core.ApiFutures; import com.google.cloud.Timestamp; import com.google.cloud.grpc.GrpcTransportOptions; @@ -37,6 +39,7 @@ import com.google.spanner.v1.CommitRequest; import com.google.spanner.v1.CommitResponse; import com.google.spanner.v1.Transaction; +import io.opencensus.trace.Span; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -44,9 +47,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.mockito.Mock; @@ -69,8 +70,6 @@ public void release(ScheduledExecutorService exec) { } } - @Rule public ExpectedException exception = ExpectedException.none(); - @Mock private SessionImpl session; @Mock TransactionRunnerImpl.TransactionContextImpl txn; private TransactionManagerImpl manager; @@ -78,7 +77,7 @@ public void release(ScheduledExecutorService exec) { @Before public void setUp() { initMocks(this); - manager = new TransactionManagerImpl(session); + manager = new TransactionManagerImpl(session, mock(Span.class)); } @Test @@ -86,26 +85,42 @@ public void beginCalledTwiceFails() { when(session.newTransaction()).thenReturn(txn); assertThat(manager.begin()).isEqualTo(txn); assertThat(manager.getState()).isEqualTo(TransactionState.STARTED); - exception.expect(IllegalStateException.class); - manager.begin(); + try { + manager.begin(); + fail("Expected exception"); + } catch (IllegalStateException ex) { + assertNotNull(ex.getMessage()); + } } @Test public void commitBeforeBeginFails() { - exception.expect(IllegalStateException.class); - manager.commit(); + try { + manager.commit(); + fail("Expected exception"); + } catch (IllegalStateException ex) { + assertNotNull(ex.getMessage()); + } } @Test public void rollbackBeforeBeginFails() { - exception.expect(IllegalStateException.class); - manager.rollback(); + try { + manager.rollback(); + fail("Expected exception"); + } catch (IllegalStateException ex) { + assertNotNull(ex.getMessage()); + } } @Test public void resetBeforeBeginFails() { - exception.expect(IllegalStateException.class); - manager.resetForRetry(); + try { + manager.resetForRetry(); + fail("Expected exception"); + } catch (IllegalStateException ex) { + assertNotNull(ex.getMessage()); + } } @Test @@ -133,8 +148,12 @@ public void resetAfterSuccessfulCommitFails() { when(session.newTransaction()).thenReturn(txn); manager.begin(); manager.commit(); - exception.expect(IllegalStateException.class); - manager.resetForRetry(); + try { + manager.resetForRetry(); + fail("Expected exception"); + } catch (IllegalStateException ex) { + assertNotNull(ex.getMessage()); + } } @Test @@ -165,8 +184,12 @@ public void resetAfterErrorFails() { } catch (SpannerException e) { assertThat(e.getErrorCode()).isEqualTo(ErrorCode.UNKNOWN); } - exception.expect(IllegalStateException.class); - manager.resetForRetry(); + try { + manager.resetForRetry(); + fail("Expected exception"); + } catch (IllegalStateException ex) { + assertNotNull(ex.getMessage()); + } } @Test @@ -174,8 +197,12 @@ public void rollbackAfterCommitFails() { when(session.newTransaction()).thenReturn(txn); manager.begin(); manager.commit(); - exception.expect(IllegalStateException.class); - manager.rollback(); + try { + manager.rollback(); + fail("Expected exception"); + } catch (IllegalStateException ex) { + assertNotNull(ex.getMessage()); + } } @Test @@ -183,8 +210,12 @@ public void commitAfterRollbackFails() { when(session.newTransaction()).thenReturn(txn); manager.begin(); manager.rollback(); - exception.expect(IllegalStateException.class); - manager.commit(); + try { + manager.commit(); + fail("Expected exception"); + } catch (IllegalStateException ex) { + assertNotNull(ex.getMessage()); + } } @SuppressWarnings("unchecked") @@ -207,8 +238,7 @@ public void usesPreparedTransaction() { .thenAnswer( new Answer>() { @Override - public List answer(InvocationOnMock invocation) - throws Throwable { + public List answer(InvocationOnMock invocation) { return Arrays.asList( com.google.spanner.v1.Session.newBuilder() .setName((String) invocation.getArguments()[0] + "/sessions/1") @@ -218,26 +248,29 @@ public List answer(InvocationOnMock invocation) .build()); } }); - when(rpc.beginTransaction(Mockito.any(BeginTransactionRequest.class), Mockito.anyMap())) + when(rpc.beginTransactionAsync(Mockito.any(BeginTransactionRequest.class), Mockito.anyMap())) .thenAnswer( - new Answer() { + new Answer>() { @Override - public Transaction answer(InvocationOnMock invocation) throws Throwable { - return Transaction.newBuilder() - .setId(ByteString.copyFromUtf8(UUID.randomUUID().toString())) - .build(); + public ApiFuture answer(InvocationOnMock invocation) { + return ApiFutures.immediateFuture( + Transaction.newBuilder() + .setId(ByteString.copyFromUtf8(UUID.randomUUID().toString())) + .build()); } }); - when(rpc.commit(Mockito.any(CommitRequest.class), Mockito.anyMap())) + when(rpc.commitAsync(Mockito.any(CommitRequest.class), Mockito.anyMap())) .thenAnswer( - new Answer() { + new Answer>() { @Override - public CommitResponse answer(InvocationOnMock invocation) throws Throwable { - return CommitResponse.newBuilder() - .setCommitTimestamp( - com.google.protobuf.Timestamp.newBuilder() - .setSeconds(System.currentTimeMillis() * 1000)) - .build(); + public ApiFuture answer(InvocationOnMock invocation) + throws Throwable { + return ApiFutures.immediateFuture( + CommitResponse.newBuilder() + .setCommitTimestamp( + com.google.protobuf.Timestamp.newBuilder() + .setSeconds(System.currentTimeMillis() * 1000)) + .build()); } }); DatabaseId db = DatabaseId.of("test", "test", "test"); @@ -248,7 +281,7 @@ public CommitResponse answer(InvocationOnMock invocation) throws Throwable { mgr.commit(); } verify(rpc, times(1)) - .beginTransaction(Mockito.any(BeginTransactionRequest.class), Mockito.anyMap()); + .beginTransactionAsync(Mockito.any(BeginTransactionRequest.class), Mockito.anyMap()); } } } 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 c45f46fbd1..d61c89300f 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 @@ -24,6 +24,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import com.google.api.core.ApiFuture; import com.google.api.core.ApiFutures; import com.google.cloud.grpc.GrpcTransportOptions; import com.google.cloud.grpc.GrpcTransportOptions.ExecutorFactory; @@ -50,6 +51,7 @@ import io.grpc.Status; import io.grpc.StatusRuntimeException; import io.grpc.protobuf.ProtoUtils; +import io.opencensus.trace.Span; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -90,11 +92,18 @@ public void release(ScheduledExecutorService exec) { private boolean firstRun; @Before - public void setUp() throws Exception { + public void setUp() { MockitoAnnotations.initMocks(this); firstRun = true; when(session.newTransaction()).thenReturn(txn); transactionRunner = new TransactionRunnerImpl(session, rpc, 1); + when(rpc.commitAsync(Mockito.any(CommitRequest.class), Mockito.anyMap())) + .thenReturn( + ApiFutures.immediateFuture( + CommitResponse.newBuilder() + .setCommitTimestamp(Timestamp.getDefaultInstance()) + .build())); + transactionRunner.setSpan(mock(Span.class)); } @SuppressWarnings("unchecked") @@ -117,8 +126,7 @@ public void usesPreparedTransaction() { .thenAnswer( new Answer>() { @Override - public List answer(InvocationOnMock invocation) - throws Throwable { + public List answer(InvocationOnMock invocation) { return Arrays.asList( com.google.spanner.v1.Session.newBuilder() .setName((String) invocation.getArguments()[0] + "/sessions/1") @@ -127,25 +135,28 @@ public List answer(InvocationOnMock invocation) .build()); } }); - when(rpc.beginTransaction(Mockito.any(BeginTransactionRequest.class), Mockito.anyMap())) + when(rpc.beginTransactionAsync(Mockito.any(BeginTransactionRequest.class), Mockito.anyMap())) .thenAnswer( - new Answer() { + new Answer>() { @Override - public Transaction answer(InvocationOnMock invocation) throws Throwable { - return Transaction.newBuilder() - .setId(ByteString.copyFromUtf8(UUID.randomUUID().toString())) - .build(); + public ApiFuture answer(InvocationOnMock invocation) { + return ApiFutures.immediateFuture( + Transaction.newBuilder() + .setId(ByteString.copyFromUtf8(UUID.randomUUID().toString())) + .build()); } }); - when(rpc.commit(Mockito.any(CommitRequest.class), Mockito.anyMap())) + when(rpc.commitAsync(Mockito.any(CommitRequest.class), Mockito.anyMap())) .thenAnswer( - new Answer() { + new Answer>() { @Override - public CommitResponse answer(InvocationOnMock invocation) throws Throwable { - return CommitResponse.newBuilder() - .setCommitTimestamp( - Timestamp.newBuilder().setSeconds(System.currentTimeMillis() * 1000)) - .build(); + public ApiFuture answer(InvocationOnMock invocation) + throws Throwable { + return ApiFutures.immediateFuture( + CommitResponse.newBuilder() + .setCommitTimestamp( + Timestamp.newBuilder().setSeconds(System.currentTimeMillis() * 1000)) + .build()); } }); DatabaseId db = DatabaseId.of("test", "test", "test"); @@ -156,12 +167,12 @@ public CommitResponse answer(InvocationOnMock invocation) throws Throwable { .run( new TransactionCallable() { @Override - public Void run(TransactionContext transaction) throws Exception { + public Void run(TransactionContext transaction) { return null; } }); verify(rpc, times(1)) - .beginTransaction(Mockito.any(BeginTransactionRequest.class), Mockito.anyMap()); + .beginTransactionAsync(Mockito.any(BeginTransactionRequest.class), Mockito.anyMap()); } } @@ -171,7 +182,7 @@ public void commitSucceeds() { transactionRunner.run( new TransactionCallable() { @Override - public Void run(TransactionContext transaction) throws Exception { + public Void run(TransactionContext transaction) { numCalls.incrementAndGet(); return null; } @@ -197,7 +208,7 @@ public void commitAbort() { transactionRunner.run( new TransactionCallable() { @Override - public Void run(TransactionContext transaction) throws Exception { + public Void run(TransactionContext transaction) { numCalls.incrementAndGet(); return null; } @@ -217,7 +228,7 @@ public void commitFailsWithNonAbort() { transactionRunner.run( new TransactionCallable() { @Override - public Void run(TransactionContext transaction) throws Exception { + public Void run(TransactionContext transaction) { numCalls.incrementAndGet(); return null; } @@ -232,7 +243,7 @@ public Void run(TransactionContext transaction) throws Exception { } @Test - public void runResourceExhaustedNoRetry() throws Exception { + public void runResourceExhaustedNoRetry() { try { runTransaction( new StatusRuntimeException(Status.fromCodeValue(Status.Code.RESOURCE_EXHAUSTED.value()))); @@ -273,10 +284,12 @@ private long[] batchDmlException(int status) { .setRpc(rpc) .build(); when(session.newTransaction()).thenReturn(transaction); - when(session.beginTransaction()) - .thenReturn(ByteString.copyFromUtf8(UUID.randomUUID().toString())); + when(session.beginTransactionAsync()) + .thenReturn( + ApiFutures.immediateFuture(ByteString.copyFromUtf8(UUID.randomUUID().toString()))); when(session.getName()).thenReturn(SessionId.of("p", "i", "d", "test").getName()); TransactionRunnerImpl runner = new TransactionRunnerImpl(session, rpc, 10); + runner.setSpan(mock(Span.class)); ExecuteBatchDmlResponse response1 = ExecuteBatchDmlResponse.newBuilder() .addResultSets( @@ -301,14 +314,15 @@ private long[] batchDmlException(int status) { .thenReturn(response1, response2); CommitResponse commitResponse = CommitResponse.newBuilder().setCommitTimestamp(Timestamp.getDefaultInstance()).build(); - when(rpc.commit(Mockito.any(CommitRequest.class), Mockito.anyMap())).thenReturn(commitResponse); + when(rpc.commitAsync(Mockito.any(CommitRequest.class), Mockito.anyMap())) + .thenReturn(ApiFutures.immediateFuture(commitResponse)); final Statement statement = Statement.of("UPDATE FOO SET BAR=1"); final AtomicInteger numCalls = new AtomicInteger(0); long updateCount[] = runner.run( new TransactionCallable() { @Override - public long[] run(TransactionContext transaction) throws Exception { + public long[] run(TransactionContext transaction) { numCalls.incrementAndGet(); return transaction.batchUpdate(Arrays.asList(statement, statement)); } @@ -324,7 +338,7 @@ private void runTransaction(final Exception exception) { transactionRunner.run( new TransactionCallable() { @Override - public Void run(TransactionContext transaction) throws Exception { + public Void run(TransactionContext transaction) { if (firstRun) { firstRun = false; throw SpannerExceptionFactory.newSpannerException(exception); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TypeTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TypeTest.java index 4665b119fd..db83c1ba66 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TypeTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TypeTest.java @@ -19,19 +19,18 @@ import static com.google.cloud.spanner.Type.StructField; import static com.google.common.testing.SerializableTester.reserializeAndAssert; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; import com.google.spanner.v1.TypeCode; import org.hamcrest.MatcherAssert; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Unit tests for {@link com.google.cloud.spanner.Type}. */ @RunWith(JUnit4.class) public class TypeTest { - @Rule public ExpectedException expectedException = ExpectedException.none(); private abstract static class ScalarTypeTester { final Type.Code expectedCode; @@ -289,34 +288,46 @@ public void emptyStruct() { @Test public void structFieldIndexNotFound() { Type t = Type.struct(StructField.of("f1", Type.int64())); - - expectedException.expect(IllegalArgumentException.class); - expectedException.expectMessage("Field not found: f2"); - t.getFieldIndex("f2"); + try { + t.getFieldIndex("f2"); + fail("Expected exception"); + } catch (IllegalArgumentException ex) { + assertThat(ex.getMessage().contains("Field not found: f2")); + } } @Test public void structFieldIndexAmbiguous() { Type t = Type.struct(StructField.of("f1", Type.int64()), StructField.of("f1", Type.string())); - - expectedException.expect(IllegalArgumentException.class); - expectedException.expectMessage("Ambiguous field name: f1"); - t.getFieldIndex("f1"); + try { + t.getFieldIndex("f1"); + fail("Expected exception"); + } catch (IllegalArgumentException ex) { + assertThat(ex.getMessage().contains("Ambiguous field name: f1")); + } } @Test public void parseErrorMissingTypeCode() { com.google.spanner.v1.Type proto = com.google.spanner.v1.Type.newBuilder().build(); - expectedException.expect(IllegalArgumentException.class); - Type.fromProto(proto); + try { + Type.fromProto(proto); + fail("Expected exception"); + } catch (IllegalArgumentException ex) { + assertNotNull(ex.getMessage()); + } } @Test public void parseErrorMissingArrayElementTypeProto() { com.google.spanner.v1.Type proto = com.google.spanner.v1.Type.newBuilder().setCode(TypeCode.ARRAY).build(); - expectedException.expect(IllegalArgumentException.class); - Type.fromProto(proto); + try { + Type.fromProto(proto); + fail("Expected exception"); + } catch (IllegalArgumentException ex) { + assertNotNull(ex.getMessage()); + } } private static void assertProtoEquals(com.google.spanner.v1.Type proto, String expected) { diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java index 58b9bb05dd..a7b99d313e 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java @@ -19,6 +19,7 @@ import static com.google.common.testing.SerializableTester.reserializeAndAssert; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; +import static org.junit.Assert.fail; import com.google.cloud.ByteArray; import com.google.cloud.Date; @@ -27,15 +28,12 @@ import com.google.common.collect.ForwardingList; import com.google.common.collect.Lists; import com.google.common.testing.EqualsTester; -import java.io.IOException; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.List; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -43,7 +41,6 @@ @RunWith(JUnit4.class) public class ValueTest { private static final String NULL_STRING = "NULL"; - @Rule public ExpectedException expectedException = ExpectedException.none(); private static ByteArray newByteArray(String data) { return ByteArray.copyFrom(data); @@ -85,10 +82,12 @@ public void boolWrapperNull() { assertThat(v.getType()).isEqualTo(Type.bool()); assertThat(v.isNull()).isTrue(); assertThat(v.toString()).isEqualTo(NULL_STRING); - - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("null value"); - v.getBool(); + try { + v.getBool(); + fail("Expected exception"); + } catch (IllegalStateException ex) { + assertThat(ex.getMessage()).contains("null value"); + } } @Test @@ -103,28 +102,34 @@ public void int64() { @Test public void int64TryGetBool() { Value value = Value.int64(1234); - - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("Expected: BOOL actual: INT64"); - value.getBool(); + try { + value.getBool(); + fail("Expected exception"); + } catch (IllegalStateException e) { + assertThat(e.getMessage()).contains("Expected: BOOL actual: INT64"); + } } @Test public void int64NullTryGetBool() { Value value = Value.int64(null); - - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("Expected: BOOL actual: INT64"); - value.getBool(); + try { + value.getBool(); + fail("Expected exception"); + } catch (IllegalStateException e) { + assertThat(e.getMessage()).contains("Expected: BOOL actual: INT64"); + } } @Test public void int64TryGetInt64Array() { Value value = Value.int64(1234); - - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("Expected: ARRAY actual: INT64"); - value.getInt64Array(); + try { + value.getInt64Array(); + fail("Expected exception"); + } catch (IllegalStateException e) { + assertThat(e.getMessage()).contains("Expected: ARRAY actual: INT64"); + } } @Test @@ -142,10 +147,12 @@ public void int64WrapperNull() { assertThat(v.getType()).isEqualTo(Type.int64()); assertThat(v.isNull()).isTrue(); assertThat(v.toString()).isEqualTo(NULL_STRING); - - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("null value"); - v.getInt64(); + try { + v.getInt64(); + fail("Expected exception"); + } catch (IllegalStateException e) { + assertThat(e.getMessage()).contains("null value"); + } } @Test @@ -172,10 +179,12 @@ public void float64WrapperNull() { assertThat(v.getType()).isEqualTo(Type.float64()); assertThat(v.isNull()).isTrue(); assertThat(v.toString()).isEqualTo(NULL_STRING); - - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("null value"); - v.getFloat64(); + try { + v.getFloat64(); + fail("Expected exception"); + } catch (IllegalStateException e) { + assertThat(e.getMessage()).contains("null value"); + } } @Test @@ -192,19 +201,21 @@ public void stringNull() { assertThat(v.getType()).isEqualTo(Type.string()); assertThat(v.isNull()).isTrue(); assertThat(v.toString()).isEqualTo(NULL_STRING); - - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("null value"); - v.getString(); + try { + v.getString(); + fail("Expected exception"); + } catch (IllegalStateException e) { + assertThat(e.getMessage().contains("null value")); + } } @Test public void stringLong() { - String str = "aaaaaaaaaabbbbbbbbbbccccccccccdddddddddd"; + String str = "aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeee"; Value v = Value.string(str); assertThat(v.getString()).isEqualTo(str); - assertThat(v.toString()).hasLength(32); - assertThat(v.toString()).startsWith(str.substring(0, 32 - 3)); + assertThat(v.toString()).hasLength(36); + assertThat(v.toString()).startsWith(str.substring(0, 36 - 3)); assertThat(v.toString()).endsWith("..."); } @@ -232,10 +243,12 @@ public void bytesNull() { assertThat(v.getType()).isEqualTo(Type.bytes()); assertThat(v.isNull()).isTrue(); assertThat(v.toString()).isEqualTo(NULL_STRING); - - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("null value"); - v.getBytes(); + try { + v.getBytes(); + fail("Expected exception"); + } catch (IllegalStateException e) { + assertThat(e.getMessage().contains("null value")); + } } @Test @@ -257,9 +270,12 @@ public void timestampNull() { assertThat(v.isNull()).isTrue(); assertThat(v.toString()).isEqualTo(NULL_STRING); assertThat(v.isCommitTimestamp()).isFalse(); - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("null value"); - v.getTimestamp(); + try { + v.getTimestamp(); + fail("Expected exception"); + } catch (IllegalStateException e) { + assertThat(e.getMessage().contains("null value")); + } } @Test @@ -274,9 +290,12 @@ public void commitTimestamp() { com.google.protobuf.Value.newBuilder() .setStringValue("spanner.commit_timestamp()") .build()); - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("Commit timestamp value"); - v.getTimestamp(); + try { + v.getTimestamp(); + fail("Expected exception"); + } catch (IllegalStateException e) { + assertThat(e.getMessage().contains("Commit timestamp value")); + } } @Test @@ -296,10 +315,12 @@ public void dateNull() { assertThat(v.getType()).isEqualTo(Type.date()); assertThat(v.isNull()).isTrue(); assertThat(v.toString()).isEqualTo(NULL_STRING); - - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("null value"); - v.getDate(); + try { + v.getDate(); + fail("Expected exception"); + } catch (IllegalStateException e) { + assertThat(e.getMessage().contains("null value")); + } } @Test @@ -323,10 +344,12 @@ public void boolArrayNull() { Value v = Value.boolArray((boolean[]) null); assertThat(v.isNull()).isTrue(); assertThat(v.toString()).isEqualTo(NULL_STRING); - - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("null value"); - v.getBoolArray(); + try { + v.getBoolArray(); + fail("Expected exception"); + } catch (IllegalStateException e) { + assertThat(e.getMessage().contains("null value")); + } } @Test @@ -342,10 +365,12 @@ public void boolArrayFromListNull() { Value v = Value.boolArray((Iterable) null); assertThat(v.isNull()).isTrue(); assertThat(v.toString()).isEqualTo(NULL_STRING); - - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("null value"); - v.getBoolArray(); + try { + v.getBoolArray(); + fail("Expected exception"); + } catch (IllegalStateException e) { + assertThat(e.getMessage().contains("null value")); + } } @Test @@ -368,10 +393,12 @@ public void boolArrayFromPlainIterable() { @Test public void boolArrayTryGetInt64Array() { Value value = Value.boolArray(Arrays.asList(true)); - - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("Expected: ARRAY actual: ARRAY"); - value.getInt64Array(); + try { + value.getInt64Array(); + fail("Expected exception"); + } catch (IllegalStateException e) { + assertThat(e.getMessage().contains("Expected: ARRAY actual: ARRAY")); + } } @Test @@ -395,10 +422,12 @@ public void int64ArrayNull() { Value v = Value.int64Array((long[]) null); assertThat(v.isNull()).isTrue(); assertThat(v.toString()).isEqualTo(NULL_STRING); - - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("null value"); - v.getInt64Array(); + try { + v.getInt64Array(); + fail("Expected exception"); + } catch (IllegalStateException e) { + assertThat(e.getMessage().contains("null value")); + } } @Test @@ -414,28 +443,34 @@ public void int64ArrayWrapperNull() { Value v = Value.int64Array((Iterable) null); assertThat(v.isNull()).isTrue(); assertThat(v.toString()).isEqualTo(NULL_STRING); - - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("null value"); - v.getInt64Array(); + try { + v.getInt64Array(); + fail("Expected exception"); + } catch (IllegalStateException e) { + assertThat(e.getMessage().contains("null value")); + } } @Test public void int64ArrayTryGetBool() { Value value = Value.int64Array(Arrays.asList(1234L)); - - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("Expected: BOOL actual: ARRAY"); - value.getBool(); + try { + value.getBool(); + fail("Expected exception"); + } catch (IllegalStateException e) { + assertThat(e.getMessage().contains("Expected: BOOL actual: ARRAY")); + } } @Test public void int64ArrayNullTryGetBool() { Value value = Value.int64Array((Iterable) null); - - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("Expected: BOOL actual: ARRAY"); - value.getBool(); + try { + value.getBool(); + fail("Expected exception"); + } catch (IllegalStateException e) { + assertThat(e.getMessage().contains("Expected: BOOL actual: ARRAY")); + } } @Test @@ -459,10 +494,12 @@ public void float64ArrayNull() { Value v = Value.float64Array((double[]) null); assertThat(v.isNull()).isTrue(); assertThat(v.toString()).isEqualTo(NULL_STRING); - - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("null value"); - v.getFloat64Array(); + try { + v.getFloat64Array(); + fail("Expected exception"); + } catch (IllegalStateException e) { + assertThat(e.getMessage().contains("null value")); + } } @Test @@ -478,19 +515,23 @@ public void float64ArrayWrapperNull() { Value v = Value.float64Array((Iterable) null); assertThat(v.isNull()).isTrue(); assertThat(v.toString()).isEqualTo(NULL_STRING); - - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("null value"); - v.getFloat64Array(); + try { + v.getFloat64Array(); + fail("Expected exception"); + } catch (IllegalStateException e) { + assertThat(e.getMessage().contains("null value")); + } } @Test public void float64ArrayTryGetInt64Array() { Value value = Value.float64Array(Arrays.asList(.1)); - - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("Expected: ARRAY actual: ARRAY"); - value.getInt64Array(); + try { + value.getInt64Array(); + fail("Expected exception"); + } catch (IllegalStateException e) { + assertThat(e.getMessage().contains("Expected: ARRAY actual: ARRAY")); + } } @Test @@ -506,19 +547,23 @@ public void stringArrayNull() { Value v = Value.stringArray(null); assertThat(v.isNull()).isTrue(); assertThat(v.toString()).isEqualTo(NULL_STRING); - - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("null value"); - v.getStringArray(); + try { + v.getStringArray(); + fail("Expected exception"); + } catch (IllegalStateException e) { + assertThat(e.getMessage().contains("null value")); + } } @Test public void stringArrayTryGetBytesArray() { Value value = Value.stringArray(Arrays.asList("a")); - - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("Expected: ARRAY actual: ARRAY"); - value.getBytesArray(); + try { + value.getBytesArray(); + fail("Expected exception"); + } catch (IllegalStateException e) { + assertThat(e.getMessage().contains("Expected: ARRAY actual: ARRAY")); + } } @Test @@ -536,19 +581,23 @@ public void bytesArrayNull() { Value v = Value.bytesArray(null); assertThat(v.isNull()).isTrue(); assertThat(v.toString()).isEqualTo(NULL_STRING); - - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("null value"); - v.getBytesArray(); + try { + v.getBytesArray(); + fail("Expected exception"); + } catch (IllegalStateException e) { + assertThat(e.getMessage().contains("null value")); + } } @Test public void bytesArrayTryGetStringArray() { Value value = Value.bytesArray(Arrays.asList(newByteArray("a"))); - - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("Expected: ARRAY actual: ARRAY"); - value.getStringArray(); + try { + value.getStringArray(); + fail("Expected exception"); + } catch (IllegalStateException e) { + assertThat(e.getMessage().contains("Expected: ARRAY actual: ARRAY")); + } } @Test @@ -570,10 +619,12 @@ public void timestampArrayNull() { Value v = Value.timestampArray(null); assertThat(v.isNull()).isTrue(); assertThat(v.toString()).isEqualTo(NULL_STRING); - - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("null value"); - v.getTimestampArray(); + try { + v.getTimestampArray(); + fail("Expected exception"); + } catch (IllegalStateException e) { + assertThat(e.getMessage().contains("null value")); + } } @Test @@ -594,10 +645,12 @@ public void dateArrayNull() { Value v = Value.dateArray(null); assertThat(v.isNull()).isTrue(); assertThat(v.toString()).isEqualTo(NULL_STRING); - - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("null value"); - v.getDateArray(); + try { + v.getDateArray(); + fail("Expected exception"); + } catch (IllegalStateException e) { + assertThat(e.getMessage().contains("null value")); + } } @Test @@ -611,10 +664,12 @@ public void struct() { Value v2 = Value.struct(struct.getType(), struct); assertThat(v2).isEqualTo(v1); - - expectedException.expect(IllegalArgumentException.class); - expectedException.expectMessage("Mismatch between struct value and type."); - Value.struct(Type.struct(Arrays.asList(StructField.of("f3", Type.string()))), struct); + try { + Value.struct(Type.struct(Arrays.asList(StructField.of("f3", Type.string()))), struct); + fail("Expected exception"); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage().contains("Mismatch between struct value and type.")); + } } @Test @@ -627,10 +682,12 @@ public void nullStruct() { assertThat(v.getType().getStructFields()).isEqualTo(fieldTypes); assertThat(v.isNull()).isTrue(); assertThat(v.toString()).isEqualTo(NULL_STRING); - - expectedException.expect(NullPointerException.class); - expectedException.expectMessage("Illegal call to create a NULL struct value."); - Value.struct(null); + try { + Value.struct(null); + fail("Expected exception"); + } catch (NullPointerException e) { + assertThat(e.getMessage().contains("Illegal call to create a NULL struct value.")); + } } @Test @@ -641,10 +698,12 @@ public void nullStructGetter() { Value v = Value.struct(Type.struct(fieldTypes), null); assertThat(v.isNull()).isTrue(); - - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("Illegal call to getter of null value."); - v.getStruct(); + try { + v.getStruct(); + fail("Expected exception"); + } catch (IllegalStateException e) { + assertThat(e.getMessage()).contains("Illegal call to getter of null value."); + } } @Test @@ -726,10 +785,12 @@ public void structArrayNull() { assertThat(v.isNull()).isTrue(); assertThat(v.getType().getArrayElementType()).isEqualTo(elementType); assertThat(v.toString()).isEqualTo(NULL_STRING); - - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("Illegal call to getter of null value"); - v.getStructArray(); + try { + v.getStructArray(); + fail("Expected exception"); + } catch (IllegalStateException e) { + assertThat(e.getMessage()).contains("Illegal call to getter of null value"); + } } @Test @@ -744,10 +805,12 @@ public void structArrayInvalidType() { Arrays.asList( Struct.newBuilder().set("ff1").to("1").set("ff2").to(1).build(), Struct.newBuilder().set("ff1").to(2).set("ff2").to(3).build()); - - expectedException.expect(IllegalArgumentException.class); - expectedException.expectMessage("must have type STRUCT"); - Value.structArray(elementType, arrayElements); + try { + Value.structArray(elementType, arrayElements); + fail("Expected exception"); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage()).contains("must have type STRUCT"); + } } @Test @@ -944,13 +1007,11 @@ protected List delegate() { return delegate; } - private void readObject(@SuppressWarnings("unused") java.io.ObjectInputStream unusedStream) - throws IOException, ClassNotFoundException { + private void readObject(@SuppressWarnings("unused") java.io.ObjectInputStream unusedStream) { throw new IllegalStateException("Serialization disabled"); } - private void writeObject(@SuppressWarnings("unused") java.io.ObjectOutputStream unusedStream) - throws IOException { + private void writeObject(@SuppressWarnings("unused") java.io.ObjectOutputStream unusedStream) { throw new IllegalStateException("Serialization disabled"); } } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/admin/database/v1/DatabaseAdminClientTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/admin/database/v1/DatabaseAdminClientTest.java index 78efb14466..e304a4ef50 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/admin/database/v1/DatabaseAdminClientTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/admin/database/v1/DatabaseAdminClientTest.java @@ -121,30 +121,29 @@ public void tearDown() throws Exception { @Test @SuppressWarnings("all") - public void listDatabasesTest() { - String nextPageToken = ""; - Database databasesElement = Database.newBuilder().build(); - List databases = Arrays.asList(databasesElement); - ListDatabasesResponse expectedResponse = - ListDatabasesResponse.newBuilder() - .setNextPageToken(nextPageToken) - .addAllDatabases(databases) + public void createDatabaseTest() throws Exception { + DatabaseName name = DatabaseName.of("[PROJECT]", "[INSTANCE]", "[DATABASE]"); + Database expectedResponse = Database.newBuilder().setName(name.toString()).build(); + Operation resultOperation = + Operation.newBuilder() + .setName("createDatabaseTest") + .setDone(true) + .setResponse(Any.pack(expectedResponse)) .build(); - mockDatabaseAdmin.addResponse(expectedResponse); + mockDatabaseAdmin.addResponse(resultOperation); InstanceName parent = InstanceName.of("[PROJECT]", "[INSTANCE]"); + String createStatement = "createStatement552974828"; - ListDatabasesPagedResponse pagedListResponse = client.listDatabases(parent); - - List resources = Lists.newArrayList(pagedListResponse.iterateAll()); - Assert.assertEquals(1, resources.size()); - Assert.assertEquals(expectedResponse.getDatabasesList().get(0), resources.get(0)); + Database actualResponse = client.createDatabaseAsync(parent, createStatement).get(); + Assert.assertEquals(expectedResponse, actualResponse); List actualRequests = mockDatabaseAdmin.getRequests(); Assert.assertEquals(1, actualRequests.size()); - ListDatabasesRequest actualRequest = (ListDatabasesRequest) actualRequests.get(0); + CreateDatabaseRequest actualRequest = (CreateDatabaseRequest) actualRequests.get(0); Assert.assertEquals(parent, InstanceName.parse(actualRequest.getParent())); + Assert.assertEquals(createStatement, actualRequest.getCreateStatement()); Assert.assertTrue( channelProvider.isHeaderSent( ApiClientHeaderProvider.getDefaultApiClientHeaderKey(), @@ -153,45 +152,47 @@ public void listDatabasesTest() { @Test @SuppressWarnings("all") - public void listDatabasesExceptionTest() throws Exception { + public void createDatabaseExceptionTest() throws Exception { StatusRuntimeException exception = new StatusRuntimeException(Status.INVALID_ARGUMENT); mockDatabaseAdmin.addException(exception); try { InstanceName parent = InstanceName.of("[PROJECT]", "[INSTANCE]"); + String createStatement = "createStatement552974828"; - client.listDatabases(parent); + client.createDatabaseAsync(parent, createStatement).get(); Assert.fail("No exception raised"); - } catch (InvalidArgumentException e) { - // Expected exception + } catch (ExecutionException e) { + Assert.assertEquals(InvalidArgumentException.class, e.getCause().getClass()); + InvalidArgumentException apiException = (InvalidArgumentException) e.getCause(); + Assert.assertEquals(StatusCode.Code.INVALID_ARGUMENT, apiException.getStatusCode().getCode()); } } @Test @SuppressWarnings("all") - public void createDatabaseTest() throws Exception { - DatabaseName name = DatabaseName.of("[PROJECT]", "[INSTANCE]", "[DATABASE]"); - Database expectedResponse = Database.newBuilder().setName(name.toString()).build(); + public void updateDatabaseDdlTest() throws Exception { + Empty expectedResponse = Empty.newBuilder().build(); Operation resultOperation = Operation.newBuilder() - .setName("createDatabaseTest") + .setName("updateDatabaseDdlTest") .setDone(true) .setResponse(Any.pack(expectedResponse)) .build(); mockDatabaseAdmin.addResponse(resultOperation); - InstanceName parent = InstanceName.of("[PROJECT]", "[INSTANCE]"); - String createStatement = "createStatement552974828"; + DatabaseName database = DatabaseName.of("[PROJECT]", "[INSTANCE]", "[DATABASE]"); + List statements = new ArrayList<>(); - Database actualResponse = client.createDatabaseAsync(parent, createStatement).get(); + Empty actualResponse = client.updateDatabaseDdlAsync(database, statements).get(); Assert.assertEquals(expectedResponse, actualResponse); List actualRequests = mockDatabaseAdmin.getRequests(); Assert.assertEquals(1, actualRequests.size()); - CreateDatabaseRequest actualRequest = (CreateDatabaseRequest) actualRequests.get(0); + UpdateDatabaseDdlRequest actualRequest = (UpdateDatabaseDdlRequest) actualRequests.get(0); - Assert.assertEquals(parent, InstanceName.parse(actualRequest.getParent())); - Assert.assertEquals(createStatement, actualRequest.getCreateStatement()); + Assert.assertEquals(database, DatabaseName.parse(actualRequest.getDatabase())); + Assert.assertEquals(statements, actualRequest.getStatementsList()); Assert.assertTrue( channelProvider.isHeaderSent( ApiClientHeaderProvider.getDefaultApiClientHeaderKey(), @@ -200,15 +201,15 @@ public void createDatabaseTest() throws Exception { @Test @SuppressWarnings("all") - public void createDatabaseExceptionTest() throws Exception { + public void updateDatabaseDdlExceptionTest() throws Exception { StatusRuntimeException exception = new StatusRuntimeException(Status.INVALID_ARGUMENT); mockDatabaseAdmin.addException(exception); try { - InstanceName parent = InstanceName.of("[PROJECT]", "[INSTANCE]"); - String createStatement = "createStatement552974828"; + DatabaseName database = DatabaseName.of("[PROJECT]", "[INSTANCE]", "[DATABASE]"); + List statements = new ArrayList<>(); - client.createDatabaseAsync(parent, createStatement).get(); + client.updateDatabaseDdlAsync(database, statements).get(); Assert.fail("No exception raised"); } catch (ExecutionException e) { Assert.assertEquals(InvalidArgumentException.class, e.getCause().getClass()); @@ -219,21 +220,38 @@ public void createDatabaseExceptionTest() throws Exception { @Test @SuppressWarnings("all") - public void getDatabaseTest() { - DatabaseName name2 = DatabaseName.of("[PROJECT]", "[INSTANCE]", "[DATABASE]"); - Database expectedResponse = Database.newBuilder().setName(name2.toString()).build(); - mockDatabaseAdmin.addResponse(expectedResponse); + public void createBackupTest() throws Exception { + DatabaseName database = DatabaseName.of("[PROJECT]", "[INSTANCE]", "[DATABASE]"); + BackupName name = BackupName.of("[PROJECT]", "[INSTANCE]", "[BACKUP]"); + long sizeBytes = 1796325715L; + Backup expectedResponse = + Backup.newBuilder() + .setDatabase(database.toString()) + .setName(name.toString()) + .setSizeBytes(sizeBytes) + .build(); + Operation resultOperation = + Operation.newBuilder() + .setName("createBackupTest") + .setDone(true) + .setResponse(Any.pack(expectedResponse)) + .build(); + mockDatabaseAdmin.addResponse(resultOperation); - DatabaseName name = DatabaseName.of("[PROJECT]", "[INSTANCE]", "[DATABASE]"); + InstanceName parent = InstanceName.of("[PROJECT]", "[INSTANCE]"); + Backup backup = Backup.newBuilder().build(); + String backupId = "backupId1355353272"; - Database actualResponse = client.getDatabase(name); + Backup actualResponse = client.createBackupAsync(parent, backup, backupId).get(); Assert.assertEquals(expectedResponse, actualResponse); List actualRequests = mockDatabaseAdmin.getRequests(); Assert.assertEquals(1, actualRequests.size()); - GetDatabaseRequest actualRequest = (GetDatabaseRequest) actualRequests.get(0); + CreateBackupRequest actualRequest = (CreateBackupRequest) actualRequests.get(0); - Assert.assertEquals(name, DatabaseName.parse(actualRequest.getName())); + Assert.assertEquals(parent, InstanceName.parse(actualRequest.getParent())); + Assert.assertEquals(backup, actualRequest.getBackup()); + Assert.assertEquals(backupId, actualRequest.getBackupId()); Assert.assertTrue( channelProvider.isHeaderSent( ApiClientHeaderProvider.getDefaultApiClientHeaderKey(), @@ -242,44 +260,51 @@ public void getDatabaseTest() { @Test @SuppressWarnings("all") - public void getDatabaseExceptionTest() throws Exception { + public void createBackupExceptionTest() throws Exception { StatusRuntimeException exception = new StatusRuntimeException(Status.INVALID_ARGUMENT); mockDatabaseAdmin.addException(exception); try { - DatabaseName name = DatabaseName.of("[PROJECT]", "[INSTANCE]", "[DATABASE]"); + InstanceName parent = InstanceName.of("[PROJECT]", "[INSTANCE]"); + Backup backup = Backup.newBuilder().build(); + String backupId = "backupId1355353272"; - client.getDatabase(name); + client.createBackupAsync(parent, backup, backupId).get(); Assert.fail("No exception raised"); - } catch (InvalidArgumentException e) { - // Expected exception + } catch (ExecutionException e) { + Assert.assertEquals(InvalidArgumentException.class, e.getCause().getClass()); + InvalidArgumentException apiException = (InvalidArgumentException) e.getCause(); + Assert.assertEquals(StatusCode.Code.INVALID_ARGUMENT, apiException.getStatusCode().getCode()); } } @Test @SuppressWarnings("all") - public void updateDatabaseDdlTest() throws Exception { - Empty expectedResponse = Empty.newBuilder().build(); + public void restoreDatabaseTest() throws Exception { + DatabaseName name = DatabaseName.of("[PROJECT]", "[INSTANCE]", "[DATABASE]"); + Database expectedResponse = Database.newBuilder().setName(name.toString()).build(); Operation resultOperation = Operation.newBuilder() - .setName("updateDatabaseDdlTest") + .setName("restoreDatabaseTest") .setDone(true) .setResponse(Any.pack(expectedResponse)) .build(); mockDatabaseAdmin.addResponse(resultOperation); - DatabaseName database = DatabaseName.of("[PROJECT]", "[INSTANCE]", "[DATABASE]"); - List statements = new ArrayList<>(); + InstanceName parent = InstanceName.of("[PROJECT]", "[INSTANCE]"); + String databaseId = "databaseId816491103"; + BackupName backup = BackupName.of("[PROJECT]", "[INSTANCE]", "[BACKUP]"); - Empty actualResponse = client.updateDatabaseDdlAsync(database, statements).get(); + Database actualResponse = client.restoreDatabaseAsync(parent, databaseId, backup).get(); Assert.assertEquals(expectedResponse, actualResponse); List actualRequests = mockDatabaseAdmin.getRequests(); Assert.assertEquals(1, actualRequests.size()); - UpdateDatabaseDdlRequest actualRequest = (UpdateDatabaseDdlRequest) actualRequests.get(0); + RestoreDatabaseRequest actualRequest = (RestoreDatabaseRequest) actualRequests.get(0); - Assert.assertEquals(database, DatabaseName.parse(actualRequest.getDatabase())); - Assert.assertEquals(statements, actualRequest.getStatementsList()); + Assert.assertEquals(parent, InstanceName.parse(actualRequest.getParent())); + Assert.assertEquals(databaseId, actualRequest.getDatabaseId()); + Assert.assertEquals(backup, BackupName.parse(actualRequest.getBackup())); Assert.assertTrue( channelProvider.isHeaderSent( ApiClientHeaderProvider.getDefaultApiClientHeaderKey(), @@ -288,15 +313,16 @@ public void updateDatabaseDdlTest() throws Exception { @Test @SuppressWarnings("all") - public void updateDatabaseDdlExceptionTest() throws Exception { + public void restoreDatabaseExceptionTest() throws Exception { StatusRuntimeException exception = new StatusRuntimeException(Status.INVALID_ARGUMENT); mockDatabaseAdmin.addException(exception); try { - DatabaseName database = DatabaseName.of("[PROJECT]", "[INSTANCE]", "[DATABASE]"); - List statements = new ArrayList<>(); + InstanceName parent = InstanceName.of("[PROJECT]", "[INSTANCE]"); + String databaseId = "databaseId816491103"; + BackupName backup = BackupName.of("[PROJECT]", "[INSTANCE]", "[BACKUP]"); - client.updateDatabaseDdlAsync(database, statements).get(); + client.restoreDatabaseAsync(parent, databaseId, backup).get(); Assert.fail("No exception raised"); } catch (ExecutionException e) { Assert.assertEquals(InvalidArgumentException.class, e.getCause().getClass()); @@ -305,6 +331,93 @@ public void updateDatabaseDdlExceptionTest() throws Exception { } } + @Test + @SuppressWarnings("all") + public void listDatabasesTest() { + String nextPageToken = ""; + Database databasesElement = Database.newBuilder().build(); + List databases = Arrays.asList(databasesElement); + ListDatabasesResponse expectedResponse = + ListDatabasesResponse.newBuilder() + .setNextPageToken(nextPageToken) + .addAllDatabases(databases) + .build(); + mockDatabaseAdmin.addResponse(expectedResponse); + + InstanceName parent = InstanceName.of("[PROJECT]", "[INSTANCE]"); + + ListDatabasesPagedResponse pagedListResponse = client.listDatabases(parent); + + List resources = Lists.newArrayList(pagedListResponse.iterateAll()); + Assert.assertEquals(1, resources.size()); + Assert.assertEquals(expectedResponse.getDatabasesList().get(0), resources.get(0)); + + List actualRequests = mockDatabaseAdmin.getRequests(); + Assert.assertEquals(1, actualRequests.size()); + ListDatabasesRequest actualRequest = (ListDatabasesRequest) actualRequests.get(0); + + Assert.assertEquals(parent, InstanceName.parse(actualRequest.getParent())); + Assert.assertTrue( + channelProvider.isHeaderSent( + ApiClientHeaderProvider.getDefaultApiClientHeaderKey(), + GaxGrpcProperties.getDefaultApiClientHeaderPattern())); + } + + @Test + @SuppressWarnings("all") + public void listDatabasesExceptionTest() throws Exception { + StatusRuntimeException exception = new StatusRuntimeException(Status.INVALID_ARGUMENT); + mockDatabaseAdmin.addException(exception); + + try { + InstanceName parent = InstanceName.of("[PROJECT]", "[INSTANCE]"); + + client.listDatabases(parent); + Assert.fail("No exception raised"); + } catch (InvalidArgumentException e) { + // Expected exception + } + } + + @Test + @SuppressWarnings("all") + public void getDatabaseTest() { + DatabaseName name2 = DatabaseName.of("[PROJECT]", "[INSTANCE]", "[DATABASE]"); + Database expectedResponse = Database.newBuilder().setName(name2.toString()).build(); + mockDatabaseAdmin.addResponse(expectedResponse); + + DatabaseName name = DatabaseName.of("[PROJECT]", "[INSTANCE]", "[DATABASE]"); + + Database actualResponse = client.getDatabase(name); + Assert.assertEquals(expectedResponse, actualResponse); + + List actualRequests = mockDatabaseAdmin.getRequests(); + Assert.assertEquals(1, actualRequests.size()); + GetDatabaseRequest actualRequest = (GetDatabaseRequest) actualRequests.get(0); + + Assert.assertEquals(name, DatabaseName.parse(actualRequest.getName())); + Assert.assertTrue( + channelProvider.isHeaderSent( + ApiClientHeaderProvider.getDefaultApiClientHeaderKey(), + GaxGrpcProperties.getDefaultApiClientHeaderPattern())); + } + + @Test + @SuppressWarnings("all") + public void getDatabaseExceptionTest() throws Exception { + StatusRuntimeException exception = new StatusRuntimeException(Status.INVALID_ARGUMENT); + mockDatabaseAdmin.addException(exception); + + try { + DatabaseName name = DatabaseName.of("[PROJECT]", "[INSTANCE]", "[DATABASE]"); + + client.getDatabase(name); + Assert.fail("No exception raised"); + } catch (InvalidArgumentException e) { + // Expected exception + } + } + @Test @SuppressWarnings("all") public void dropDatabaseTest() { @@ -504,66 +617,6 @@ public void testIamPermissionsExceptionTest() throws Exception { } } - @Test - @SuppressWarnings("all") - public void createBackupTest() throws Exception { - DatabaseName database = DatabaseName.of("[PROJECT]", "[INSTANCE]", "[DATABASE]"); - BackupName name = BackupName.of("[PROJECT]", "[INSTANCE]", "[BACKUP]"); - long sizeBytes = 1796325715L; - Backup expectedResponse = - Backup.newBuilder() - .setDatabase(database.toString()) - .setName(name.toString()) - .setSizeBytes(sizeBytes) - .build(); - Operation resultOperation = - Operation.newBuilder() - .setName("createBackupTest") - .setDone(true) - .setResponse(Any.pack(expectedResponse)) - .build(); - mockDatabaseAdmin.addResponse(resultOperation); - - InstanceName parent = InstanceName.of("[PROJECT]", "[INSTANCE]"); - Backup backup = Backup.newBuilder().build(); - String backupId = "backupId1355353272"; - - Backup actualResponse = client.createBackupAsync(parent, backup, backupId).get(); - Assert.assertEquals(expectedResponse, actualResponse); - - List actualRequests = mockDatabaseAdmin.getRequests(); - Assert.assertEquals(1, actualRequests.size()); - CreateBackupRequest actualRequest = (CreateBackupRequest) actualRequests.get(0); - - Assert.assertEquals(parent, InstanceName.parse(actualRequest.getParent())); - Assert.assertEquals(backup, actualRequest.getBackup()); - Assert.assertEquals(backupId, actualRequest.getBackupId()); - Assert.assertTrue( - channelProvider.isHeaderSent( - ApiClientHeaderProvider.getDefaultApiClientHeaderKey(), - GaxGrpcProperties.getDefaultApiClientHeaderPattern())); - } - - @Test - @SuppressWarnings("all") - public void createBackupExceptionTest() throws Exception { - StatusRuntimeException exception = new StatusRuntimeException(Status.INVALID_ARGUMENT); - mockDatabaseAdmin.addException(exception); - - try { - InstanceName parent = InstanceName.of("[PROJECT]", "[INSTANCE]"); - Backup backup = Backup.newBuilder().build(); - String backupId = "backupId1355353272"; - - client.createBackupAsync(parent, backup, backupId).get(); - Assert.fail("No exception raised"); - } catch (ExecutionException e) { - Assert.assertEquals(InvalidArgumentException.class, e.getCause().getClass()); - InvalidArgumentException apiException = (InvalidArgumentException) e.getCause(); - Assert.assertEquals(StatusCode.Code.INVALID_ARGUMENT, apiException.getStatusCode().getCode()); - } - } - @Test @SuppressWarnings("all") public void getBackupTest() { @@ -744,59 +797,6 @@ public void listBackupsExceptionTest() throws Exception { } } - @Test - @SuppressWarnings("all") - public void restoreDatabaseTest() throws Exception { - DatabaseName name = DatabaseName.of("[PROJECT]", "[INSTANCE]", "[DATABASE]"); - Database expectedResponse = Database.newBuilder().setName(name.toString()).build(); - Operation resultOperation = - Operation.newBuilder() - .setName("restoreDatabaseTest") - .setDone(true) - .setResponse(Any.pack(expectedResponse)) - .build(); - mockDatabaseAdmin.addResponse(resultOperation); - - InstanceName parent = InstanceName.of("[PROJECT]", "[INSTANCE]"); - String databaseId = "databaseId816491103"; - BackupName backup = BackupName.of("[PROJECT]", "[INSTANCE]", "[BACKUP]"); - - Database actualResponse = client.restoreDatabaseAsync(parent, databaseId, backup).get(); - Assert.assertEquals(expectedResponse, actualResponse); - - List actualRequests = mockDatabaseAdmin.getRequests(); - Assert.assertEquals(1, actualRequests.size()); - RestoreDatabaseRequest actualRequest = (RestoreDatabaseRequest) actualRequests.get(0); - - Assert.assertEquals(parent, InstanceName.parse(actualRequest.getParent())); - Assert.assertEquals(databaseId, actualRequest.getDatabaseId()); - Assert.assertEquals(backup, BackupName.parse(actualRequest.getBackup())); - Assert.assertTrue( - channelProvider.isHeaderSent( - ApiClientHeaderProvider.getDefaultApiClientHeaderKey(), - GaxGrpcProperties.getDefaultApiClientHeaderPattern())); - } - - @Test - @SuppressWarnings("all") - public void restoreDatabaseExceptionTest() throws Exception { - StatusRuntimeException exception = new StatusRuntimeException(Status.INVALID_ARGUMENT); - mockDatabaseAdmin.addException(exception); - - try { - InstanceName parent = InstanceName.of("[PROJECT]", "[INSTANCE]"); - String databaseId = "databaseId816491103"; - BackupName backup = BackupName.of("[PROJECT]", "[INSTANCE]", "[BACKUP]"); - - client.restoreDatabaseAsync(parent, databaseId, backup).get(); - Assert.fail("No exception raised"); - } catch (ExecutionException e) { - Assert.assertEquals(InvalidArgumentException.class, e.getCause().getClass()); - InvalidArgumentException apiException = (InvalidArgumentException) e.getCause(); - Assert.assertEquals(StatusCode.Code.INVALID_ARGUMENT, apiException.getStatusCode().getCode()); - } - } - @Test @SuppressWarnings("all") public void listDatabaseOperationsTest() { diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/admin/instance/v1/InstanceAdminClientTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/admin/instance/v1/InstanceAdminClientTest.java index 406171d3aa..889de79200 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/admin/instance/v1/InstanceAdminClientTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/admin/instance/v1/InstanceAdminClientTest.java @@ -109,30 +109,40 @@ public void tearDown() throws Exception { @Test @SuppressWarnings("all") - public void listInstanceConfigsTest() { - String nextPageToken = ""; - InstanceConfig instanceConfigsElement = InstanceConfig.newBuilder().build(); - List instanceConfigs = Arrays.asList(instanceConfigsElement); - ListInstanceConfigsResponse expectedResponse = - ListInstanceConfigsResponse.newBuilder() - .setNextPageToken(nextPageToken) - .addAllInstanceConfigs(instanceConfigs) + public void createInstanceTest() throws Exception { + InstanceName name = InstanceName.of("[PROJECT]", "[INSTANCE]"); + InstanceConfigName config = InstanceConfigName.of("[PROJECT]", "[INSTANCE_CONFIG]"); + String displayName = "displayName1615086568"; + int nodeCount = 1539922066; + Instance expectedResponse = + Instance.newBuilder() + .setName(name.toString()) + .setConfig(config.toString()) + .setDisplayName(displayName) + .setNodeCount(nodeCount) .build(); - mockInstanceAdmin.addResponse(expectedResponse); + Operation resultOperation = + Operation.newBuilder() + .setName("createInstanceTest") + .setDone(true) + .setResponse(Any.pack(expectedResponse)) + .build(); + mockInstanceAdmin.addResponse(resultOperation); ProjectName parent = ProjectName.of("[PROJECT]"); + String instanceId = "instanceId-2101995259"; + Instance instance = Instance.newBuilder().build(); - ListInstanceConfigsPagedResponse pagedListResponse = client.listInstanceConfigs(parent); - - List resources = Lists.newArrayList(pagedListResponse.iterateAll()); - Assert.assertEquals(1, resources.size()); - Assert.assertEquals(expectedResponse.getInstanceConfigsList().get(0), resources.get(0)); + Instance actualResponse = client.createInstanceAsync(parent, instanceId, instance).get(); + Assert.assertEquals(expectedResponse, actualResponse); List actualRequests = mockInstanceAdmin.getRequests(); Assert.assertEquals(1, actualRequests.size()); - ListInstanceConfigsRequest actualRequest = (ListInstanceConfigsRequest) actualRequests.get(0); + CreateInstanceRequest actualRequest = (CreateInstanceRequest) actualRequests.get(0); Assert.assertEquals(parent, ProjectName.parse(actualRequest.getParent())); + Assert.assertEquals(instanceId, actualRequest.getInstanceId()); + Assert.assertEquals(instance, actualRequest.getInstance()); Assert.assertTrue( channelProvider.isHeaderSent( ApiClientHeaderProvider.getDefaultApiClientHeaderKey(), @@ -141,39 +151,58 @@ public void listInstanceConfigsTest() { @Test @SuppressWarnings("all") - public void listInstanceConfigsExceptionTest() throws Exception { + public void createInstanceExceptionTest() throws Exception { StatusRuntimeException exception = new StatusRuntimeException(Status.INVALID_ARGUMENT); mockInstanceAdmin.addException(exception); try { ProjectName parent = ProjectName.of("[PROJECT]"); + String instanceId = "instanceId-2101995259"; + Instance instance = Instance.newBuilder().build(); - client.listInstanceConfigs(parent); + client.createInstanceAsync(parent, instanceId, instance).get(); Assert.fail("No exception raised"); - } catch (InvalidArgumentException e) { - // Expected exception + } catch (ExecutionException e) { + Assert.assertEquals(InvalidArgumentException.class, e.getCause().getClass()); + InvalidArgumentException apiException = (InvalidArgumentException) e.getCause(); + Assert.assertEquals(StatusCode.Code.INVALID_ARGUMENT, apiException.getStatusCode().getCode()); } } @Test @SuppressWarnings("all") - public void getInstanceConfigTest() { - InstanceConfigName name2 = InstanceConfigName.of("[PROJECT]", "[INSTANCE_CONFIG]"); + public void updateInstanceTest() throws Exception { + InstanceName name = InstanceName.of("[PROJECT]", "[INSTANCE]"); + InstanceConfigName config = InstanceConfigName.of("[PROJECT]", "[INSTANCE_CONFIG]"); String displayName = "displayName1615086568"; - InstanceConfig expectedResponse = - InstanceConfig.newBuilder().setName(name2.toString()).setDisplayName(displayName).build(); - mockInstanceAdmin.addResponse(expectedResponse); + int nodeCount = 1539922066; + Instance expectedResponse = + Instance.newBuilder() + .setName(name.toString()) + .setConfig(config.toString()) + .setDisplayName(displayName) + .setNodeCount(nodeCount) + .build(); + Operation resultOperation = + Operation.newBuilder() + .setName("updateInstanceTest") + .setDone(true) + .setResponse(Any.pack(expectedResponse)) + .build(); + mockInstanceAdmin.addResponse(resultOperation); - InstanceConfigName name = InstanceConfigName.of("[PROJECT]", "[INSTANCE_CONFIG]"); + Instance instance = Instance.newBuilder().build(); + FieldMask fieldMask = FieldMask.newBuilder().build(); - InstanceConfig actualResponse = client.getInstanceConfig(name); + Instance actualResponse = client.updateInstanceAsync(instance, fieldMask).get(); Assert.assertEquals(expectedResponse, actualResponse); List actualRequests = mockInstanceAdmin.getRequests(); Assert.assertEquals(1, actualRequests.size()); - GetInstanceConfigRequest actualRequest = (GetInstanceConfigRequest) actualRequests.get(0); + UpdateInstanceRequest actualRequest = (UpdateInstanceRequest) actualRequests.get(0); - Assert.assertEquals(name, InstanceConfigName.parse(actualRequest.getName())); + Assert.assertEquals(instance, actualRequest.getInstance()); + Assert.assertEquals(fieldMask, actualRequest.getFieldMask()); Assert.assertTrue( channelProvider.isHeaderSent( ApiClientHeaderProvider.getDefaultApiClientHeaderKey(), @@ -182,44 +211,47 @@ public void getInstanceConfigTest() { @Test @SuppressWarnings("all") - public void getInstanceConfigExceptionTest() throws Exception { + public void updateInstanceExceptionTest() throws Exception { StatusRuntimeException exception = new StatusRuntimeException(Status.INVALID_ARGUMENT); mockInstanceAdmin.addException(exception); try { - InstanceConfigName name = InstanceConfigName.of("[PROJECT]", "[INSTANCE_CONFIG]"); + Instance instance = Instance.newBuilder().build(); + FieldMask fieldMask = FieldMask.newBuilder().build(); - client.getInstanceConfig(name); + client.updateInstanceAsync(instance, fieldMask).get(); Assert.fail("No exception raised"); - } catch (InvalidArgumentException e) { - // Expected exception + } catch (ExecutionException e) { + Assert.assertEquals(InvalidArgumentException.class, e.getCause().getClass()); + InvalidArgumentException apiException = (InvalidArgumentException) e.getCause(); + Assert.assertEquals(StatusCode.Code.INVALID_ARGUMENT, apiException.getStatusCode().getCode()); } } @Test @SuppressWarnings("all") - public void listInstancesTest() { + public void listInstanceConfigsTest() { String nextPageToken = ""; - Instance instancesElement = Instance.newBuilder().build(); - List instances = Arrays.asList(instancesElement); - ListInstancesResponse expectedResponse = - ListInstancesResponse.newBuilder() + InstanceConfig instanceConfigsElement = InstanceConfig.newBuilder().build(); + List instanceConfigs = Arrays.asList(instanceConfigsElement); + ListInstanceConfigsResponse expectedResponse = + ListInstanceConfigsResponse.newBuilder() .setNextPageToken(nextPageToken) - .addAllInstances(instances) + .addAllInstanceConfigs(instanceConfigs) .build(); mockInstanceAdmin.addResponse(expectedResponse); ProjectName parent = ProjectName.of("[PROJECT]"); - ListInstancesPagedResponse pagedListResponse = client.listInstances(parent); + ListInstanceConfigsPagedResponse pagedListResponse = client.listInstanceConfigs(parent); - List resources = Lists.newArrayList(pagedListResponse.iterateAll()); + List resources = Lists.newArrayList(pagedListResponse.iterateAll()); Assert.assertEquals(1, resources.size()); - Assert.assertEquals(expectedResponse.getInstancesList().get(0), resources.get(0)); + Assert.assertEquals(expectedResponse.getInstanceConfigsList().get(0), resources.get(0)); List actualRequests = mockInstanceAdmin.getRequests(); Assert.assertEquals(1, actualRequests.size()); - ListInstancesRequest actualRequest = (ListInstancesRequest) actualRequests.get(0); + ListInstanceConfigsRequest actualRequest = (ListInstanceConfigsRequest) actualRequests.get(0); Assert.assertEquals(parent, ProjectName.parse(actualRequest.getParent())); Assert.assertTrue( @@ -230,14 +262,14 @@ public void listInstancesTest() { @Test @SuppressWarnings("all") - public void listInstancesExceptionTest() throws Exception { + public void listInstanceConfigsExceptionTest() throws Exception { StatusRuntimeException exception = new StatusRuntimeException(Status.INVALID_ARGUMENT); mockInstanceAdmin.addException(exception); try { ProjectName parent = ProjectName.of("[PROJECT]"); - client.listInstances(parent); + client.listInstanceConfigs(parent); Assert.fail("No exception raised"); } catch (InvalidArgumentException e) { // Expected exception @@ -246,30 +278,23 @@ public void listInstancesExceptionTest() throws Exception { @Test @SuppressWarnings("all") - public void getInstanceTest() { - InstanceName name2 = InstanceName.of("[PROJECT]", "[INSTANCE]"); - InstanceConfigName config = InstanceConfigName.of("[PROJECT]", "[INSTANCE_CONFIG]"); + public void getInstanceConfigTest() { + InstanceConfigName name2 = InstanceConfigName.of("[PROJECT]", "[INSTANCE_CONFIG]"); String displayName = "displayName1615086568"; - int nodeCount = 1539922066; - Instance expectedResponse = - Instance.newBuilder() - .setName(name2.toString()) - .setConfig(config.toString()) - .setDisplayName(displayName) - .setNodeCount(nodeCount) - .build(); + InstanceConfig expectedResponse = + InstanceConfig.newBuilder().setName(name2.toString()).setDisplayName(displayName).build(); mockInstanceAdmin.addResponse(expectedResponse); - InstanceName name = InstanceName.of("[PROJECT]", "[INSTANCE]"); + InstanceConfigName name = InstanceConfigName.of("[PROJECT]", "[INSTANCE_CONFIG]"); - Instance actualResponse = client.getInstance(name); + InstanceConfig actualResponse = client.getInstanceConfig(name); Assert.assertEquals(expectedResponse, actualResponse); List actualRequests = mockInstanceAdmin.getRequests(); Assert.assertEquals(1, actualRequests.size()); - GetInstanceRequest actualRequest = (GetInstanceRequest) actualRequests.get(0); + GetInstanceConfigRequest actualRequest = (GetInstanceConfigRequest) actualRequests.get(0); - Assert.assertEquals(name, InstanceName.parse(actualRequest.getName())); + Assert.assertEquals(name, InstanceConfigName.parse(actualRequest.getName())); Assert.assertTrue( channelProvider.isHeaderSent( ApiClientHeaderProvider.getDefaultApiClientHeaderKey(), @@ -278,14 +303,14 @@ public void getInstanceTest() { @Test @SuppressWarnings("all") - public void getInstanceExceptionTest() throws Exception { + public void getInstanceConfigExceptionTest() throws Exception { StatusRuntimeException exception = new StatusRuntimeException(Status.INVALID_ARGUMENT); mockInstanceAdmin.addException(exception); try { - InstanceName name = InstanceName.of("[PROJECT]", "[INSTANCE]"); + InstanceConfigName name = InstanceConfigName.of("[PROJECT]", "[INSTANCE_CONFIG]"); - client.getInstance(name); + client.getInstanceConfig(name); Assert.fail("No exception raised"); } catch (InvalidArgumentException e) { // Expected exception @@ -294,40 +319,30 @@ public void getInstanceExceptionTest() throws Exception { @Test @SuppressWarnings("all") - public void createInstanceTest() throws Exception { - InstanceName name = InstanceName.of("[PROJECT]", "[INSTANCE]"); - InstanceConfigName config = InstanceConfigName.of("[PROJECT]", "[INSTANCE_CONFIG]"); - String displayName = "displayName1615086568"; - int nodeCount = 1539922066; - Instance expectedResponse = - Instance.newBuilder() - .setName(name.toString()) - .setConfig(config.toString()) - .setDisplayName(displayName) - .setNodeCount(nodeCount) - .build(); - Operation resultOperation = - Operation.newBuilder() - .setName("createInstanceTest") - .setDone(true) - .setResponse(Any.pack(expectedResponse)) + public void listInstancesTest() { + String nextPageToken = ""; + Instance instancesElement = Instance.newBuilder().build(); + List instances = Arrays.asList(instancesElement); + ListInstancesResponse expectedResponse = + ListInstancesResponse.newBuilder() + .setNextPageToken(nextPageToken) + .addAllInstances(instances) .build(); - mockInstanceAdmin.addResponse(resultOperation); + mockInstanceAdmin.addResponse(expectedResponse); ProjectName parent = ProjectName.of("[PROJECT]"); - String instanceId = "instanceId-2101995259"; - Instance instance = Instance.newBuilder().build(); - Instance actualResponse = client.createInstanceAsync(parent, instanceId, instance).get(); - Assert.assertEquals(expectedResponse, actualResponse); + ListInstancesPagedResponse pagedListResponse = client.listInstances(parent); + + List resources = Lists.newArrayList(pagedListResponse.iterateAll()); + Assert.assertEquals(1, resources.size()); + Assert.assertEquals(expectedResponse.getInstancesList().get(0), resources.get(0)); List actualRequests = mockInstanceAdmin.getRequests(); Assert.assertEquals(1, actualRequests.size()); - CreateInstanceRequest actualRequest = (CreateInstanceRequest) actualRequests.get(0); + ListInstancesRequest actualRequest = (ListInstancesRequest) actualRequests.get(0); Assert.assertEquals(parent, ProjectName.parse(actualRequest.getParent())); - Assert.assertEquals(instanceId, actualRequest.getInstanceId()); - Assert.assertEquals(instance, actualRequest.getInstance()); Assert.assertTrue( channelProvider.isHeaderSent( ApiClientHeaderProvider.getDefaultApiClientHeaderKey(), @@ -336,58 +351,46 @@ public void createInstanceTest() throws Exception { @Test @SuppressWarnings("all") - public void createInstanceExceptionTest() throws Exception { + public void listInstancesExceptionTest() throws Exception { StatusRuntimeException exception = new StatusRuntimeException(Status.INVALID_ARGUMENT); mockInstanceAdmin.addException(exception); try { ProjectName parent = ProjectName.of("[PROJECT]"); - String instanceId = "instanceId-2101995259"; - Instance instance = Instance.newBuilder().build(); - client.createInstanceAsync(parent, instanceId, instance).get(); + client.listInstances(parent); Assert.fail("No exception raised"); - } catch (ExecutionException e) { - Assert.assertEquals(InvalidArgumentException.class, e.getCause().getClass()); - InvalidArgumentException apiException = (InvalidArgumentException) e.getCause(); - Assert.assertEquals(StatusCode.Code.INVALID_ARGUMENT, apiException.getStatusCode().getCode()); + } catch (InvalidArgumentException e) { + // Expected exception } } @Test @SuppressWarnings("all") - public void updateInstanceTest() throws Exception { - InstanceName name = InstanceName.of("[PROJECT]", "[INSTANCE]"); + public void getInstanceTest() { + InstanceName name2 = InstanceName.of("[PROJECT]", "[INSTANCE]"); InstanceConfigName config = InstanceConfigName.of("[PROJECT]", "[INSTANCE_CONFIG]"); String displayName = "displayName1615086568"; int nodeCount = 1539922066; Instance expectedResponse = Instance.newBuilder() - .setName(name.toString()) + .setName(name2.toString()) .setConfig(config.toString()) .setDisplayName(displayName) .setNodeCount(nodeCount) .build(); - Operation resultOperation = - Operation.newBuilder() - .setName("updateInstanceTest") - .setDone(true) - .setResponse(Any.pack(expectedResponse)) - .build(); - mockInstanceAdmin.addResponse(resultOperation); + mockInstanceAdmin.addResponse(expectedResponse); - Instance instance = Instance.newBuilder().build(); - FieldMask fieldMask = FieldMask.newBuilder().build(); + InstanceName name = InstanceName.of("[PROJECT]", "[INSTANCE]"); - Instance actualResponse = client.updateInstanceAsync(instance, fieldMask).get(); + Instance actualResponse = client.getInstance(name); Assert.assertEquals(expectedResponse, actualResponse); List actualRequests = mockInstanceAdmin.getRequests(); Assert.assertEquals(1, actualRequests.size()); - UpdateInstanceRequest actualRequest = (UpdateInstanceRequest) actualRequests.get(0); + GetInstanceRequest actualRequest = (GetInstanceRequest) actualRequests.get(0); - Assert.assertEquals(instance, actualRequest.getInstance()); - Assert.assertEquals(fieldMask, actualRequest.getFieldMask()); + Assert.assertEquals(name, InstanceName.parse(actualRequest.getName())); Assert.assertTrue( channelProvider.isHeaderSent( ApiClientHeaderProvider.getDefaultApiClientHeaderKey(), @@ -396,20 +399,17 @@ public void updateInstanceTest() throws Exception { @Test @SuppressWarnings("all") - public void updateInstanceExceptionTest() throws Exception { + public void getInstanceExceptionTest() throws Exception { StatusRuntimeException exception = new StatusRuntimeException(Status.INVALID_ARGUMENT); mockInstanceAdmin.addException(exception); try { - Instance instance = Instance.newBuilder().build(); - FieldMask fieldMask = FieldMask.newBuilder().build(); + InstanceName name = InstanceName.of("[PROJECT]", "[INSTANCE]"); - client.updateInstanceAsync(instance, fieldMask).get(); + client.getInstance(name); Assert.fail("No exception raised"); - } catch (ExecutionException e) { - Assert.assertEquals(InvalidArgumentException.class, e.getCause().getClass()); - InvalidArgumentException apiException = (InvalidArgumentException) e.getCause(); - Assert.assertEquals(StatusCode.Code.INVALID_ARGUMENT, apiException.getStatusCode().getCode()); + } catch (InvalidArgumentException e) { + // Expected exception } } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/AbstractMockServerTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/AbstractMockServerTest.java index 3497b42bc7..a54a5b848a 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/AbstractMockServerTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/AbstractMockServerTest.java @@ -170,6 +170,10 @@ protected String getBaseUrl() { server.getPort()); } + protected int getPort() { + return server.getPort(); + } + protected ExecuteSqlRequest getLastExecuteSqlRequest() { List requests = mockSpanner.getRequests(); for (int i = requests.size() - 1; i >= 0; i--) { 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 e13e1d0c15..f5295ef96b 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 @@ -216,7 +216,7 @@ public static ConnectionImpl createConnection(ConnectionOptions options) { .thenAnswer( new Answer() { @Override - public ResultSet answer(InvocationOnMock invocation) throws Throwable { + public ResultSet answer(InvocationOnMock invocation) { if (select1ResultSet.nextCalled) { // create a new mock return new SimpleResultSet(createSelect1MockResultSet()); @@ -232,7 +232,7 @@ public ResultSet answer(InvocationOnMock invocation) throws Throwable { .then( new Answer() { @Override - public Timestamp answer(InvocationOnMock invocation) throws Throwable { + public Timestamp answer(InvocationOnMock invocation) { if (select1ResultSet.isNextCalled() || select1ResultSetWithStats.isNextCalled()) { return Timestamp.now(); } @@ -247,13 +247,13 @@ public Timestamp answer(InvocationOnMock invocation) throws Throwable { .thenAnswer( new Answer() { @Override - public TransactionManager answer(InvocationOnMock invocation) throws Throwable { + public TransactionManager answer(InvocationOnMock invocation) { TransactionContext txContext = mock(TransactionContext.class); when(txContext.executeQuery(Statement.of(SELECT))) .thenAnswer( new Answer() { @Override - public ResultSet answer(InvocationOnMock invocation) throws Throwable { + public ResultSet answer(InvocationOnMock invocation) { if (select1ResultSet.nextCalled) { // create a new mock return new SimpleResultSet(createSelect1MockResultSet()); @@ -274,13 +274,13 @@ public ResultSet answer(InvocationOnMock invocation) throws Throwable { .thenAnswer( new Answer() { @Override - public ReadOnlyTransaction answer(InvocationOnMock invocation) throws Throwable { + public ReadOnlyTransaction answer(InvocationOnMock invocation) { ReadOnlyTransaction tx = mock(ReadOnlyTransaction.class); when(tx.executeQuery(Statement.of(SELECT))) .thenAnswer( new Answer() { @Override - public ResultSet answer(InvocationOnMock invocation) throws Throwable { + public ResultSet answer(InvocationOnMock invocation) { if (select1ResultSet.nextCalled) { // create a new mock return new SimpleResultSet(createSelect1MockResultSet()); @@ -296,7 +296,7 @@ public ResultSet answer(InvocationOnMock invocation) throws Throwable { .then( new Answer() { @Override - public Timestamp answer(InvocationOnMock invocation) throws Throwable { + public Timestamp answer(InvocationOnMock invocation) { if (select1ResultSet.isNextCalled() || select1ResultSetWithStats.isNextCalled()) { return Timestamp.now(); @@ -314,7 +314,7 @@ public Timestamp answer(InvocationOnMock invocation) throws Throwable { .thenAnswer( new Answer() { @Override - public TransactionRunner answer(InvocationOnMock invocation) throws Throwable { + public TransactionRunner answer(InvocationOnMock invocation) { TransactionRunner runner = new TransactionRunner() { private Timestamp commitTimestamp; diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionStatementWithNoParametersTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionStatementWithNoParametersTest.java index 8012f255dc..e3be3cfeae 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionStatementWithNoParametersTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionStatementWithNoParametersTest.java @@ -32,7 +32,7 @@ public class ConnectionStatementWithNoParametersTest { private final StatementParser parser = StatementParser.INSTANCE; @Test - public void testExecuteGetAutocommit() throws Exception { + public void testExecuteGetAutocommit() { ParsedStatement statement = parser.parse(Statement.of("show variable autocommit")); ConnectionImpl connection = mock(ConnectionImpl.class); ConnectionStatementExecutorImpl executor = mock(ConnectionStatementExecutorImpl.class); @@ -43,7 +43,7 @@ public void testExecuteGetAutocommit() throws Exception { } @Test - public void testExecuteGetReadOnly() throws Exception { + public void testExecuteGetReadOnly() { ParsedStatement statement = parser.parse(Statement.of("show variable readonly")); ConnectionImpl connection = mock(ConnectionImpl.class); ConnectionStatementExecutorImpl executor = mock(ConnectionStatementExecutorImpl.class); @@ -54,7 +54,7 @@ public void testExecuteGetReadOnly() throws Exception { } @Test - public void testExecuteGetAutocommitDmlMode() throws Exception { + public void testExecuteGetAutocommitDmlMode() { ParsedStatement statement = parser.parse(Statement.of("show variable autocommit_dml_mode")); ConnectionImpl connection = mock(ConnectionImpl.class); ConnectionStatementExecutorImpl executor = mock(ConnectionStatementExecutorImpl.class); @@ -66,7 +66,7 @@ public void testExecuteGetAutocommitDmlMode() throws Exception { } @Test - public void testExecuteGetStatementTimeout() throws Exception { + public void testExecuteGetStatementTimeout() { ParsedStatement statement = parser.parse(Statement.of("show variable statement_timeout")); ConnectionImpl connection = mock(ConnectionImpl.class); ConnectionStatementExecutorImpl executor = mock(ConnectionStatementExecutorImpl.class); @@ -79,7 +79,7 @@ public void testExecuteGetStatementTimeout() throws Exception { } @Test - public void testExecuteGetReadTimestamp() throws Exception { + public void testExecuteGetReadTimestamp() { ParsedStatement statement = parser.parse(Statement.of("show variable read_timestamp")); ConnectionImpl connection = mock(ConnectionImpl.class); ConnectionStatementExecutorImpl executor = mock(ConnectionStatementExecutorImpl.class); @@ -91,7 +91,7 @@ public void testExecuteGetReadTimestamp() throws Exception { } @Test - public void testExecuteGetCommitTimestamp() throws Exception { + public void testExecuteGetCommitTimestamp() { ParsedStatement statement = parser.parse(Statement.of("show variable commit_timestamp")); ConnectionImpl connection = mock(ConnectionImpl.class); ConnectionStatementExecutorImpl executor = mock(ConnectionStatementExecutorImpl.class); @@ -103,7 +103,7 @@ public void testExecuteGetCommitTimestamp() throws Exception { } @Test - public void testExecuteGetReadOnlyStaleness() throws Exception { + public void testExecuteGetReadOnlyStaleness() { ParsedStatement statement = parser.parse(Statement.of("show variable read_only_staleness")); ConnectionImpl connection = mock(ConnectionImpl.class); ConnectionStatementExecutorImpl executor = mock(ConnectionStatementExecutorImpl.class); @@ -115,7 +115,7 @@ public void testExecuteGetReadOnlyStaleness() throws Exception { } @Test - public void testExecuteGetOptimizerVersion() throws Exception { + public void testExecuteGetOptimizerVersion() { ParsedStatement statement = parser.parse(Statement.of("show variable optimizer_version")); ConnectionImpl connection = mock(ConnectionImpl.class); ConnectionStatementExecutorImpl executor = mock(ConnectionStatementExecutorImpl.class); @@ -127,7 +127,7 @@ public void testExecuteGetOptimizerVersion() throws Exception { } @Test - public void testExecuteBegin() throws Exception { + public void testExecuteBegin() { ParsedStatement subject = parser.parse(Statement.of("begin")); for (String statement : subject.getClientSideStatement().getExampleStatements()) { ConnectionImpl connection = mock(ConnectionImpl.class); @@ -140,7 +140,7 @@ public void testExecuteBegin() throws Exception { } @Test - public void testExecuteCommit() throws Exception { + public void testExecuteCommit() { ParsedStatement subject = parser.parse(Statement.of("commit")); for (String statement : subject.getClientSideStatement().getExampleStatements()) { ConnectionImpl connection = mock(ConnectionImpl.class); @@ -153,7 +153,7 @@ public void testExecuteCommit() throws Exception { } @Test - public void testExecuteRollback() throws Exception { + public void testExecuteRollback() { ParsedStatement subject = parser.parse(Statement.of("rollback")); for (String statement : subject.getClientSideStatement().getExampleStatements()) { ConnectionImpl connection = mock(ConnectionImpl.class); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionStatementWithOneParameterTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionStatementWithOneParameterTest.java index b07c308a0e..56c5c689b0 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionStatementWithOneParameterTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionStatementWithOneParameterTest.java @@ -34,7 +34,7 @@ public class ConnectionStatementWithOneParameterTest { private final StatementParser parser = StatementParser.INSTANCE; @Test - public void testExecuteSetAutcommit() throws Exception { + public void testExecuteSetAutcommit() { ParsedStatement subject = parser.parse(Statement.of("set autocommit = true")); ConnectionImpl connection = mock(ConnectionImpl.class); ConnectionStatementExecutorImpl executor = mock(ConnectionStatementExecutorImpl.class); @@ -49,7 +49,7 @@ public void testExecuteSetAutcommit() throws Exception { } @Test - public void testExecuteSetReadOnly() throws Exception { + public void testExecuteSetReadOnly() { ParsedStatement subject = parser.parse(Statement.of("set readonly = true")); ConnectionImpl connection = mock(ConnectionImpl.class); ConnectionStatementExecutorImpl executor = mock(ConnectionStatementExecutorImpl.class); @@ -64,7 +64,7 @@ public void testExecuteSetReadOnly() throws Exception { } @Test - public void testExecuteSetAutcommitDmlMode() throws Exception { + public void testExecuteSetAutcommitDmlMode() { ParsedStatement subject = parser.parse(Statement.of("set autocommit_dml_mode='foo'")); ConnectionImpl connection = mock(ConnectionImpl.class); ConnectionStatementExecutorImpl executor = mock(ConnectionStatementExecutorImpl.class); @@ -79,7 +79,7 @@ public void testExecuteSetAutcommitDmlMode() throws Exception { } @Test - public void testExecuteSetStatementTimeout() throws Exception { + public void testExecuteSetStatementTimeout() { ConnectionStatementExecutorImpl executor = mock(ConnectionStatementExecutorImpl.class); when(executor.statementSetStatementTimeout(any(Duration.class))).thenCallRealMethod(); ConnectionImpl connection = mock(ConnectionImpl.class); @@ -109,7 +109,7 @@ public void testExecuteSetStatementTimeout() throws Exception { } @Test - public void testExecuteSetReadOnlyStaleness() throws Exception { + public void testExecuteSetReadOnlyStaleness() { ParsedStatement subject = parser.parse(Statement.of("set read_only_staleness='foo'")); ConnectionImpl connection = mock(ConnectionImpl.class); ConnectionStatementExecutorImpl executor = mock(ConnectionStatementExecutorImpl.class); @@ -149,7 +149,7 @@ private String timestampBoundToString(TimestampBound staleness) { } @Test - public void testExecuteSetOptimizerVersion() throws Exception { + public void testExecuteSetOptimizerVersion() { ParsedStatement subject = parser.parse(Statement.of("set optimizer_version='foo'")); ConnectionImpl connection = mock(ConnectionImpl.class); ConnectionStatementExecutorImpl executor = mock(ConnectionStatementExecutorImpl.class); @@ -164,7 +164,7 @@ public void testExecuteSetOptimizerVersion() throws Exception { } @Test - public void testExecuteSetTransaction() throws Exception { + public void testExecuteSetTransaction() { ParsedStatement subject = parser.parse(Statement.of("set transaction read_only")); ConnectionImpl connection = mock(ConnectionImpl.class); ConnectionStatementExecutorImpl executor = mock(ConnectionStatementExecutorImpl.class); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/CredentialsServiceTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/CredentialsServiceTest.java index 7a48c577c2..e8dc7a4f87 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/CredentialsServiceTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/CredentialsServiceTest.java @@ -54,14 +54,14 @@ GoogleCredentials internalGetApplicationDefault() throws IOException { }; @Test - public void testCreateCredentialsDefault() throws Exception { + public void testCreateCredentialsDefault() { ServiceAccountCredentials credentials = (ServiceAccountCredentials) service.createCredentials(null); assertThat(credentials.getProjectId(), is(equalTo(APP_DEFAULT_PROJECT_ID))); } @Test - public void testCreateCredentialsFile() throws IOException { + public void testCreateCredentialsFile() { ServiceAccountCredentials credentials = (ServiceAccountCredentials) service.createCredentials(FILE_TEST_PATH); assertThat(credentials.getProjectId(), is(equalTo(TEST_PROJECT_ID))); 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 50f576fe69..4f02fb9a36 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 @@ -19,6 +19,7 @@ import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; import static org.mockito.Matchers.anyListOf; import static org.mockito.Matchers.anyString; @@ -53,9 +54,7 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.mockito.ArgumentMatcher; @@ -65,8 +64,6 @@ @RunWith(JUnit4.class) public class DdlBatchTest { - @Rule public ExpectedException exception = ExpectedException.none(); - private DdlClient createDefaultMockDdlClient() { return createDefaultMockDdlClient(false, 0L); } @@ -139,8 +136,12 @@ private DdlBatch createSubject(DdlClient ddlClient, DatabaseClient dbClient) { @Test public void testExecuteQuery() { DdlBatch batch = createSubject(); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - batch.executeQuery(mock(ParsedStatement.class), AnalyzeMode.NONE); + try { + batch.executeQuery(mock(ParsedStatement.class), AnalyzeMode.NONE); + fail("expected FAILED_PRECONDITION"); + } catch (SpannerException e) { + assertEquals(ErrorCode.FAILED_PRECONDITION, e.getErrorCode()); + } } @Test @@ -165,38 +166,58 @@ public void testExecuteMetadataQuery() { @Test public void testExecuteUpdate() { DdlBatch batch = createSubject(); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - batch.executeUpdate(mock(ParsedStatement.class)); + try { + batch.executeUpdate(mock(ParsedStatement.class)); + fail("expected FAILED_PRECONDITION"); + } catch (SpannerException e) { + assertEquals(ErrorCode.FAILED_PRECONDITION, e.getErrorCode()); + } } @Test public void testGetCommitTimestamp() { DdlBatch batch = createSubject(); batch.runBatch(); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - batch.getCommitTimestamp(); + try { + batch.getCommitTimestamp(); + fail("expected FAILED_PRECONDITION"); + } catch (SpannerException e) { + assertEquals(ErrorCode.FAILED_PRECONDITION, e.getErrorCode()); + } } @Test public void testGetReadTimestamp() { DdlBatch batch = createSubject(); batch.runBatch(); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - batch.getReadTimestamp(); + try { + batch.getReadTimestamp(); + fail("expected FAILED_PRECONDITION"); + } catch (SpannerException e) { + assertEquals(ErrorCode.FAILED_PRECONDITION, e.getErrorCode()); + } } @Test public void testWrite() { DdlBatch batch = createSubject(); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - batch.write(Mutation.newInsertBuilder("foo").build()); + try { + batch.write(Mutation.newInsertBuilder("foo").build()); + fail("expected FAILED_PRECONDITION"); + } catch (SpannerException e) { + assertEquals(ErrorCode.FAILED_PRECONDITION, e.getErrorCode()); + } } @Test public void testWriteIterable() { DdlBatch batch = createSubject(); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - batch.write(Arrays.asList(Mutation.newInsertBuilder("foo").build())); + try { + batch.write(Arrays.asList(Mutation.newInsertBuilder("foo").build())); + fail("expected FAILED_PRECONDITION"); + } catch (SpannerException e) { + assertEquals(ErrorCode.FAILED_PRECONDITION, e.getErrorCode()); + } } @Test @@ -471,22 +492,34 @@ public void run() { }, 100, TimeUnit.MILLISECONDS); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.CANCELLED)); - batch.runBatch(); + try { + batch.runBatch(); + fail("expected CANCELLED"); + } catch (SpannerException e) { + assertEquals(ErrorCode.CANCELLED, e.getErrorCode()); + } } @Test public void testCommit() { DdlBatch batch = createSubject(); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - batch.commit(); + try { + batch.commit(); + fail("expected FAILED_PRECONDITION"); + } catch (SpannerException e) { + assertEquals(ErrorCode.FAILED_PRECONDITION, e.getErrorCode()); + } } @Test public void testRollback() { DdlBatch batch = createSubject(); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - batch.rollback(); + try { + batch.rollback(); + fail("expected FAILED_PRECONDITION"); + } catch (SpannerException e) { + assertEquals(ErrorCode.FAILED_PRECONDITION, e.getErrorCode()); + } } @Test diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DirectExecuteResultSetTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DirectExecuteResultSetTest.java index 477c872179..237c6af718 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DirectExecuteResultSetTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/DirectExecuteResultSetTest.java @@ -144,8 +144,7 @@ private void callMethods( } @Test - public void testValidMethodCall() - throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { + public void testValidMethodCall() throws IllegalArgumentException { ResultSet delegate = mock(ResultSet.class); when(delegate.next()).thenReturn(true, true, false); DirectExecuteResultSet subject = DirectExecuteResultSet.ofResultSet(delegate); 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 2ef5709341..e841601db7 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 @@ -18,6 +18,8 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; import static org.mockito.Matchers.anyListOf; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; @@ -31,21 +33,18 @@ import com.google.cloud.spanner.connection.StatementParser.StatementType; import com.google.cloud.spanner.connection.UnitOfWork.UnitOfWorkState; import java.util.Arrays; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public class DmlBatchTest { + private final ParsedStatement statement1 = StatementParser.INSTANCE.parse(Statement.of("UPDATE FOO SET BAR=1 WHERE BAZ=2")); private final ParsedStatement statement2 = StatementParser.INSTANCE.parse(Statement.of("UPDATE FOO SET BAR=2 WHERE BAZ=3")); - @Rule public ExpectedException exception = ExpectedException.none(); - private DmlBatch createSubject() { UnitOfWork transaction = mock(UnitOfWork.class); when(transaction.executeBatchUpdate(Arrays.asList(statement1, statement2))) @@ -63,23 +62,35 @@ private DmlBatch createSubject(UnitOfWork transaction) { @Test public void testExecuteQuery() { DmlBatch batch = createSubject(); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - batch.executeQuery(mock(ParsedStatement.class), AnalyzeMode.NONE); + try { + batch.executeQuery(mock(ParsedStatement.class), AnalyzeMode.NONE); + fail("Expected exception"); + } catch (SpannerException e) { + assertEquals(ErrorCode.FAILED_PRECONDITION, e.getErrorCode()); + } } @Test public void testExecuteDdl() { DmlBatch batch = createSubject(); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - batch.executeDdl(mock(ParsedStatement.class)); + try { + batch.executeDdl(mock(ParsedStatement.class)); + fail("Expected exception"); + } catch (SpannerException e) { + assertEquals(ErrorCode.FAILED_PRECONDITION, e.getErrorCode()); + } } @Test public void testGetReadTimestamp() { DmlBatch batch = createSubject(); batch.runBatch(); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - batch.getReadTimestamp(); + try { + batch.getReadTimestamp(); + fail("Expected exception"); + } catch (SpannerException e) { + assertEquals(ErrorCode.FAILED_PRECONDITION, e.getErrorCode()); + } } @Test @@ -92,22 +103,34 @@ public void testIsReadOnly() { public void testGetCommitTimestamp() { DmlBatch batch = createSubject(); batch.runBatch(); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - batch.getCommitTimestamp(); + try { + batch.getCommitTimestamp(); + fail("Expected exception"); + } catch (SpannerException e) { + assertEquals(ErrorCode.FAILED_PRECONDITION, e.getErrorCode()); + } } @Test public void testWrite() { DmlBatch batch = createSubject(); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - batch.write(Mutation.newInsertBuilder("foo").build()); + try { + batch.write(Mutation.newInsertBuilder("foo").build()); + fail("Expected exception"); + } catch (SpannerException e) { + assertEquals(ErrorCode.FAILED_PRECONDITION, e.getErrorCode()); + } } @Test public void testWriteIterable() { DmlBatch batch = createSubject(); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - batch.write(Arrays.asList(Mutation.newInsertBuilder("foo").build())); + try { + batch.write(Arrays.asList(Mutation.newInsertBuilder("foo").build())); + fail("Expected exception"); + } catch (SpannerException e) { + assertEquals(ErrorCode.FAILED_PRECONDITION, e.getErrorCode()); + } } @Test @@ -150,14 +173,22 @@ public void testGetStateAndIsActive() { @Test public void testCommit() { DmlBatch batch = createSubject(); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - batch.commit(); + try { + batch.commit(); + fail("Expected exception"); + } catch (SpannerException e) { + assertEquals(ErrorCode.FAILED_PRECONDITION, e.getErrorCode()); + } } @Test public void testRollback() { DmlBatch batch = createSubject(); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - batch.rollback(); + try { + batch.rollback(); + fail("Expected exception"); + } catch (SpannerException e) { + assertEquals(ErrorCode.FAILED_PRECONDITION, e.getErrorCode()); + } } } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ITAbstractSpannerTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ITAbstractSpannerTest.java index a14b9aa4a7..fae463ceb1 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ITAbstractSpannerTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ITAbstractSpannerTest.java @@ -16,10 +16,10 @@ package com.google.cloud.spanner.connection; +import com.google.cloud.NoCredentials; import com.google.cloud.spanner.Database; import com.google.cloud.spanner.ErrorCode; import com.google.cloud.spanner.GceTestEnvConfig; -import com.google.cloud.spanner.IntegrationTest; import com.google.cloud.spanner.IntegrationTestEnv; import com.google.cloud.spanner.ResultSet; import com.google.cloud.spanner.SpannerExceptionFactory; @@ -33,7 +33,6 @@ import com.google.cloud.spanner.connection.StatementParser.ParsedStatement; import com.google.common.base.Preconditions; import com.google.common.base.Strings; -import java.io.IOException; import java.lang.reflect.Field; import java.nio.file.Files; import java.nio.file.Paths; @@ -41,17 +40,15 @@ import java.util.Collections; import java.util.List; import java.util.Random; -import java.util.concurrent.ExecutionException; +import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.ClassRule; -import org.junit.experimental.categories.Category; /** * Base class for integration tests. This class is located in this package to be able to access * package-private methods of the Connection API */ -@Category(IntegrationTest.class) public abstract class ITAbstractSpannerTest { protected class ITConnectionProvider implements GenericConnectionProvider { public ITConnectionProvider() {} @@ -182,14 +179,22 @@ public static StringBuilder extractConnectionUrl(SpannerOptions options, Databas url.append(options.getHost().substring(options.getHost().indexOf(':') + 1)); } url.append("/").append(database.getId().getName()); + if (options.getCredentials() == NoCredentials.getInstance()) { + url.append(";usePlainText=true"); + } return url; } @BeforeClass - public static void setup() throws IOException, InterruptedException, ExecutionException { + public static void setup() { database = env.getTestHelper().createTestDatabase(); } + @AfterClass + public static void teardown() { + ConnectionOptions.closeSpanner(); + } + /** * Creates a new default connection to a test database. Use the method {@link * ITAbstractSpannerTest#appendConnectionUri(StringBuilder)} to append additional connection @@ -268,7 +273,7 @@ protected boolean doCreateDefaultTestTable() { } @Before - public void createTestTable() throws Exception { + public void createTestTable() { if (doCreateDefaultTestTable()) { try (Connection connection = createConnection()) { connection.setAutocommit(true); 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 3fb0e3a04d..118f596c86 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 @@ -21,10 +21,14 @@ import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import com.google.api.core.ApiFuture; import com.google.cloud.Timestamp; +import com.google.cloud.spanner.AsyncResultSet; import com.google.cloud.spanner.DatabaseClient; import com.google.cloud.spanner.ErrorCode; import com.google.cloud.spanner.Key; @@ -46,15 +50,12 @@ import java.util.Calendar; import java.util.List; import java.util.concurrent.TimeUnit; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public class ReadOnlyTransactionTest { - @Rule public ExpectedException exception = ExpectedException.none(); private static final class SimpleReadOnlyTransaction implements com.google.cloud.spanner.ReadOnlyTransaction { @@ -132,6 +133,34 @@ public void close() {} public Timestamp getReadTimestamp() { return readTimestamp; } + + @Override + public AsyncResultSet readAsync( + String table, KeySet keys, Iterable columns, ReadOption... options) { + return null; + } + + @Override + public AsyncResultSet readUsingIndexAsync( + String table, String index, KeySet keys, Iterable columns, ReadOption... options) { + return null; + } + + @Override + public ApiFuture readRowAsync(String table, Key key, Iterable columns) { + return null; + } + + @Override + public ApiFuture readRowUsingIndexAsync( + String table, String index, Key key, Iterable columns) { + return null; + } + + @Override + public AsyncResultSet executeQueryAsync(Statement statement, QueryOption... options) { + return null; + } } private ReadOnlyTransaction createSubject() { @@ -153,44 +182,68 @@ private ReadOnlyTransaction createSubject(TimestampBound staleness) { public void testExecuteDdl() { ParsedStatement ddl = mock(ParsedStatement.class); when(ddl.getType()).thenReturn(StatementType.DDL); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - createSubject().executeDdl(ddl); + try { + createSubject().executeDdl(ddl); + fail("Expected exception"); + } catch (SpannerException ex) { + assertEquals(ErrorCode.FAILED_PRECONDITION, ex.getErrorCode()); + } } @Test public void testExecuteUpdate() { ParsedStatement update = mock(ParsedStatement.class); when(update.getType()).thenReturn(StatementType.UPDATE); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - createSubject().executeUpdate(update); + try { + createSubject().executeUpdate(update); + fail("Expected exception"); + } catch (SpannerException ex) { + assertEquals(ErrorCode.FAILED_PRECONDITION, ex.getErrorCode()); + } } @Test public void testWrite() { Mutation mutation = Mutation.newInsertBuilder("foo").build(); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - createSubject().write(mutation); + try { + createSubject().write(mutation); + fail("Expected exception"); + } catch (SpannerException ex) { + assertEquals(ErrorCode.FAILED_PRECONDITION, ex.getErrorCode()); + } } @Test public void testWriteIterable() { Mutation mutation = Mutation.newInsertBuilder("foo").build(); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - createSubject().write(Arrays.asList(mutation, mutation)); + try { + createSubject().write(Arrays.asList(mutation, mutation)); + fail("Expected exception"); + } catch (SpannerException ex) { + assertEquals(ErrorCode.FAILED_PRECONDITION, ex.getErrorCode()); + } } @Test public void testRunBatch() { ReadOnlyTransaction subject = createSubject(); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - subject.runBatch(); + try { + subject.runBatch(); + fail("Expected exception"); + } catch (SpannerException ex) { + assertEquals(ErrorCode.FAILED_PRECONDITION, ex.getErrorCode()); + } } @Test public void testAbortBatch() { ReadOnlyTransaction subject = createSubject(); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - subject.abortBatch(); + try { + subject.abortBatch(); + fail("Expected exception"); + } catch (SpannerException ex) { + assertEquals(ErrorCode.FAILED_PRECONDITION, ex.getErrorCode()); + } } @Test @@ -198,8 +251,12 @@ public void testGetCommitTimestamp() { ReadOnlyTransaction transaction = createSubject(); transaction.commit(); assertThat(transaction.getState(), is(UnitOfWorkState.COMMITTED)); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - transaction.getCommitTimestamp(); + try { + transaction.getCommitTimestamp(); + fail("Expected exception"); + } catch (SpannerException ex) { + assertEquals(ErrorCode.FAILED_PRECONDITION, ex.getErrorCode()); + } } @Test 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 d244f1d46a..e0cd8db9a6 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 @@ -22,6 +22,8 @@ import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; import static org.mockito.Matchers.any; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; @@ -48,10 +50,7 @@ import com.google.spanner.v1.ResultSetStats; import java.util.Arrays; import java.util.Collections; -import java.util.concurrent.ExecutionException; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.mockito.invocation.InvocationOnMock; @@ -60,8 +59,6 @@ @RunWith(JUnit4.class) public class ReadWriteTransactionTest { - @Rule public final ExpectedException exception = ExpectedException.none(); - private enum CommitBehavior { SUCCEED, FAIL, @@ -147,7 +144,7 @@ private ReadWriteTransaction createSubject( .thenAnswer( new Answer() { @Override - public TransactionManager answer(InvocationOnMock invocation) throws Throwable { + public TransactionManager answer(InvocationOnMock invocation) { TransactionContext txContext = mock(TransactionContext.class); when(txContext.executeQuery(any(Statement.class))) .thenReturn(mock(ResultSet.class)); @@ -173,22 +170,34 @@ public void testExecuteDdl() { when(statement.getType()).thenReturn(StatementType.DDL); ReadWriteTransaction transaction = createSubject(); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - transaction.executeDdl(statement); + try { + transaction.executeDdl(statement); + fail("Expected exception"); + } catch (SpannerException ex) { + assertEquals(ErrorCode.FAILED_PRECONDITION, ex.getErrorCode()); + } } @Test public void testRunBatch() { ReadWriteTransaction subject = createSubject(); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - subject.runBatch(); + try { + subject.runBatch(); + fail("Expected exception"); + } catch (SpannerException ex) { + assertEquals(ErrorCode.FAILED_PRECONDITION, ex.getErrorCode()); + } } @Test public void testAbortBatch() { ReadWriteTransaction subject = createSubject(); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - subject.abortBatch(); + try { + subject.abortBatch(); + fail("Expected exception"); + } catch (SpannerException ex) { + assertEquals(ErrorCode.FAILED_PRECONDITION, ex.getErrorCode()); + } } @Test @@ -261,9 +270,12 @@ public void testGetCommitTimestampBeforeCommit() { ReadWriteTransaction transaction = createSubject(); assertThat(transaction.executeUpdate(parsedStatement), is(1L)); - - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - transaction.getCommitTimestamp(); + try { + transaction.getCommitTimestamp(); + fail("Expected exception"); + } catch (SpannerException ex) { + assertEquals(ErrorCode.FAILED_PRECONDITION, ex.getErrorCode()); + } } @Test @@ -291,9 +303,12 @@ public void testGetReadTimestamp() { ReadWriteTransaction transaction = createSubject(); assertThat(transaction.executeQuery(parsedStatement, AnalyzeMode.NONE), is(notNullValue())); - - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - transaction.getReadTimestamp(); + try { + transaction.getReadTimestamp(); + fail("Expected exception"); + } catch (SpannerException ex) { + assertEquals(ErrorCode.FAILED_PRECONDITION, ex.getErrorCode()); + } } @Test @@ -454,7 +469,7 @@ public void testRetry() { } @Test - public void testChecksumResultSet() throws InterruptedException, ExecutionException { + public void testChecksumResultSet() { DatabaseClient client = mock(DatabaseClient.class); ReadWriteTransaction transaction = ReadWriteTransaction.newBuilder() @@ -518,7 +533,7 @@ public void testChecksumResultSet() throws InterruptedException, ExecutionExcept } @Test - public void testChecksumResultSetWithArray() throws InterruptedException, ExecutionException { + public void testChecksumResultSetWithArray() { DatabaseClient client = mock(DatabaseClient.class); ReadWriteTransaction transaction = ReadWriteTransaction.newBuilder() diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReplaceableForwardingResultSetTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReplaceableForwardingResultSetTest.java index e95f23a865..a30b15e6c2 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReplaceableForwardingResultSetTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ReplaceableForwardingResultSetTest.java @@ -195,8 +195,7 @@ private void callMethods( } @Test - public void testValidMethodCall() - throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { + public void testValidMethodCall() throws IllegalArgumentException { ResultSet delegate = mock(ResultSet.class); when(delegate.next()).thenReturn(true, true, false); try (ReplaceableForwardingResultSet subject = new ReplaceableForwardingResultSet(delegate)) { 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 7286b426d4..e73eb8e0b2 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 @@ -24,8 +24,10 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import com.google.api.core.ApiFuture; import com.google.api.gax.longrunning.OperationFuture; import com.google.cloud.Timestamp; +import com.google.cloud.spanner.AsyncResultSet; import com.google.cloud.spanner.DatabaseClient; import com.google.cloud.spanner.ErrorCode; import com.google.cloud.spanner.Key; @@ -230,6 +232,34 @@ public void close() {} public Timestamp getReadTimestamp() { return readTimestamp; } + + @Override + public AsyncResultSet readAsync( + String table, KeySet keys, Iterable columns, ReadOption... options) { + return null; + } + + @Override + public AsyncResultSet readUsingIndexAsync( + String table, String index, KeySet keys, Iterable columns, ReadOption... options) { + return null; + } + + @Override + public ApiFuture readRowAsync(String table, Key key, Iterable columns) { + return null; + } + + @Override + public ApiFuture readRowUsingIndexAsync( + String table, String index, Key key, Iterable columns) { + return null; + } + + @Override + public AsyncResultSet executeQueryAsync(Statement statement, QueryOption... options) { + return null; + } } private DdlClient createDefaultMockDdlClient() { @@ -346,7 +376,7 @@ public Long answer(InvocationOnMock invocation) throws Throwable { .thenAnswer( new Answer() { @Override - public TransactionRunner answer(InvocationOnMock invocation) throws Throwable { + public TransactionRunner answer(InvocationOnMock invocation) { TransactionRunner runner = new TransactionRunner() { private Timestamp commitTimestamp; diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/SpannerPoolTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/SpannerPoolTest.java index bfd413001b..c0145203ce 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/SpannerPoolTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/SpannerPoolTest.java @@ -35,7 +35,6 @@ import com.google.cloud.spanner.connection.ConnectionImpl.LeakedConnectionException; import com.google.cloud.spanner.connection.SpannerPool.CheckAndCloseSpannersMode; import java.io.ByteArrayOutputStream; -import java.io.IOException; import java.io.OutputStream; import java.util.logging.Handler; import java.util.logging.Logger; @@ -174,13 +173,13 @@ private void attachLogCapturer() { log.addHandler(customLogHandler); } - public String getTestCapturedLog() throws IOException { + public String getTestCapturedLog() { customLogHandler.flush(); return logCapturingStream.toString(); } @Test - public void testRemoveConnectionOptionsNotRegistered() throws IOException { + public void testRemoveConnectionOptionsNotRegistered() { attachLogCapturer(); final String expectedLogPart = "There is no Spanner registered for ConnectionOptions"; SpannerPool pool = createSubjectAndMocks(); @@ -191,7 +190,7 @@ public void testRemoveConnectionOptionsNotRegistered() throws IOException { } @Test - public void testRemoveConnectionConnectionNotRegistered() throws IOException { + public void testRemoveConnectionConnectionNotRegistered() { attachLogCapturer(); final String expectedLogPart = "There are no connections registered for ConnectionOptions"; SpannerPool pool = createSubjectAndMocks(); @@ -202,7 +201,7 @@ public void testRemoveConnectionConnectionNotRegistered() throws IOException { } @Test - public void testRemoveConnectionConnectionAlreadyRemoved() throws IOException { + public void testRemoveConnectionConnectionAlreadyRemoved() { attachLogCapturer(); final String expectedLogPart = "There are no connections registered for ConnectionOptions"; SpannerPool pool = createSubjectAndMocks(); @@ -214,7 +213,7 @@ public void testRemoveConnectionConnectionAlreadyRemoved() throws IOException { } @Test - public void testCloseSpanner() throws IOException { + public void testCloseSpanner() { SpannerPool pool = createSubjectAndMocks(); Spanner spanner = pool.getSpanner(options1, connection1); // verify that closing is not possible until all connections have been removed @@ -246,7 +245,7 @@ public void testCloseSpanner() throws IOException { } @Test - public void testLeakedConnection() throws IOException { + public void testLeakedConnection() { ConnectionOptions options = ConnectionOptions.newBuilder() .setCredentials(NoCredentials.getInstance()) diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/StatementResultImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/StatementResultImplTest.java index f641a51ed4..c28d3b75b9 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/StatementResultImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/StatementResultImplTest.java @@ -20,37 +20,45 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; import com.google.cloud.Timestamp; import com.google.cloud.spanner.ErrorCode; import com.google.cloud.spanner.ResultSet; +import com.google.cloud.spanner.SpannerException; import com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType; import com.google.cloud.spanner.connection.StatementResult.ResultType; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public class StatementResultImplTest { - @Rule public ExpectedException exception = ExpectedException.none(); @Test public void testNoResultGetResultSet() { StatementResult subject = StatementResultImpl.noResult(); assertThat(subject.getResultType(), is(equalTo(ResultType.NO_RESULT))); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - subject.getResultSet(); + try { + subject.getResultSet(); + fail("Expected exception"); + } catch (SpannerException ex) { + assertEquals(ErrorCode.FAILED_PRECONDITION, ex.getErrorCode()); + } } @Test public void testNoResultGetUpdateCount() { StatementResult subject = StatementResultImpl.noResult(); assertThat(subject.getResultType(), is(equalTo(ResultType.NO_RESULT))); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - subject.getUpdateCount(); + try { + subject.getUpdateCount(); + fail("Expected exception"); + } catch (SpannerException ex) { + assertEquals(ErrorCode.FAILED_PRECONDITION, ex.getErrorCode()); + } } @Test @@ -64,16 +72,24 @@ public void testResultSetGetResultSet() { public void testResultSetGetUpdateCount() { StatementResult subject = StatementResultImpl.of(mock(ResultSet.class)); assertThat(subject.getResultType(), is(equalTo(ResultType.RESULT_SET))); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - subject.getUpdateCount(); + try { + subject.getUpdateCount(); + fail("Expected exception"); + } catch (SpannerException ex) { + assertEquals(ErrorCode.FAILED_PRECONDITION, ex.getErrorCode()); + } } @Test public void testUpdateCountGetResultSet() { StatementResult subject = StatementResultImpl.of(1L); assertThat(subject.getResultType(), is(equalTo(ResultType.UPDATE_COUNT))); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - subject.getResultSet(); + try { + subject.getResultSet(); + fail("Expected exception"); + } catch (SpannerException ex) { + assertEquals(ErrorCode.FAILED_PRECONDITION, ex.getErrorCode()); + } } @Test diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/StatementTimeoutTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/StatementTimeoutTest.java index d44dc7086d..e483a50279 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/StatementTimeoutTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/StatementTimeoutTest.java @@ -20,6 +20,8 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyListOf; import static org.mockito.Mockito.doAnswer; @@ -51,9 +53,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.mockito.Matchers; @@ -62,6 +62,7 @@ @RunWith(JUnit4.class) public class StatementTimeoutTest { + private static final String URI = "cloudspanner:/projects/test-project-123/instances/test-instance/databases/test-database"; private static final String SLOW_SELECT = "SELECT foo FROM bar"; @@ -100,8 +101,6 @@ private enum CommitRollbackBehavior { SLOW_ROLLBACK; } - @Rule public ExpectedException expected = ExpectedException.none(); - private static final class DelayedQueryExecution implements Answer { @Override public ResultSet answer(InvocationOnMock invocation) throws Throwable { @@ -192,15 +191,14 @@ private ConnectionImpl createConnection( .thenAnswer( new Answer() { @Override - public TransactionManager answer(InvocationOnMock invocation) throws Throwable { + public TransactionManager answer(InvocationOnMock invocation) { TransactionManager txManager = mock(TransactionManager.class); when(txManager.getState()).thenReturn(null, TransactionState.STARTED); when(txManager.begin()) .thenAnswer( new Answer() { @Override - public TransactionContext answer(InvocationOnMock invocation) - throws Throwable { + public TransactionContext answer(InvocationOnMock invocation) { TransactionContext txContext = mock(TransactionContext.class); when(txContext.executeQuery(Statement.of(SLOW_SELECT))) .thenAnswer(new DelayedQueryExecution()); @@ -273,8 +271,12 @@ public void testTimeoutExceptionReadOnlyAutocommit() { .build())) { connection.setReadOnly(true); connection.setStatementTimeout(TIMEOUT_FOR_SLOW_STATEMENTS, TimeUnit.MILLISECONDS); - expected.expect(SpannerExceptionMatcher.matchCode(ErrorCode.DEADLINE_EXCEEDED)); - connection.executeQuery(Statement.of(SLOW_SELECT)); + try { + connection.executeQuery(Statement.of(SLOW_SELECT)); + fail("Expected exception"); + } catch (SpannerException ex) { + assertEquals(ErrorCode.DEADLINE_EXCEEDED, ex.getErrorCode()); + } } } @@ -315,8 +317,12 @@ public void testTimeoutExceptionReadOnlyTransactional() { connection.setReadOnly(true); connection.setAutocommit(false); connection.setStatementTimeout(TIMEOUT_FOR_SLOW_STATEMENTS, TimeUnit.MILLISECONDS); - expected.expect(SpannerExceptionMatcher.matchCode(ErrorCode.DEADLINE_EXCEEDED)); - connection.executeQuery(Statement.of(SLOW_SELECT)); + try { + connection.executeQuery(Statement.of(SLOW_SELECT)); + fail("Expected exception"); + } catch (SpannerException ex) { + assertEquals(ErrorCode.DEADLINE_EXCEEDED, ex.getErrorCode()); + } } } @@ -359,8 +365,12 @@ public void testTimeoutExceptionReadWriteAutocommit() { .setUri(URI) .build())) { connection.setStatementTimeout(TIMEOUT_FOR_SLOW_STATEMENTS, TimeUnit.MILLISECONDS); - expected.expect(SpannerExceptionMatcher.matchCode(ErrorCode.DEADLINE_EXCEEDED)); - connection.executeQuery(Statement.of(SLOW_SELECT)); + try { + connection.executeQuery(Statement.of(SLOW_SELECT)); + fail("Expected exception"); + } catch (SpannerException ex) { + assertEquals(ErrorCode.DEADLINE_EXCEEDED, ex.getErrorCode()); + } } } @@ -398,8 +408,12 @@ public void testTimeoutExceptionReadWriteAutocommitSlowUpdate() { .setUri(URI) .build())) { connection.setStatementTimeout(TIMEOUT_FOR_SLOW_STATEMENTS, TimeUnit.MILLISECONDS); - expected.expect(SpannerExceptionMatcher.matchCode(ErrorCode.DEADLINE_EXCEEDED)); - connection.execute(Statement.of(SLOW_UPDATE)); + try { + connection.execute(Statement.of(SLOW_UPDATE)); + fail("Expected exception"); + } catch (SpannerException ex) { + assertEquals(ErrorCode.DEADLINE_EXCEEDED, ex.getErrorCode()); + } } } @@ -449,8 +463,12 @@ public void testTimeoutExceptionReadWriteAutocommitSlowCommit() { // gRPC call will be slow. connection.setStatementTimeout(TIMEOUT_FOR_SLOW_STATEMENTS, TimeUnit.MILLISECONDS); connection.setAutocommit(true); - expected.expect(SpannerExceptionMatcher.matchCode(ErrorCode.DEADLINE_EXCEEDED)); - connection.execute(Statement.of(FAST_UPDATE)); + try { + connection.execute(Statement.of(FAST_UPDATE)); + fail("Expected exception"); + } catch (SpannerException ex) { + assertEquals(ErrorCode.DEADLINE_EXCEEDED, ex.getErrorCode()); + } } } @@ -494,8 +512,12 @@ public void testTimeoutExceptionReadWriteAutocommitPartitioned() { connection.execute(Statement.of(FAST_UPDATE)); connection.setStatementTimeout(TIMEOUT_FOR_SLOW_STATEMENTS, TimeUnit.MILLISECONDS); - expected.expect(SpannerExceptionMatcher.matchCode(ErrorCode.DEADLINE_EXCEEDED)); - connection.execute(Statement.of(SLOW_UPDATE)); + try { + connection.execute(Statement.of(SLOW_UPDATE)); + fail("Expected exception"); + } catch (SpannerException ex) { + assertEquals(ErrorCode.DEADLINE_EXCEEDED, ex.getErrorCode()); + } } } @@ -509,8 +531,12 @@ public void testTimeoutExceptionReadWriteTransactional() { .build())) { connection.setAutocommit(false); connection.setStatementTimeout(TIMEOUT_FOR_SLOW_STATEMENTS, TimeUnit.MILLISECONDS); - expected.expect(SpannerExceptionMatcher.matchCode(ErrorCode.DEADLINE_EXCEEDED)); - connection.executeQuery(Statement.of(SLOW_SELECT)); + try { + connection.executeQuery(Statement.of(SLOW_SELECT)); + fail("Expected exception"); + } catch (SpannerException ex) { + assertEquals(ErrorCode.DEADLINE_EXCEEDED, ex.getErrorCode()); + } } } @@ -564,8 +590,12 @@ public void testTimeoutExceptionReadWriteTransactionalSlowCommit() { connection.executeQuery(Statement.of(FAST_SELECT)); connection.setStatementTimeout(TIMEOUT_FOR_SLOW_STATEMENTS, TimeUnit.MILLISECONDS); - expected.expect(SpannerExceptionMatcher.matchCode(ErrorCode.DEADLINE_EXCEEDED)); - connection.commit(); + try { + connection.commit(); + fail("Expected exception"); + } catch (SpannerException ex) { + assertEquals(ErrorCode.DEADLINE_EXCEEDED, ex.getErrorCode()); + } } } @@ -583,8 +613,12 @@ public void testTimeoutExceptionReadWriteTransactionalSlowRollback() { connection.setStatementTimeout(TIMEOUT_FOR_FAST_STATEMENTS, TimeUnit.MILLISECONDS); connection.executeQuery(Statement.of(FAST_SELECT)); connection.setStatementTimeout(TIMEOUT_FOR_SLOW_STATEMENTS, TimeUnit.MILLISECONDS); - expected.expect(SpannerExceptionMatcher.matchCode(ErrorCode.DEADLINE_EXCEEDED)); - connection.rollback(); + try { + connection.rollback(); + fail("Expected exception"); + } catch (SpannerException ex) { + assertEquals(ErrorCode.DEADLINE_EXCEEDED, ex.getErrorCode()); + } } } @@ -646,7 +680,7 @@ private void testInterruptedException(final ConnectionConsumer consumer) executor.submit( new Callable() { @Override - public Boolean call() throws Exception { + public Boolean call() { try (Connection connection = createConnection( ConnectionOptions.newBuilder() @@ -683,9 +717,12 @@ public void testInvalidQueryReadOnlyAutocommit() { .build())) { connection.setReadOnly(true); connection.setStatementTimeout(TIMEOUT_FOR_FAST_STATEMENTS, TimeUnit.MILLISECONDS); - - expected.expect(SpannerExceptionMatcher.matchCode(ErrorCode.INVALID_ARGUMENT)); - connection.executeQuery(Statement.of(INVALID_SELECT)); + try { + connection.executeQuery(Statement.of(INVALID_SELECT)); + fail("Expected exception"); + } catch (SpannerException ex) { + assertEquals(ErrorCode.INVALID_ARGUMENT, ex.getErrorCode()); + } } } @@ -700,9 +737,12 @@ public void testInvalidQueryReadOnlyTransactional() { connection.setReadOnly(true); connection.setAutocommit(false); connection.setStatementTimeout(TIMEOUT_FOR_FAST_STATEMENTS, TimeUnit.MILLISECONDS); - - expected.expect(SpannerExceptionMatcher.matchCode(ErrorCode.INVALID_ARGUMENT)); - connection.executeQuery(Statement.of(INVALID_SELECT)); + try { + connection.executeQuery(Statement.of(INVALID_SELECT)); + fail("Expected exception"); + } catch (SpannerException ex) { + assertEquals(ErrorCode.INVALID_ARGUMENT, ex.getErrorCode()); + } } } @@ -715,9 +755,12 @@ public void testInvalidQueryReadWriteAutocommit() { .setUri(URI) .build())) { connection.setStatementTimeout(TIMEOUT_FOR_FAST_STATEMENTS, TimeUnit.MILLISECONDS); - - expected.expect(SpannerExceptionMatcher.matchCode(ErrorCode.INVALID_ARGUMENT)); - connection.executeQuery(Statement.of(INVALID_SELECT)); + try { + connection.executeQuery(Statement.of(INVALID_SELECT)); + fail("Expected exception"); + } catch (SpannerException ex) { + assertEquals(ErrorCode.INVALID_ARGUMENT, ex.getErrorCode()); + } } } @@ -731,9 +774,12 @@ public void testInvalidQueryReadWriteTransactional() { .build())) { connection.setAutocommit(false); connection.setStatementTimeout(TIMEOUT_FOR_FAST_STATEMENTS, TimeUnit.MILLISECONDS); - - expected.expect(SpannerExceptionMatcher.matchCode(ErrorCode.INVALID_ARGUMENT)); - connection.executeQuery(Statement.of(INVALID_SELECT)); + try { + connection.executeQuery(Statement.of(INVALID_SELECT)); + fail("Expected exception"); + } catch (SpannerException ex) { + assertEquals(ErrorCode.INVALID_ARGUMENT, ex.getErrorCode()); + } } } @@ -756,9 +802,12 @@ public void run() { }, WAIT_BEFORE_CANCEL, TimeUnit.MILLISECONDS); - - expected.expect(SpannerExceptionMatcher.matchCode(ErrorCode.CANCELLED)); - connection.executeQuery(Statement.of(SLOW_SELECT)); + try { + connection.executeQuery(Statement.of(SLOW_SELECT)); + fail("Expected exception"); + } catch (SpannerException ex) { + assertEquals(ErrorCode.CANCELLED, ex.getErrorCode()); + } } } @@ -816,9 +865,12 @@ public void run() { }, WAIT_BEFORE_CANCEL, TimeUnit.MILLISECONDS); - - expected.expect(SpannerExceptionMatcher.matchCode(ErrorCode.CANCELLED)); - connection.executeQuery(Statement.of(SLOW_SELECT)); + try { + connection.executeQuery(Statement.of(SLOW_SELECT)); + fail("Expected exception"); + } catch (SpannerException ex) { + assertEquals(ErrorCode.CANCELLED, ex.getErrorCode()); + } } } @@ -878,9 +930,12 @@ public void run() { }, WAIT_BEFORE_CANCEL, TimeUnit.MILLISECONDS); - - expected.expect(SpannerExceptionMatcher.matchCode(ErrorCode.CANCELLED)); - connection.executeQuery(Statement.of(SLOW_SELECT)); + try { + connection.executeQuery(Statement.of(SLOW_SELECT)); + fail("Expected exception"); + } catch (SpannerException ex) { + assertEquals(ErrorCode.CANCELLED, ex.getErrorCode()); + } } } @@ -935,9 +990,12 @@ public void run() { }, WAIT_BEFORE_CANCEL, TimeUnit.MILLISECONDS); - - expected.expect(SpannerExceptionMatcher.matchCode(ErrorCode.CANCELLED)); - connection.execute(Statement.of(SLOW_UPDATE)); + try { + connection.execute(Statement.of(SLOW_UPDATE)); + fail("Expected exception"); + } catch (SpannerException ex) { + assertEquals(ErrorCode.CANCELLED, ex.getErrorCode()); + } } } @@ -960,9 +1018,10 @@ public void run() { }, WAIT_BEFORE_CANCEL, TimeUnit.MILLISECONDS); - - expected.expect(SpannerExceptionMatcher.matchCode(ErrorCode.CANCELLED)); connection.execute(Statement.of(FAST_UPDATE)); + fail("Expected exception"); + } catch (SpannerException ex) { + assertEquals(ErrorCode.CANCELLED, ex.getErrorCode()); } } @@ -986,8 +1045,10 @@ public void run() { WAIT_BEFORE_CANCEL, TimeUnit.MILLISECONDS); - expected.expect(SpannerExceptionMatcher.matchCode(ErrorCode.CANCELLED)); connection.executeQuery(Statement.of(SLOW_SELECT)); + fail("Expected exception"); + } catch (SpannerException ex) { + assertEquals(ErrorCode.CANCELLED, ex.getErrorCode()); } } @@ -1014,6 +1075,7 @@ public void run() { boolean cancelled = false; try { connection.executeQuery(Statement.of(SLOW_SELECT)); + fail("Expected exception"); } catch (SpannerException e) { cancelled = e.getErrorCode() == ErrorCode.CANCELLED; } @@ -1048,9 +1110,10 @@ public void run() { }, WAIT_BEFORE_CANCEL, TimeUnit.MILLISECONDS); - - expected.expect(SpannerExceptionMatcher.matchCode(ErrorCode.CANCELLED)); connection.runBatch(); + fail("Expected exception"); + } catch (SpannerException ex) { + assertEquals(ErrorCode.CANCELLED, ex.getErrorCode()); } } @@ -1073,8 +1136,10 @@ public void run() { WAIT_BEFORE_CANCEL, TimeUnit.MILLISECONDS); - expected.expect(SpannerExceptionMatcher.matchCode(ErrorCode.CANCELLED)); connection.execute(Statement.of(SLOW_DDL)); + fail("Expected exception"); + } catch (SpannerException ex) { + assertEquals(ErrorCode.CANCELLED, ex.getErrorCode()); } } @@ -1087,8 +1152,10 @@ public void testTimeoutExceptionDdlAutocommit() { .setUri(URI) .build())) { connection.setStatementTimeout(TIMEOUT_FOR_SLOW_STATEMENTS, TimeUnit.MILLISECONDS); - expected.expect(SpannerExceptionMatcher.matchCode(ErrorCode.DEADLINE_EXCEEDED)); connection.execute(Statement.of(SLOW_DDL)); + fail("Expected exception"); + } catch (SpannerException ex) { + assertEquals(ErrorCode.DEADLINE_EXCEEDED, ex.getErrorCode()); } } @@ -1133,8 +1200,10 @@ public void testTimeoutExceptionDdlBatch() { // the following statement will NOT timeout as the statement is only buffered locally connection.execute(Statement.of(SLOW_DDL)); // the commit sends the statement to the server and should timeout - expected.expect(SpannerExceptionMatcher.matchCode(ErrorCode.DEADLINE_EXCEEDED)); connection.runBatch(); + fail("Expected exception"); + } catch (SpannerException ex) { + assertEquals(ErrorCode.DEADLINE_EXCEEDED, ex.getErrorCode()); } } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/it/ITBulkConnectionTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/it/ITBulkConnectionTest.java index fc86864929..dac9efc6b8 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/it/ITBulkConnectionTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/it/ITBulkConnectionTest.java @@ -20,7 +20,7 @@ import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.MatcherAssert.assertThat; -import com.google.cloud.spanner.ParallelIntegrationTest; +import com.google.cloud.spanner.IntegrationTest; import com.google.cloud.spanner.ResultSet; import com.google.cloud.spanner.Statement; import com.google.cloud.spanner.connection.ITAbstractSpannerTest; @@ -35,8 +35,12 @@ import org.junit.runner.RunWith; import org.junit.runners.JUnit4; -/** Test opening multiple generic (not JDBC) Spanner connections. */ -@Category(ParallelIntegrationTest.class) +/** + * Test opening multiple generic (not JDBC) Spanner connections. This test should not be run in + * parallel with other tests, as it tries to close all active connections, and should not try to + * close connections of other integration tests. + */ +@Category(IntegrationTest.class) @RunWith(JUnit4.class) public class ITBulkConnectionTest extends ITAbstractSpannerTest { private static final int NUMBER_OF_TEST_CONNECTIONS = 250; @@ -68,7 +72,7 @@ public void testBulkCreateConnectionsMultiThreaded() throws InterruptedException executor.submit( new Callable() { @Override - public Void call() throws Exception { + public Void call() { try (ITConnection connection = createConnection()) { try (ResultSet rs = connection.executeQuery(Statement.of("select 1"))) { assertThat(rs.next(), is(true)); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/it/ITReadOnlySpannerTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/it/ITReadOnlySpannerTest.java index a01d533dbd..db83349232 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/it/ITReadOnlySpannerTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/it/ITReadOnlySpannerTest.java @@ -20,6 +20,9 @@ import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import static org.junit.Assume.assumeFalse; import com.google.cloud.spanner.ErrorCode; import com.google.cloud.spanner.Mutation; @@ -30,7 +33,6 @@ import com.google.cloud.spanner.SpannerException; import com.google.cloud.spanner.Statement; import com.google.cloud.spanner.connection.ITAbstractSpannerTest; -import com.google.cloud.spanner.connection.SpannerExceptionMatcher; import com.google.cloud.spanner.connection.SqlScriptVerifier; import java.math.BigInteger; import java.util.concurrent.ExecutorService; @@ -38,10 +40,8 @@ import java.util.concurrent.TimeUnit; import java.util.logging.Logger; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.experimental.categories.Category; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -55,8 +55,6 @@ public class ITReadOnlySpannerTest extends ITAbstractSpannerTest { private static final Logger logger = Logger.getLogger(ITReadOnlySpannerTest.class.getName()); private static final long TEST_ROWS_COUNT = 1000L; - @Rule public ExpectedException exception = ExpectedException.none(); - @Override protected void appendConnectionUri(StringBuilder url) { url.append(";readOnly=true"); @@ -108,22 +106,25 @@ public void testSqlScript() throws Exception { } @Test - public void testStatementTimeoutTransactional() throws Exception { + public void testStatementTimeoutTransactional() { try (ITConnection connection = createConnection()) { connection.beginTransaction(); connection.setStatementTimeout(1L, TimeUnit.MILLISECONDS); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.DEADLINE_EXCEEDED)); try (ResultSet rs = connection.executeQuery( Statement.of( - "SELECT (SELECT COUNT(*) FROM PRIME_NUMBERS)/(SELECT COUNT(*) FROM NUMBERS) AS PRIME_NUMBER_RATIO"))) {} + "SELECT (SELECT COUNT(*) FROM PRIME_NUMBERS)/(SELECT COUNT(*) FROM NUMBERS) AS PRIME_NUMBER_RATIO"))) { + fail("Expected exception"); + } // should never be reached connection.commit(); + } catch (SpannerException ex) { + assertEquals(ErrorCode.DEADLINE_EXCEEDED, ex.getErrorCode()); } } @Test - public void testStatementTimeoutTransactionalMultipleStatements() throws Exception { + public void testStatementTimeoutTransactionalMultipleStatements() { long startTime = System.currentTimeMillis(); try (ITConnection connection = createConnection()) { connection.beginTransaction(); @@ -151,20 +152,24 @@ public void testStatementTimeoutTransactionalMultipleStatements() throws Excepti } @Test - public void testStatementTimeoutAutocommit() throws Exception { + public void testStatementTimeoutAutocommit() { try (ITConnection connection = createConnection()) { assertThat(connection.isAutocommit(), is(true)); connection.setStatementTimeout(1L, TimeUnit.MILLISECONDS); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.DEADLINE_EXCEEDED)); try (ResultSet rs = connection.executeQuery( Statement.of( - "SELECT (SELECT COUNT(*) FROM PRIME_NUMBERS)/(SELECT COUNT(*) FROM NUMBERS) AS PRIME_NUMBER_RATIO"))) {} + "SELECT (SELECT COUNT(*) FROM PRIME_NUMBERS)/(SELECT COUNT(*) FROM NUMBERS) AS PRIME_NUMBER_RATIO"))) { + fail("Expected exception"); + } catch (SpannerException ex) { + assertEquals(ErrorCode.DEADLINE_EXCEEDED, ex.getErrorCode()); + } } } @Test public void testAnalyzeQuery() { + assumeFalse("analyze query is not supported on the emulator", env.getTestHelper().isEmulator()); try (ITConnection connection = createConnection()) { for (QueryAnalyzeMode mode : QueryAnalyzeMode.values()) { try (ResultSet rs = diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/it/ITReadWriteAutocommitSpannerTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/it/ITReadWriteAutocommitSpannerTest.java index bb0b9fb28f..6b85d8b44b 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/it/ITReadWriteAutocommitSpannerTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/it/ITReadWriteAutocommitSpannerTest.java @@ -21,6 +21,7 @@ import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.fail; +import static org.junit.Assume.assumeFalse; import com.google.cloud.spanner.ErrorCode; import com.google.cloud.spanner.Mutation; @@ -34,10 +35,8 @@ import java.util.Arrays; import java.util.concurrent.TimeUnit; import org.junit.FixMethodOrder; -import org.junit.Rule; import org.junit.Test; import org.junit.experimental.categories.Category; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.junit.runners.MethodSorters; @@ -47,8 +46,6 @@ @FixMethodOrder(MethodSorters.NAME_ASCENDING) public class ITReadWriteAutocommitSpannerTest extends ITAbstractSpannerTest { - @Rule public ExpectedException exception = ExpectedException.none(); - @Override protected void appendConnectionUri(StringBuilder uri) { uri.append(";autocommit=true"); @@ -67,7 +64,7 @@ public void test01_SqlScript() throws Exception { } @Test - public void test02_WriteMutation() throws Exception { + public void test02_WriteMutation() { try (ITConnection connection = createConnection()) { connection.write( Mutation.newInsertBuilder("TEST").set("ID").to(9999L).set("NAME").to("FOO").build()); @@ -76,7 +73,10 @@ public void test02_WriteMutation() throws Exception { } @Test - public void test03_MultipleStatements_WithTimeouts() throws InterruptedException { + public void test03_MultipleStatements_WithTimeouts() { + assumeFalse( + "Rolling back a transaction while an update statement is still in flight can cause the transaction to remain active on the emulator", + env.getTestHelper().isEmulator()); try (ITConnection connection = createConnection()) { // do an insert that should succeed assertThat( @@ -95,10 +95,9 @@ public void test03_MultipleStatements_WithTimeouts() throws InterruptedException connection.setStatementTimeout(1L, TimeUnit.MILLISECONDS); try { connection.executeUpdate(Statement.of("UPDATE TEST SET NAME='test18' WHERE ID=1000")); + fail("missing expected exception"); } catch (SpannerException e) { - if (e.getErrorCode() != ErrorCode.DEADLINE_EXCEEDED) { - throw e; - } + assertThat(e.getErrorCode(), is(equalTo(ErrorCode.DEADLINE_EXCEEDED))); } // remove the timeout setting connection.clearStatementTimeout(); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/it/ITSqlMusicScriptTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/it/ITSqlMusicScriptTest.java index b213ed3536..5f239f2c9e 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/it/ITSqlMusicScriptTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/it/ITSqlMusicScriptTest.java @@ -19,6 +19,7 @@ import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assume.assumeFalse; import com.google.cloud.spanner.AbortedDueToConcurrentModificationException; import com.google.cloud.spanner.Mutation; @@ -58,6 +59,10 @@ public void test01_RunScript() throws Exception { @Test public void test02_RunAbortedTest() { + assumeFalse( + "concurrent transactions are not supported on the emulator", + env.getTestHelper().isEmulator()); + final long SINGER_ID = 2L; final long VENUE_ID = 68L; final long NUMBER_OF_SINGERS = 30L; diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/it/ITSqlScriptTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/it/ITSqlScriptTest.java index 6f343d29d5..9ff245ffe5 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/it/ITSqlScriptTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/it/ITSqlScriptTest.java @@ -16,7 +16,9 @@ package com.google.cloud.spanner.connection.it; +import com.google.cloud.spanner.ErrorCode; import com.google.cloud.spanner.ParallelIntegrationTest; +import com.google.cloud.spanner.SpannerException; import com.google.cloud.spanner.connection.ITAbstractSpannerTest; import com.google.cloud.spanner.connection.SqlScriptVerifier; import com.google.cloud.spanner.connection.SqlScriptVerifier.SpannerGenericConnection; @@ -73,6 +75,12 @@ public void test02_InsertTestData() throws Exception { SpannerGenericConnection.of(connection), INSERT_AND_VERIFY_TEST_DATA, SqlScriptVerifier.class); + } catch (SpannerException e) { + if (env.getTestHelper().isEmulator() && e.getErrorCode() == ErrorCode.ALREADY_EXISTS) { + // Errors in a transaction are 'sticky' on the emulator, so any query in the same + // transaction will return the same error as the error generated by a previous (update) + // statement. + } } } @@ -93,6 +101,11 @@ public void test04_TestGetCommitTimestamp() throws Exception { SpannerGenericConnection.of(connection), TEST_GET_COMMIT_TIMESTAMP, SqlScriptVerifier.class); + } catch (SpannerException e) { + if (env.getTestHelper().isEmulator() && e.getErrorCode() == ErrorCode.INVALID_ARGUMENT) { + // Errors in a transaction are 'sticky' on the emulator, so any query in the same + // transaction will return the same error as the error generated by a previous statement. + } } } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/it/ITTransactionModeTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/it/ITTransactionModeTest.java index 07deb7deca..3874f595ba 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/it/ITTransactionModeTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/it/ITTransactionModeTest.java @@ -19,32 +19,30 @@ import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; import com.google.cloud.spanner.ErrorCode; import com.google.cloud.spanner.Key; import com.google.cloud.spanner.Mutation; import com.google.cloud.spanner.ParallelIntegrationTest; import com.google.cloud.spanner.ResultSet; +import com.google.cloud.spanner.SpannerException; import com.google.cloud.spanner.Statement; import com.google.cloud.spanner.connection.ITAbstractSpannerTest; -import com.google.cloud.spanner.connection.SpannerExceptionMatcher; import com.google.cloud.spanner.connection.SqlScriptVerifier; import java.util.Arrays; -import org.junit.Rule; import org.junit.Test; import org.junit.experimental.categories.Category; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @Category(ParallelIntegrationTest.class) @RunWith(JUnit4.class) public class ITTransactionModeTest extends ITAbstractSpannerTest { - @Rule public ExpectedException exception = ExpectedException.none(); - @Override public void appendConnectionUri(StringBuilder uri) { - uri.append("?autocommit=false"); + uri.append(";autocommit=false"); } @Override @@ -142,8 +140,12 @@ public void testDoNotAllowBufferedWriteInReadOnlyTransaction() { try (ITConnection connection = createConnection()) { connection.execute(Statement.of("SET TRANSACTION READ ONLY")); assertThat(connection.isAutocommit(), is(false)); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - connection.bufferedWrite(Mutation.newInsertBuilder("FOO").set("ID").to(1L).build()); + try { + connection.bufferedWrite(Mutation.newInsertBuilder("FOO").set("ID").to(1L).build()); + fail("Expected exception"); + } catch (SpannerException e) { + assertEquals(ErrorCode.FAILED_PRECONDITION, e.getErrorCode()); + } } } @@ -152,11 +154,15 @@ public void testDoNotAllowBufferedWriteIterableInReadOnlyTransaction() { try (ITConnection connection = createConnection()) { connection.execute(Statement.of("SET TRANSACTION READ ONLY")); assertThat(connection.isAutocommit(), is(false)); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - connection.bufferedWrite( - Arrays.asList( - Mutation.newInsertBuilder("FOO").set("ID").to(1L).build(), - Mutation.newInsertBuilder("FOO").set("ID").to(2L).build())); + try { + connection.bufferedWrite( + Arrays.asList( + Mutation.newInsertBuilder("FOO").set("ID").to(1L).build(), + Mutation.newInsertBuilder("FOO").set("ID").to(2L).build())); + fail("Expected exception"); + } catch (SpannerException ex) { + assertEquals(ErrorCode.FAILED_PRECONDITION, ex.getErrorCode()); + } } } @@ -166,8 +172,12 @@ public void testDoNotAllowBufferedWriteInDdlBatch() { connection.startBatchDdl(); assertThat(connection.isAutocommit(), is(false)); assertThat(connection.isDdlBatchActive(), is(true)); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - connection.bufferedWrite(Mutation.newInsertBuilder("FOO").set("ID").to(1L).build()); + try { + connection.bufferedWrite(Mutation.newInsertBuilder("FOO").set("ID").to(1L).build()); + fail("Expected exception"); + } catch (SpannerException e) { + assertEquals(ErrorCode.FAILED_PRECONDITION, e.getErrorCode()); + } } } @@ -177,11 +187,15 @@ public void testDoNotAllowBufferedWriteIterableInDdlBatch() { connection.startBatchDdl(); assertThat(connection.isAutocommit(), is(false)); assertThat(connection.isDdlBatchActive(), is(true)); - exception.expect(SpannerExceptionMatcher.matchCode(ErrorCode.FAILED_PRECONDITION)); - connection.bufferedWrite( - Arrays.asList( - Mutation.newInsertBuilder("FOO").set("ID").to(1L).build(), - Mutation.newInsertBuilder("FOO").set("ID").to(2L).build())); + try { + connection.bufferedWrite( + Arrays.asList( + Mutation.newInsertBuilder("FOO").set("ID").to(1L).build(), + Mutation.newInsertBuilder("FOO").set("ID").to(2L).build())); + fail("Expected exception"); + } catch (SpannerException e) { + assertEquals(ErrorCode.FAILED_PRECONDITION, e.getErrorCode()); + } } } } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/it/ITTransactionRetryTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/it/ITTransactionRetryTest.java index 05fb54a791..1259e2ab8d 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/it/ITTransactionRetryTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/it/ITTransactionRetryTest.java @@ -19,6 +19,7 @@ import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assume.assumeFalse; import com.google.cloud.Timestamp; import com.google.cloud.spanner.AbortedDueToConcurrentModificationException; @@ -315,7 +316,7 @@ public void testNextCallAborted() { connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (1, 'test 1')")); connection.executeUpdate(Statement.of("INSERT INTO TEST (ID, NAME) VALUES (2, 'test 2')")); // do a query - try (ResultSet rs = connection.executeQuery(Statement.of("SELECT * FROM TEST"))) { + try (ResultSet rs = connection.executeQuery(Statement.of("SELECT * FROM TEST ORDER BY ID"))) { // the first record should be accessible without any problems assertThat(rs.next(), is(true)); assertThat(rs.getLong("ID"), is(equalTo(1L))); @@ -366,9 +367,6 @@ public void testMultipleAborts() { connection.commit(); assertThat(RETRY_STATISTICS.totalSuccessfulRetries >= 3, is(true)); - assertThat( - RETRY_STATISTICS.totalNestedAborts, - is(equalTo(RETRY_STATISTICS.totalSuccessfulRetries - 3))); // verify that the insert succeeded try (ResultSet rs = connection.executeQuery(Statement.of("SELECT COUNT(*) AS C FROM TEST"))) { assertThat(rs.next(), is(true)); @@ -490,6 +488,9 @@ public void testAbortWithResultSetFullyConsumed() { @Test public void testAbortWithConcurrentInsert() { + assumeFalse( + "concurrent transactions are not supported on the emulator", + env.getTestHelper().isEmulator()); AbortInterceptor interceptor = new AbortInterceptor(0); try (ITConnection connection = createConnection(interceptor, new CountTransactionRetryListener())) { @@ -524,6 +525,9 @@ public void testAbortWithConcurrentInsert() { @Test public void testAbortWithConcurrentDelete() { + assumeFalse( + "concurrent transactions are not supported on the emulator", + env.getTestHelper().isEmulator()); AbortInterceptor interceptor = new AbortInterceptor(0); // first insert two test records try (ITConnection connection = createConnection()) { @@ -562,6 +566,9 @@ public void testAbortWithConcurrentDelete() { @Test public void testAbortWithConcurrentUpdate() { + assumeFalse( + "concurrent transactions are not supported on the emulator", + env.getTestHelper().isEmulator()); AbortInterceptor interceptor = new AbortInterceptor(0); // first insert two test records try (ITConnection connection = createConnection()) { @@ -605,6 +612,9 @@ public void testAbortWithConcurrentUpdate() { */ @Test public void testAbortWithUnseenConcurrentInsert() { + assumeFalse( + "concurrent transactions are not supported on the emulator", + env.getTestHelper().isEmulator()); AbortInterceptor interceptor = new AbortInterceptor(0); try (ITConnection connection = createConnection(interceptor, new CountTransactionRetryListener())) { @@ -652,6 +662,9 @@ public void testAbortWithUnseenConcurrentInsert() { */ @Test public void testAbortWithUnseenConcurrentInsertAbortOnNext() { + assumeFalse( + "concurrent transactions are not supported on the emulator", + env.getTestHelper().isEmulator()); // no calls to next(), this should succeed assertThat(testAbortWithUnseenConcurrentInsertAbortOnNext(0) >= 1, is(true)); // 1 call to next() should also succeed, as there were 2 records in the original result set @@ -673,6 +686,9 @@ public void testAbortWithUnseenConcurrentInsertAbortOnNext() { private int testAbortWithUnseenConcurrentInsertAbortOnNext(int callsToNext) throws AbortedDueToConcurrentModificationException { + assumeFalse( + "concurrent transactions are not supported on the emulator", + env.getTestHelper().isEmulator()); int retries = 0; clearTable(); clearStatistics(); @@ -732,6 +748,9 @@ private int testAbortWithUnseenConcurrentInsertAbortOnNext(int callsToNext) */ @Test public void testAbortWithConcurrentInsertAndContinue() { + assumeFalse( + "concurrent transactions are not supported on the emulator", + env.getTestHelper().isEmulator()); AbortInterceptor interceptor = new AbortInterceptor(0); try (ITConnection connection = createConnection(interceptor, new CountTransactionRetryListener())) { @@ -941,6 +960,9 @@ protected boolean shouldAbort(String statement, ExecutionStep step) { */ @Test public void testNestedAbortWithConcurrentInsert() { + assumeFalse( + "concurrent transactions are not supported on the emulator", + env.getTestHelper().isEmulator()); AbortInterceptor interceptor = new AbortInterceptor(0) { private boolean alreadyAborted = false; @@ -1003,6 +1025,9 @@ protected boolean shouldAbort(String statement, ExecutionStep step) { */ @Test public void testAbortWithDifferentUpdateCount() { + assumeFalse( + "concurrent transactions are not supported on the emulator", + env.getTestHelper().isEmulator()); AbortInterceptor interceptor = new AbortInterceptor(0); // first insert two test records try (ITConnection connection = createConnection()) { @@ -1048,6 +1073,9 @@ public void testAbortWithDifferentUpdateCount() { */ @Test public void testAbortWithExceptionOnSelect() { + assumeFalse( + "resume after error in transaction is not supported on the emulator", + env.getTestHelper().isEmulator()); AbortInterceptor interceptor = new AbortInterceptor(0); // first insert two test records try (ITConnection connection = createConnection()) { @@ -1097,6 +1125,9 @@ public void testAbortWithExceptionOnSelect() { */ @Test public void testAbortWithExceptionOnSelectAndConcurrentModification() { + assumeFalse( + "concurrent transactions are not supported on the emulator", + env.getTestHelper().isEmulator()); boolean abortedDueToConcurrentModification = false; AbortInterceptor interceptor = new AbortInterceptor(0); // first insert two test records @@ -1164,6 +1195,9 @@ public void testAbortWithExceptionOnSelectAndConcurrentModification() { */ @Test public void testAbortWithExceptionOnInsertAndConcurrentModification() { + assumeFalse( + "concurrent transactions are not supported on the emulator", + env.getTestHelper().isEmulator()); boolean abortedDueToConcurrentModification = false; AbortInterceptor interceptor = new AbortInterceptor(0); // first insert two test records @@ -1230,6 +1264,9 @@ public void testAbortWithExceptionOnInsertAndConcurrentModification() { */ @Test public void testAbortWithDroppedTableConcurrentModification() { + assumeFalse( + "concurrent transactions are not supported on the emulator", + env.getTestHelper().isEmulator()); boolean abortedDueToConcurrentModification = false; AbortInterceptor interceptor = new AbortInterceptor(0); // first insert two test records @@ -1292,6 +1329,9 @@ public void testAbortWithDroppedTableConcurrentModification() { */ @Test public void testAbortWithInsertOnDroppedTableConcurrentModification() { + assumeFalse( + "concurrent transactions are not supported on the emulator", + env.getTestHelper().isEmulator()); boolean abortedDueToConcurrentModification = false; AbortInterceptor interceptor = new AbortInterceptor(0); // first insert two test records @@ -1351,6 +1391,9 @@ public void testAbortWithInsertOnDroppedTableConcurrentModification() { */ @Test public void testAbortWithCursorHalfwayDroppedTableConcurrentModification() { + assumeFalse( + "concurrent transactions are not supported on the emulator", + env.getTestHelper().isEmulator()); boolean abortedDueToConcurrentModification = false; AbortInterceptor interceptor = new AbortInterceptor(0); // first insert two test records @@ -1503,6 +1546,9 @@ public void testRetryHighAbortRate() { @Test public void testAbortWithConcurrentInsertOnEmptyTable() { + assumeFalse( + "concurrent transactions are not supported on the emulator", + env.getTestHelper().isEmulator()); AbortInterceptor interceptor = new AbortInterceptor(0); try (ITConnection connection = createConnection(interceptor, new CountTransactionRetryListener())) { diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITAsyncAPITest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITAsyncAPITest.java new file mode 100644 index 0000000000..a2239aa3b9 --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITAsyncAPITest.java @@ -0,0 +1,309 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.spanner.it; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; +import static org.junit.Assume.assumeFalse; + +import com.google.api.core.ApiFuture; +import com.google.cloud.spanner.AsyncResultSet; +import com.google.cloud.spanner.AsyncResultSet.CallbackResponse; +import com.google.cloud.spanner.AsyncResultSet.ReadyCallback; +import com.google.cloud.spanner.AsyncRunner; +import com.google.cloud.spanner.AsyncRunner.AsyncWork; +import com.google.cloud.spanner.Database; +import com.google.cloud.spanner.DatabaseClient; +import com.google.cloud.spanner.DatabaseId; +import com.google.cloud.spanner.ErrorCode; +import com.google.cloud.spanner.IntegrationTest; +import com.google.cloud.spanner.IntegrationTestEnv; +import com.google.cloud.spanner.Key; +import com.google.cloud.spanner.KeyRange; +import com.google.cloud.spanner.KeySet; +import com.google.cloud.spanner.Mutation; +import com.google.cloud.spanner.SpannerException; +import com.google.cloud.spanner.Statement; +import com.google.cloud.spanner.Struct; +import com.google.cloud.spanner.TimestampBound; +import com.google.cloud.spanner.TransactionContext; +import com.google.cloud.spanner.Type; +import com.google.cloud.spanner.Type.StructField; +import com.google.cloud.spanner.testing.RemoteSpannerHelper; +import com.google.common.util.concurrent.SettableFuture; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Integration tests for asynchronous APIs. */ +@Category(IntegrationTest.class) +@RunWith(JUnit4.class) +public class ITAsyncAPITest { + @ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv(); + private static final String TABLE_NAME = "TestTable"; + private static final String INDEX_NAME = "TestTableByValue"; + private static final List ALL_COLUMNS = Arrays.asList("Key", "StringValue"); + private static final Type TABLE_TYPE = + Type.struct( + StructField.of("Key", Type.string()), StructField.of("StringValue", Type.string())); + + private static Database db; + private static DatabaseClient client; + private static ExecutorService executor; + + @BeforeClass + public static void setUpDatabase() { + db = + env.getTestHelper() + .createTestDatabase( + "CREATE TABLE TestTable (" + + " Key STRING(MAX) NOT NULL," + + " StringValue STRING(MAX)," + + ") PRIMARY KEY (Key)", + "CREATE INDEX TestTableByValue ON TestTable(StringValue)", + "CREATE INDEX TestTableByValueDesc ON TestTable(StringValue DESC)"); + client = env.getTestHelper().getDatabaseClient(db); + + // Includes k0..k14. Note that strings k{10,14} sort between k1 and k2. + List mutations = new ArrayList<>(); + for (int i = 0; i < 15; ++i) { + mutations.add( + Mutation.newInsertOrUpdateBuilder(TABLE_NAME) + .set("Key") + .to("k" + i) + .set("StringValue") + .to("v" + i) + .build()); + } + client.write(mutations); + executor = Executors.newSingleThreadExecutor(); + } + + @AfterClass + public static void cleanup() { + executor.shutdown(); + } + + @Test + public void emptyReadAsync() throws Exception { + final SettableFuture result = SettableFuture.create(); + AsyncResultSet resultSet = + client + .singleUse(TimestampBound.strong()) + .readAsync( + TABLE_NAME, + KeySet.range(KeyRange.closedOpen(Key.of("k99"), Key.of("z"))), + ALL_COLUMNS); + resultSet.setCallback( + executor, + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + try { + while (true) { + switch (resultSet.tryNext()) { + case OK: + fail("received unexpected data"); + case NOT_READY: + return CallbackResponse.CONTINUE; + case DONE: + assertThat(resultSet.getType()).isEqualTo(TABLE_TYPE); + result.set(true); + return CallbackResponse.DONE; + } + } + } catch (Throwable t) { + result.setException(t); + return CallbackResponse.DONE; + } + } + }); + assertThat(result.get()).isTrue(); + } + + @Test + public void indexEmptyReadAsync() throws Exception { + final SettableFuture result = SettableFuture.create(); + AsyncResultSet resultSet = + client + .singleUse(TimestampBound.strong()) + .readUsingIndexAsync( + TABLE_NAME, + INDEX_NAME, + KeySet.range(KeyRange.closedOpen(Key.of("v99"), Key.of("z"))), + ALL_COLUMNS); + resultSet.setCallback( + executor, + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + try { + while (true) { + switch (resultSet.tryNext()) { + case OK: + fail("received unexpected data"); + case NOT_READY: + return CallbackResponse.CONTINUE; + case DONE: + assertThat(resultSet.getType()).isEqualTo(TABLE_TYPE); + result.set(true); + return CallbackResponse.DONE; + } + } + } catch (Throwable t) { + result.setException(t); + return CallbackResponse.DONE; + } + } + }); + assertThat(result.get()).isTrue(); + } + + @Test + public void pointReadAsync() throws Exception { + ApiFuture row = + client + .singleUse(TimestampBound.strong()) + .readRowAsync(TABLE_NAME, Key.of("k1"), ALL_COLUMNS); + assertThat(row.get()).isNotNull(); + assertThat(row.get().getString(0)).isEqualTo("k1"); + assertThat(row.get().getString(1)).isEqualTo("v1"); + // Ensure that the Struct implementation supports equality properly. + assertThat(row.get()) + .isEqualTo(Struct.newBuilder().set("Key").to("k1").set("StringValue").to("v1").build()); + } + + @Test + public void indexPointReadAsync() throws Exception { + ApiFuture row = + client + .singleUse(TimestampBound.strong()) + .readRowUsingIndexAsync(TABLE_NAME, INDEX_NAME, Key.of("v1"), ALL_COLUMNS); + assertThat(row.get()).isNotNull(); + assertThat(row.get().getString(0)).isEqualTo("k1"); + assertThat(row.get().getString(1)).isEqualTo("v1"); + } + + @Test + public void pointReadNotFound() throws Exception { + ApiFuture row = + client + .singleUse(TimestampBound.strong()) + .readRowAsync(TABLE_NAME, Key.of("k999"), ALL_COLUMNS); + assertThat(row.get()).isNull(); + } + + @Test + public void indexPointReadNotFound() throws Exception { + ApiFuture row = + client + .singleUse(TimestampBound.strong()) + .readRowUsingIndexAsync(TABLE_NAME, INDEX_NAME, Key.of("v999"), ALL_COLUMNS); + assertThat(row.get()).isNull(); + } + + @Test + public void invalidDatabase() throws Exception { + RemoteSpannerHelper helper = env.getTestHelper(); + DatabaseClient invalidClient = + helper.getClient().getDatabaseClient(DatabaseId.of(helper.getInstanceId(), "invalid")); + ApiFuture row = + invalidClient + .singleUse(TimestampBound.strong()) + .readRowAsync(TABLE_NAME, Key.of("k99"), ALL_COLUMNS); + try { + row.get(); + fail("missing expected exception"); + } catch (ExecutionException e) { + assertThat(e.getCause()).isInstanceOf(SpannerException.class); + SpannerException se = (SpannerException) e.getCause(); + assertThat(se.getErrorCode()).isEqualTo(ErrorCode.NOT_FOUND); + } + } + + @Test + public void tableNotFound() throws Exception { + ApiFuture row = + client + .singleUse(TimestampBound.strong()) + .readRowAsync("BadTableName", Key.of("k1"), ALL_COLUMNS); + try { + row.get(); + } catch (ExecutionException e) { + assertThat(e.getCause()).isInstanceOf(SpannerException.class); + SpannerException se = (SpannerException) e.getCause(); + assertThat(se.getErrorCode()).isEqualTo(ErrorCode.NOT_FOUND); + assertThat(se.getMessage()).contains("BadTableName"); + } + } + + @Test + public void columnNotFound() throws Exception { + ApiFuture row = + client + .singleUse(TimestampBound.strong()) + .readRowAsync(TABLE_NAME, Key.of("k1"), Arrays.asList("Key", "BadColumnName")); + try { + row.get(); + } catch (ExecutionException e) { + assertThat(e.getCause()).isInstanceOf(SpannerException.class); + SpannerException se = (SpannerException) e.getCause(); + assertThat(se.getErrorCode()).isEqualTo(ErrorCode.NOT_FOUND); + assertThat(se.getMessage()).contains("BadColumnName"); + } + } + + @Test + public void asyncRunnerFireAndForgetInvalidUpdate() throws Exception { + assumeFalse( + "errors in read/write transactions on emulator are sticky", + env.getTestHelper().isEmulator()); + try { + assertThat(client.singleUse().readRow("TestTable", Key.of("k999"), ALL_COLUMNS)).isNull(); + AsyncRunner runner = client.runAsync(); + ApiFuture res = + runner.runAsync( + new AsyncWork() { + @Override + public ApiFuture doWorkAsync(TransactionContext txn) { + // The error returned by this update statement will not bubble up and fail the + // transaction. + txn.executeUpdateAsync(Statement.of("UPDATE BadTableName SET FOO=1 WHERE ID=2")); + return txn.executeUpdateAsync( + Statement.of( + "INSERT INTO TestTable (Key, StringValue) VALUES ('k999', 'v999')")); + } + }, + executor); + assertThat(res.get()).isEqualTo(1L); + assertThat(client.singleUse().readRow("TestTable", Key.of("k999"), ALL_COLUMNS)).isNotNull(); + } finally { + client.writeAtLeastOnce(Arrays.asList(Mutation.delete("TestTable", Key.of("k999")))); + assertThat(client.singleUse().readRow("TestTable", Key.of("k999"), ALL_COLUMNS)).isNull(); + } + } +} diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITAsyncExamplesTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITAsyncExamplesTest.java new file mode 100644 index 0000000000..c5e2419ba6 --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITAsyncExamplesTest.java @@ -0,0 +1,550 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.spanner.it; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import com.google.api.core.ApiFunction; +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutures; +import com.google.api.core.SettableApiFuture; +import com.google.cloud.spanner.AsyncResultSet; +import com.google.cloud.spanner.AsyncResultSet.CallbackResponse; +import com.google.cloud.spanner.AsyncResultSet.ReadyCallback; +import com.google.cloud.spanner.AsyncRunner; +import com.google.cloud.spanner.AsyncRunner.AsyncWork; +import com.google.cloud.spanner.Database; +import com.google.cloud.spanner.DatabaseClient; +import com.google.cloud.spanner.ErrorCode; +import com.google.cloud.spanner.IntegrationTest; +import com.google.cloud.spanner.IntegrationTestEnv; +import com.google.cloud.spanner.Key; +import com.google.cloud.spanner.KeySet; +import com.google.cloud.spanner.Mutation; +import com.google.cloud.spanner.ReadOnlyTransaction; +import com.google.cloud.spanner.SpannerException; +import com.google.cloud.spanner.Statement; +import com.google.cloud.spanner.Struct; +import com.google.cloud.spanner.StructReader; +import com.google.cloud.spanner.TransactionContext; +import com.google.common.base.Function; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Deque; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Integration tests for asynchronous APIs. */ +@Category(IntegrationTest.class) +@RunWith(JUnit4.class) +public class ITAsyncExamplesTest { + @ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv(); + private static final String TABLE_NAME = "TestTable"; + private static final String INDEX_NAME = "TestTableByValue"; + private static final List ALL_COLUMNS = Arrays.asList("Key", "StringValue"); + private static final ImmutableList ALL_VALUES_IN_PK_ORDER = + ImmutableList.of( + "v0", "v1", "v10", "v11", "v12", "v13", "v14", "v2", "v3", "v4", "v5", "v6", "v7", "v8", + "v9"); + + private static Database db; + private static DatabaseClient client; + private static ExecutorService executor; + + @BeforeClass + public static void setUpDatabase() { + db = + env.getTestHelper() + .createTestDatabase( + "CREATE TABLE TestTable (" + + " Key STRING(MAX) NOT NULL," + + " StringValue STRING(MAX)," + + ") PRIMARY KEY (Key)", + "CREATE INDEX TestTableByValue ON TestTable(StringValue)", + "CREATE INDEX TestTableByValueDesc ON TestTable(StringValue DESC)"); + client = env.getTestHelper().getDatabaseClient(db); + + // Includes k0..k14. Note that strings k{10,14} sort between k1 and k2. + List mutations = new ArrayList<>(); + for (int i = 0; i < 15; ++i) { + mutations.add( + Mutation.newInsertOrUpdateBuilder(TABLE_NAME) + .set("Key") + .to("k" + i) + .set("StringValue") + .to("v" + i) + .build()); + } + client.write(mutations); + executor = Executors.newScheduledThreadPool(8); + } + + @AfterClass + public static void cleanup() { + executor.shutdown(); + } + + @Test + public void readAsync() throws Exception { + final SettableApiFuture> future = SettableApiFuture.create(); + try (AsyncResultSet rs = client.singleUse().readAsync(TABLE_NAME, KeySet.all(), ALL_COLUMNS)) { + rs.setCallback( + executor, + new ReadyCallback() { + final List values = new LinkedList<>(); + + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + try { + while (true) { + switch (resultSet.tryNext()) { + case DONE: + future.set(values); + return CallbackResponse.DONE; + case NOT_READY: + return CallbackResponse.CONTINUE; + case OK: + values.add(resultSet.getString("StringValue")); + break; + } + } + } catch (Throwable t) { + future.setException(t); + return CallbackResponse.DONE; + } + } + }); + } + assertThat(future.get()).containsExactlyElementsIn(ALL_VALUES_IN_PK_ORDER); + } + + @Test + public void readUsingIndexAsync() throws Exception { + final SettableApiFuture> future = SettableApiFuture.create(); + try (AsyncResultSet rs = + client.singleUse().readUsingIndexAsync(TABLE_NAME, INDEX_NAME, KeySet.all(), ALL_COLUMNS)) { + rs.setCallback( + executor, + new ReadyCallback() { + final List values = new LinkedList<>(); + + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + try { + while (true) { + switch (resultSet.tryNext()) { + case DONE: + future.set(values); + return CallbackResponse.DONE; + case NOT_READY: + return CallbackResponse.CONTINUE; + case OK: + values.add(resultSet.getString("StringValue")); + break; + } + } + } catch (Throwable t) { + future.setException(t); + return CallbackResponse.DONE; + } + } + }); + } + assertThat(future.get()).containsExactlyElementsIn(ALL_VALUES_IN_PK_ORDER); + } + + @Test + public void readRowAsync() throws Exception { + ApiFuture row = client.singleUse().readRowAsync(TABLE_NAME, Key.of("k1"), ALL_COLUMNS); + assertThat(row.get().getString("StringValue")).isEqualTo("v1"); + } + + @Test + public void readRowUsingIndexAsync() throws Exception { + ApiFuture row = + client + .singleUse() + .readRowUsingIndexAsync(TABLE_NAME, INDEX_NAME, Key.of("v2"), ALL_COLUMNS); + assertThat(row.get().getString("Key")).isEqualTo("k2"); + } + + @Test + public void executeQueryAsync() throws Exception { + final ImmutableList keys = ImmutableList.of("k3", "k4"); + final SettableApiFuture> future = SettableApiFuture.create(); + try (AsyncResultSet rs = + client + .singleUse() + .executeQueryAsync( + Statement.newBuilder("SELECT StringValue FROM TestTable WHERE Key IN UNNEST(@keys)") + .bind("keys") + .toStringArray(keys) + .build())) { + rs.setCallback( + executor, + new ReadyCallback() { + final List values = new LinkedList<>(); + + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + try { + while (true) { + switch (resultSet.tryNext()) { + case DONE: + future.set(values); + return CallbackResponse.DONE; + case NOT_READY: + return CallbackResponse.CONTINUE; + case OK: + values.add(resultSet.getString("StringValue")); + break; + } + } + } catch (Throwable t) { + future.setException(t); + return CallbackResponse.DONE; + } + } + }); + } + assertThat(future.get()).containsExactly("v3", "v4"); + } + + @Test + public void runAsync() throws Exception { + AsyncRunner runner = client.runAsync(); + ApiFuture insertCount = + runner.runAsync( + new AsyncWork() { + @Override + public ApiFuture doWorkAsync(TransactionContext txn) { + // Even though this is a shoot-and-forget asynchronous DML statement, it is + // guaranteed to be executed within the transaction before the commit is executed. + return txn.executeUpdateAsync( + Statement.newBuilder( + "INSERT INTO TestTable (Key, StringValue) VALUES (@key, @value)") + .bind("key") + .to("k999") + .bind("value") + .to("v999") + .build()); + } + }, + executor); + assertThat(insertCount.get()).isEqualTo(1L); + ApiFuture deleteCount = + runner.runAsync( + new AsyncWork() { + @Override + public ApiFuture doWorkAsync(TransactionContext txn) { + return txn.executeUpdateAsync( + Statement.newBuilder("DELETE FROM TestTable WHERE Key=@key") + .bind("key") + .to("k999") + .build()); + } + }, + executor); + assertThat(deleteCount.get()).isEqualTo(1L); + } + + @Test + public void runAsyncBatchUpdate() throws Exception { + AsyncRunner runner = client.runAsync(); + ApiFuture insertCount = + runner.runAsync( + new AsyncWork() { + @Override + public ApiFuture doWorkAsync(TransactionContext txn) { + // Even though this is a shoot-and-forget asynchronous DML statement, it is + // guaranteed to be executed within the transaction before the commit is executed. + return txn.batchUpdateAsync( + ImmutableList.of( + Statement.newBuilder( + "INSERT INTO TestTable (Key, StringValue) VALUES (@key, @value)") + .bind("key") + .to("k997") + .bind("value") + .to("v997") + .build(), + Statement.newBuilder( + "INSERT INTO TestTable (Key, StringValue) VALUES (@key, @value)") + .bind("key") + .to("k998") + .bind("value") + .to("v998") + .build(), + Statement.newBuilder( + "INSERT INTO TestTable (Key, StringValue) VALUES (@key, @value)") + .bind("key") + .to("k999") + .bind("value") + .to("v999") + .build())); + } + }, + executor); + assertThat(insertCount.get()).asList().containsExactly(1L, 1L, 1L); + ApiFuture deleteCount = + runner.runAsync( + new AsyncWork() { + @Override + public ApiFuture doWorkAsync(TransactionContext txn) { + return txn.batchUpdateAsync( + ImmutableList.of( + Statement.newBuilder("DELETE FROM TestTable WHERE Key=@key") + .bind("key") + .to("k997") + .build(), + Statement.newBuilder("DELETE FROM TestTable WHERE Key=@key") + .bind("key") + .to("k998") + .build(), + Statement.newBuilder("DELETE FROM TestTable WHERE Key=@key") + .bind("key") + .to("k999") + .build())); + } + }, + executor); + assertThat(deleteCount.get()).asList().containsExactly(1L, 1L, 1L); + } + + @Test + public void readOnlyTransaction() throws Exception { + ImmutableList keys1 = ImmutableList.of("k10", "k11", "k12"); + ImmutableList keys2 = ImmutableList.of("k1", "k2", "k3"); + ApiFuture> values1; + ApiFuture> values2; + try (ReadOnlyTransaction tx = client.readOnlyTransaction()) { + try (AsyncResultSet rs = + tx.executeQueryAsync( + Statement.newBuilder("SELECT * FROM TestTable WHERE Key IN UNNEST(@keys)") + .bind("keys") + .toStringArray(keys1) + .build())) { + values1 = + rs.toListAsync( + new Function() { + @Override + public String apply(StructReader input) { + return input.getString("StringValue"); + } + }, + executor); + } + try (AsyncResultSet rs = + tx.executeQueryAsync( + Statement.newBuilder("SELECT * FROM TestTable WHERE Key IN UNNEST(@keys)") + .bind("keys") + .toStringArray(keys2) + .build())) { + values2 = + rs.toListAsync( + new Function() { + @Override + public String apply(StructReader input) { + return input.getString("StringValue"); + } + }, + executor); + } + } + ApiFuture> allValues = + ApiFutures.transform( + ApiFutures.allAsList(Arrays.asList(values1, values2)), + new ApiFunction>, Iterable>() { + @Override + public Iterable apply(List> input) { + return Iterables.mergeSorted( + input, + new Comparator() { + @Override + public int compare(String o1, String o2) { + // Compare based on numerical order (i.e. without the preceding 'v'). + return Integer.valueOf(o1.substring(1)) + .compareTo(Integer.valueOf(o2.substring(1))); + } + }); + } + }, + executor); + assertThat(allValues.get()).containsExactly("v1", "v2", "v3", "v10", "v11", "v12"); + } + + @Test + public void pauseResume() throws Exception { + Statement unevenStatement = + Statement.of( + "SELECT * FROM TestTable WHERE MOD(CAST(SUBSTR(Key, 2) AS INT64), 2) = 1 ORDER BY CAST(SUBSTR(Key, 2) AS INT64)"); + Statement evenStatement = + Statement.of( + "SELECT * FROM TestTable WHERE MOD(CAST(SUBSTR(Key, 2) AS INT64), 2) = 0 ORDER BY CAST(SUBSTR(Key, 2) AS INT64)"); + + final Object lock = new Object(); + final SettableApiFuture evenFinished = SettableApiFuture.create(); + final SettableApiFuture unevenFinished = SettableApiFuture.create(); + final CountDownLatch evenReturnedFirstRow = new CountDownLatch(1); + final Deque allValues = new LinkedList<>(); + try (ReadOnlyTransaction tx = client.readOnlyTransaction()) { + try (AsyncResultSet evenRs = tx.executeQueryAsync(evenStatement); + AsyncResultSet unevenRs = tx.executeQueryAsync(unevenStatement)) { + evenRs.setCallback( + executor, + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + try { + while (true) { + switch (resultSet.tryNext()) { + case DONE: + evenFinished.set(true); + return CallbackResponse.DONE; + case NOT_READY: + return CallbackResponse.CONTINUE; + case OK: + synchronized (lock) { + allValues.add(resultSet.getString("StringValue")); + } + evenReturnedFirstRow.countDown(); + return CallbackResponse.PAUSE; + } + } + } catch (Throwable t) { + evenFinished.setException(t); + return CallbackResponse.DONE; + } + } + }); + + unevenRs.setCallback( + executor, + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + try { + // Make sure the even result set has returned the first before we start the uneven + // results. + evenReturnedFirstRow.await(); + while (true) { + switch (resultSet.tryNext()) { + case DONE: + unevenFinished.set(true); + return CallbackResponse.DONE; + case NOT_READY: + return CallbackResponse.CONTINUE; + case OK: + synchronized (lock) { + allValues.add(resultSet.getString("StringValue")); + } + return CallbackResponse.PAUSE; + } + } + } catch (Throwable t) { + unevenFinished.setException(t); + return CallbackResponse.DONE; + } + } + }); + while (!(evenFinished.isDone() && unevenFinished.isDone())) { + synchronized (lock) { + if (allValues.peekLast() != null) { + if (Integer.valueOf(allValues.peekLast().substring(1)) % 2 == 1) { + evenRs.resume(); + } else { + unevenRs.resume(); + } + } + if (allValues.size() == 15) { + unevenRs.resume(); + evenRs.resume(); + } + } + } + } + } + assertThat(ApiFutures.allAsList(Arrays.asList(evenFinished, unevenFinished)).get()) + .containsExactly(Boolean.TRUE, Boolean.TRUE); + assertThat(allValues) + .containsExactly( + "v0", "v1", "v2", "v3", "v4", "v5", "v6", "v7", "v8", "v9", "v10", "v11", "v12", "v13", + "v14"); + } + + @Test + public void cancel() throws Exception { + final List values = new LinkedList<>(); + final SettableApiFuture finished = SettableApiFuture.create(); + final CountDownLatch receivedFirstRow = new CountDownLatch(1); + final CountDownLatch cancelled = new CountDownLatch(1); + try (AsyncResultSet rs = client.singleUse().readAsync(TABLE_NAME, KeySet.all(), ALL_COLUMNS)) { + rs.setCallback( + executor, + new ReadyCallback() { + @Override + public CallbackResponse cursorReady(AsyncResultSet resultSet) { + try { + while (true) { + switch (resultSet.tryNext()) { + case DONE: + finished.set(true); + return CallbackResponse.DONE; + case NOT_READY: + return CallbackResponse.CONTINUE; + case OK: + values.add(resultSet.getString("StringValue")); + receivedFirstRow.countDown(); + cancelled.await(); + break; + } + } + } catch (Throwable t) { + finished.setException(t); + return CallbackResponse.DONE; + } + } + }); + receivedFirstRow.await(); + rs.cancel(); + } + cancelled.countDown(); + try { + finished.get(); + fail("missing expected exception"); + } catch (ExecutionException e) { + assertThat(e.getCause()).isInstanceOf(SpannerException.class); + SpannerException se = (SpannerException) e.getCause(); + assertThat(se.getErrorCode()).isEqualTo(ErrorCode.CANCELLED); + assertThat(values).containsExactly("v0"); + } + } +} diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITBackupTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITBackupTest.java index c4cb3e032a..2a35a17083 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITBackupTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITBackupTest.java @@ -66,10 +66,8 @@ import org.junit.Before; import org.junit.BeforeClass; import org.junit.ClassRule; -import org.junit.Rule; import org.junit.Test; import org.junit.experimental.categories.Category; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -84,7 +82,6 @@ public class ITBackupTest { private static final String EXPECTED_OP_NAME_FORMAT = "%s/backups/%s/operations/"; @ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv(); - @Rule public ExpectedException expectedException = ExpectedException.none(); private DatabaseAdminClient dbAdminClient; private InstanceAdminClient instanceAdminClient; private Instance instance; @@ -100,7 +97,7 @@ public static void doNotRunOnEmulator() { } @Before - public void setUp() throws Exception { + public void setUp() { logger.info("Setting up tests"); testHelper = env.getTestHelper(); dbAdminClient = testHelper.getClient().getDatabaseAdminClient(); @@ -477,7 +474,7 @@ private void testGetBackup(Database db, String backupId, Timestamp expireTime) { assertThat(backup.getDatabase()).isEqualTo(db.getId()); } - private void testUpdateBackup(Backup backup) throws InterruptedException, ExecutionException { + private void testUpdateBackup(Backup backup) { // Update the expire time. Timestamp tomorrow = tomorrow(); backup = backup.toBuilder().setExpireTime(tomorrow).build(); @@ -528,7 +525,7 @@ private void testPagination(int expectedMinimumTotalBackups) { assertThat(numBackups).isAtLeast(expectedMinimumTotalBackups); } - private void testDelete(String backupId) throws InterruptedException, ExecutionException { + private void testDelete(String backupId) throws InterruptedException { waitForDbOperations(backupId); // Get the backup. logger.info(String.format("Fetching backup %s", backupId)); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITBatchDmlTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITBatchDmlTest.java index a080d1c396..b2cf021445 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITBatchDmlTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITBatchDmlTest.java @@ -108,7 +108,7 @@ public void batchDml() { final TransactionCallable callable = new TransactionCallable() { @Override - public long[] run(TransactionContext transaction) throws Exception { + public long[] run(TransactionContext transaction) { List stmts = new ArrayList<>(); stmts.add(Statement.of(INSERT_DML)); stmts.add(Statement.of(UPDATE_DML)); @@ -129,7 +129,7 @@ public void mixedBatchDmlAndDml() { final TransactionCallable callable = new TransactionCallable() { @Override - public long[] run(TransactionContext transaction) throws Exception { + public long[] run(TransactionContext transaction) { long rowCount = transaction.executeUpdate(Statement.of(INSERT_DML)); List stmts = new ArrayList<>(); stmts.add(Statement.of(UPDATE_DML)); @@ -195,10 +195,7 @@ public long[] run(TransactionContext transaction) { runner.run(callable); Assert.fail("Expecting an exception."); } catch (SpannerBatchUpdateException e) { - // TODO: Remove if-statement when emulator returns the same error code as Cloud Spanner. - if (!env.getTestHelper().isEmulator()) { - assertThat(e.getErrorCode()).isEqualTo(ErrorCode.ALREADY_EXISTS); - } + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.ALREADY_EXISTS); long[] rowCounts = e.getUpdateCounts(); assertThat(rowCounts.length).isEqualTo(1); for (long rc : rowCounts) { diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITBatchReadTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITBatchReadTest.java index a6aa4f715d..e682dd685e 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITBatchReadTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITBatchReadTest.java @@ -17,7 +17,6 @@ package com.google.cloud.spanner.it; import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assume.assumeFalse; import com.google.cloud.ByteArray; import com.google.cloud.Timestamp; @@ -89,9 +88,6 @@ private static List manyRows() { @BeforeClass public static void setUpDatabase() throws Exception { - assumeFalse( - "BatchReadOnlyTransactions are not supported on the emulator", - env.getTestHelper().isEmulator()); db = env.getTestHelper() .createTestDatabase( @@ -177,7 +173,7 @@ public void readUsingIndex() { } @After - public void tearDown() throws Exception { + public void tearDown() { if (batchTxn != null) { batchTxn.close(); } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITClosedSessionTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITClosedSessionTest.java index 40913f9f2d..3031b0edd8 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITClosedSessionTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITClosedSessionTest.java @@ -17,6 +17,8 @@ package com.google.cloud.spanner.it; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; import static org.junit.Assume.assumeFalse; import com.google.cloud.spanner.AbortedException; @@ -37,10 +39,8 @@ import org.junit.Before; import org.junit.BeforeClass; import org.junit.ClassRule; -import org.junit.Rule; import org.junit.Test; import org.junit.experimental.categories.Category; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -57,7 +57,6 @@ public class ITClosedSessionTest { new IntegrationTestWithClosedSessionsEnv(); private static Database db; - @Rule public ExpectedException expectedException = ExpectedException.none(); private static DatabaseClientWithClosedSessionImpl client; @BeforeClass @@ -94,9 +93,11 @@ public void testSingleUseNoRecreation() { // This should trigger an exception with code NOT_FOUND and the text 'Session not found'. client.setAllowSessionReplacing(false); client.invalidateNextSession(); - expectedException.expect(SessionNotFoundException.class); try (ResultSet rs = Statement.of("SELECT 1").executeQuery(client.singleUse())) { rs.next(); + fail("Expected exception"); + } catch (SessionNotFoundException ex) { + assertNotNull(ex.getMessage()); } } @@ -169,11 +170,14 @@ public void testReadOnlyTransaction() { public void testReadOnlyTransactionNoRecreation() { client.setAllowSessionReplacing(false); client.invalidateNextSession(); - expectedException.expect(SessionNotFoundException.class); try (ReadOnlyTransaction txn = client.readOnlyTransaction()) { try (ResultSet rs = txn.executeQuery(Statement.of("SELECT 1"))) { rs.next(); + fail("Expected exception"); } + fail("Expected exception"); + } catch (SessionNotFoundException ex) { + assertNotNull(ex.getMessage()); } } @@ -203,7 +207,7 @@ public void testReadWriteTransaction() { txn.run( new TransactionCallable() { @Override - public Void run(TransactionContext transaction) throws Exception { + public Void run(TransactionContext transaction) { for (int i = 0; i < 2; i++) { try (ResultSet rs = transaction.executeQuery(Statement.of("SELECT 1"))) { assertThat(rs.next()).isTrue(); @@ -221,18 +225,23 @@ public Void run(TransactionContext transaction) throws Exception { public void testReadWriteTransactionNoRecreation() { client.setAllowSessionReplacing(false); client.invalidateNextSession(); - expectedException.expect(SessionNotFoundException.class); - TransactionRunner txn = client.readWriteTransaction(); - txn.run( - new TransactionCallable() { - @Override - public Void run(TransactionContext transaction) throws Exception { - try (ResultSet rs = transaction.executeQuery(Statement.of("SELECT 1"))) { - rs.next(); + try { + TransactionRunner txn = client.readWriteTransaction(); + txn.run( + new TransactionCallable() { + @Override + public Void run(TransactionContext transaction) { + try (ResultSet rs = transaction.executeQuery(Statement.of("SELECT 1"))) { + rs.next(); + fail("Expected exception"); + } + return null; } - return null; - } - }); + }); + fail("Expected exception"); + } catch (SessionNotFoundException ex) { + assertNotNull(ex.getMessage()); + } } @Test @@ -262,17 +271,19 @@ public void testTransactionManager() throws InterruptedException { } @Test - public void testTransactionManagerNoRecreation() throws InterruptedException { + public void testTransactionManagerNoRecreation() { client.setAllowSessionReplacing(false); client.invalidateNextSession(); - expectedException.expect(SessionNotFoundException.class); try (TransactionManager manager = client.transactionManager()) { TransactionContext txn = manager.begin(); while (true) { try (ResultSet rs = txn.executeQuery(Statement.of("SELECT 1"))) { rs.next(); + fail("Expected exception"); } } + } catch (SessionNotFoundException ex) { + assertNotNull(ex.getMessage()); } } } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITCommitTimestampTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITCommitTimestampTest.java index c2cba6cb54..84d1a67559 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITCommitTimestampTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITCommitTimestampTest.java @@ -34,11 +34,13 @@ import com.google.cloud.spanner.Struct; import com.google.cloud.spanner.TimestampBound; import com.google.cloud.spanner.Value; +import com.google.cloud.spanner.connection.ConnectionOptions; import com.google.cloud.spanner.testing.RemoteSpannerHelper; import com.google.common.collect.ImmutableList; import java.util.Arrays; import java.util.concurrent.ExecutionException; import org.junit.After; +import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Test; @@ -61,7 +63,7 @@ public class ITCommitTimestampTest { private static String databaseId; @BeforeClass - public static void setUp() throws Exception { + public static void setUp() { testHelper = env.getTestHelper(); db = testHelper.createTestDatabase( @@ -77,6 +79,11 @@ public static void setUp() throws Exception { databaseId = db.getId().getDatabase(); } + @AfterClass + public static void teardown() { + ConnectionOptions.closeSpanner(); + } + @After public void deleteAllTestRecords() { client.write(ImmutableList.of(Mutation.delete("T", KeySet.all()))); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDMLTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDMLTest.java index 9af4a22f76..aabf93b3a6 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDMLTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDMLTest.java @@ -18,7 +18,6 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; -import static org.junit.Assume.assumeFalse; import com.google.cloud.spanner.AbortedException; import com.google.cloud.spanner.Database; @@ -137,8 +136,6 @@ public void abortOnceShouldSucceedAfterRetry() { @Test public void partitionedDML() { - assumeFalse("The emulator does not support partitioned DML", env.getTestHelper().isEmulator()); - executeUpdate(DML_COUNT, insertDml()); assertThat( client diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDatabaseAdminTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDatabaseAdminTest.java index 39260122ea..f66154d66c 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDatabaseAdminTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDatabaseAdminTest.java @@ -16,8 +16,8 @@ package com.google.cloud.spanner.it; -import static com.google.cloud.spanner.SpannerMatchers.isSpannerException; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; import static org.junit.Assume.assumeFalse; import com.google.api.gax.grpc.GrpcInterceptorProvider; @@ -45,7 +45,6 @@ import io.grpc.CallOptions; import io.grpc.Channel; import io.grpc.ClientCall; -import io.grpc.ClientCall.Listener; import io.grpc.ClientInterceptor; import io.grpc.ForwardingClientCall.SimpleForwardingClientCall; import io.grpc.ForwardingClientCallListener.SimpleForwardingClientCallListener; @@ -63,10 +62,8 @@ import org.junit.After; import org.junit.Before; import org.junit.ClassRule; -import org.junit.Rule; import org.junit.Test; import org.junit.experimental.categories.Category; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -75,19 +72,18 @@ @RunWith(JUnit4.class) public class ITDatabaseAdminTest { @ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv(); - @Rule public ExpectedException expectedException = ExpectedException.none(); private DatabaseAdminClient dbAdminClient; private RemoteSpannerHelper testHelper; private List dbs = new ArrayList<>(); @Before - public void setUp() throws Exception { + public void setUp() { testHelper = env.getTestHelper(); dbAdminClient = testHelper.getClient().getDatabaseAdminClient(); } @After - public void tearDown() throws Exception { + public void tearDown() { for (Database db : dbs) { db.drop(); } @@ -128,8 +124,12 @@ public void databaseOperations() throws Exception { dbAdminClient.dropDatabase(instanceId, dbId); dbs.clear(); - expectedException.expect(isSpannerException(ErrorCode.NOT_FOUND)); - db = dbAdminClient.getDatabase(testHelper.getInstanceId().getInstance(), dbId); + try { + db = dbAdminClient.getDatabase(testHelper.getInstanceId().getInstance(), dbId); + fail("Expected exception"); + } catch (SpannerException ex) { + assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.NOT_FOUND); + } } @Test @@ -170,11 +170,14 @@ public void databaseOperationsViaEntity() throws Exception { op2.get(); Iterable statementsInDb = db.getDdl(); assertThat(statementsInDb).containsExactly(statement1, statement2); - db.drop(); dbs.clear(); - expectedException.expect(isSpannerException(ErrorCode.NOT_FOUND)); - db.reload(); + try { + db.reload(); + fail("Expected exception"); + } catch (SpannerException ex) { + assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.NOT_FOUND); + } } @Test diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDatabaseTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDatabaseTest.java index af1193f298..450c7463c5 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDatabaseTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDatabaseTest.java @@ -16,7 +16,6 @@ package com.google.cloud.spanner.it; -import static com.google.cloud.spanner.SpannerMatchers.isSpannerException; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; import static org.junit.Assume.assumeFalse; @@ -33,14 +32,13 @@ import com.google.cloud.spanner.IntegrationTestEnv; import com.google.cloud.spanner.ParallelIntegrationTest; import com.google.cloud.spanner.ResultSet; +import com.google.cloud.spanner.SpannerException; import com.google.cloud.spanner.Statement; import com.google.spanner.admin.database.v1.CreateDatabaseMetadata; import java.util.Collections; import org.junit.ClassRule; -import org.junit.Rule; import org.junit.Test; import org.junit.experimental.categories.Category; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -49,14 +47,16 @@ @RunWith(JUnit4.class) public class ITDatabaseTest { @ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv(); - @Rule public ExpectedException expectedException = ExpectedException.none(); @Test public void badDdl() { - expectedException.expect(isSpannerException(ErrorCode.INVALID_ARGUMENT)); - expectedException.expectMessage("Syntax error on line 1"); - - env.getTestHelper().createTestDatabase("CREATE TABLE T ( Illegal Way To Define A Table )"); + try { + env.getTestHelper().createTestDatabase("CREATE TABLE T ( Illegal Way To Define A Table )"); + fail("Expected exception"); + } catch (SpannerException ex) { + assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + assertThat(ex.getMessage()).contains("Syntax error on line 1"); + } } @Test @@ -146,6 +146,7 @@ public void instanceNotFound() { .getClient() .getDatabaseClient(DatabaseId.of(nonExistingInstanceId, "some-db")); try (ResultSet rs = client.singleUse().executeQuery(Statement.of("SELECT 1"))) { + rs.next(); fail("missing expected exception"); } catch (InstanceNotFoundException e) { assertThat(e.getResourceName()).isEqualTo(nonExistingInstanceId.getName()); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITInstanceAdminTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITInstanceAdminTest.java index 5b9e3a06a9..68e4e615a3 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITInstanceAdminTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITInstanceAdminTest.java @@ -34,10 +34,8 @@ import java.util.Random; import org.junit.Before; import org.junit.ClassRule; -import org.junit.Rule; import org.junit.Test; import org.junit.experimental.categories.Category; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -46,7 +44,6 @@ @RunWith(JUnit4.class) public class ITInstanceAdminTest { @ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv(); - @Rule public ExpectedException expectedException = ExpectedException.none(); InstanceAdminClient instanceClient; @Before @@ -67,7 +64,7 @@ public void instanceConfigOperations() { } @Test - public void listInstances() throws Exception { + public void listInstances() { Instance instance = Iterators.getOnlyElement( instanceClient diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITQueryOptionsTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITQueryOptionsTest.java index c60e2ae889..30cfa80c45 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITQueryOptionsTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITQueryOptionsTest.java @@ -34,10 +34,8 @@ import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions; import org.junit.BeforeClass; import org.junit.ClassRule; -import org.junit.Rule; import org.junit.Test; import org.junit.experimental.categories.Category; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -46,7 +44,6 @@ public class ITQueryOptionsTest { @ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv(); private static Database db; - @Rule public ExpectedException expectedException = ExpectedException.none(); private static DatabaseClient client; @BeforeClass @@ -114,7 +111,7 @@ public void executeUpdate() { .run( new TransactionCallable() { @Override - public Long run(TransactionContext transaction) throws Exception { + public Long run(TransactionContext transaction) { return transaction.executeUpdate( Statement.newBuilder("INSERT INTO TEST (ID, NAME) VALUES (@id, @name)") .bind("id") @@ -135,7 +132,7 @@ public Long run(TransactionContext transaction) throws Exception { .run( new TransactionCallable() { @Override - public Long run(TransactionContext transaction) throws Exception { + public Long run(TransactionContext transaction) { return transaction.executeUpdate( Statement.newBuilder("INSERT INTO TEST (ID, NAME) VALUES (@id, @name)") .bind("id") @@ -156,7 +153,7 @@ public Long run(TransactionContext transaction) throws Exception { .run( new TransactionCallable() { @Override - public Long run(TransactionContext transaction) throws Exception { + public Long run(TransactionContext transaction) { return transaction.executeUpdate( Statement.newBuilder("INSERT INTO TEST (ID, NAME) VALUES (@id, @name)") .bind("id") diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITQueryTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITQueryTest.java index 07e520c8c0..b54984362d 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITQueryTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITQueryTest.java @@ -16,10 +16,10 @@ package com.google.cloud.spanner.it; -import static com.google.cloud.spanner.SpannerMatchers.isSpannerException; import static com.google.cloud.spanner.Type.StructField; import static com.google.common.truth.Truth.assertThat; import static java.util.Arrays.asList; +import static org.junit.Assert.fail; import static org.junit.Assume.assumeFalse; import com.google.cloud.ByteArray; @@ -33,6 +33,7 @@ import com.google.cloud.spanner.ParallelIntegrationTest; import com.google.cloud.spanner.ReadContext.QueryAnalyzeMode; import com.google.cloud.spanner.ResultSet; +import com.google.cloud.spanner.SpannerException; import com.google.cloud.spanner.Statement; import com.google.cloud.spanner.Struct; import com.google.cloud.spanner.TimestampBound; @@ -47,10 +48,8 @@ import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Ignore; -import org.junit.Rule; import org.junit.Test; import org.junit.experimental.categories.Category; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -60,7 +59,6 @@ public class ITQueryTest { @ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv(); private static Database db; - @Rule public ExpectedException expectedException = ExpectedException.none(); private static DatabaseClient client; @BeforeClass @@ -78,9 +76,13 @@ public void simple() { @Test public void badQuery() { - expectedException.expect(isSpannerException(ErrorCode.INVALID_ARGUMENT)); - expectedException.expectMessage("Unrecognized name: Apples"); - execute(Statement.of("SELECT Apples AND Oranges"), Type.int64()); + try { + execute(Statement.of("SELECT Apples AND Oranges"), Type.int64()); + fail("Expected exception"); + } catch (SpannerException ex) { + assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + assertThat(ex.getMessage()).contains("Unrecognized name: Apples"); + } } @Test @@ -494,12 +496,16 @@ public void bindDateArrayNull() { @Test public void unsupportedSelectStructValue() { assumeFalse("The emulator accepts this query", env.getTestHelper().isEmulator()); - Struct p = structValue(); - expectedException.expect(isSpannerException(ErrorCode.UNIMPLEMENTED)); - expectedException.expectMessage( - "Unsupported query shape: " + "A struct value cannot be returned as a column value."); - execute(Statement.newBuilder("SELECT @p").bind("p").to(p).build(), p.getType()); + try { + execute(Statement.newBuilder("SELECT @p").bind("p").to(p).build(), p.getType()); + fail("Expected exception"); + } catch (SpannerException ex) { + assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.UNIMPLEMENTED); + assertThat(ex.getMessage()) + .contains( + "Unsupported query shape: A struct value cannot be returned as a column value."); + } } @Test @@ -509,23 +515,31 @@ public void unsupportedSelectArrayStructValue() { env.getTestHelper().isEmulator()); Struct p = structValue(); - expectedException.expect(isSpannerException(ErrorCode.UNIMPLEMENTED)); - expectedException.expectMessage( - "Unsupported query shape: " - + "This query can return a null-valued array of struct, " - + "which is not supported by Spanner."); - execute( - Statement.newBuilder("SELECT @p").bind("p").toStructArray(p.getType(), asList(p)).build(), - p.getType()); + try { + execute( + Statement.newBuilder("SELECT @p").bind("p").toStructArray(p.getType(), asList(p)).build(), + p.getType()); + fail("Expected exception"); + } catch (SpannerException ex) { + assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.UNIMPLEMENTED); + assertThat(ex.getMessage()) + .contains( + "Unsupported query shape: " + + "This query can return a null-valued array of struct, " + + "which is not supported by Spanner."); + } } @Test public void invalidAmbiguousFieldAccess() { Struct p = Struct.newBuilder().set("f1").to(20).set("f1").to("abc").build(); - - expectedException.expect(isSpannerException(ErrorCode.INVALID_ARGUMENT)); - expectedException.expectMessage("Struct field name f1 is ambiguous"); - execute(Statement.newBuilder("SELECT @p.f1").bind("p").to(p).build(), Type.int64()); + try { + execute(Statement.newBuilder("SELECT @p.f1").bind("p").to(p).build(), Type.int64()); + fail("Expected exception"); + } catch (SpannerException ex) { + assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + assertThat(ex.getMessage()).contains("Struct field name f1 is ambiguous"); + } } private Struct structValue() { @@ -720,8 +734,12 @@ public void bindStructWithArrayOfStructField() { public void unboundParameter() { ResultSet resultSet = Statement.of("SELECT @v").executeQuery(client.singleUse(TimestampBound.strong())); - expectedException.expect(isSpannerException(ErrorCode.INVALID_ARGUMENT)); - resultSet.next(); + try { + resultSet.next(); + fail("Expected exception"); + } catch (SpannerException ex) { + assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + } } @Test @@ -766,9 +784,13 @@ public void largeErrorText() { .to("(" + veryLongString) .build(); ResultSet resultSet = statement.executeQuery(client.singleUse(TimestampBound.strong())); - expectedException.expect(isSpannerException(ErrorCode.OUT_OF_RANGE)); - expectedException.expectMessage("Cannot parse regular expression"); - resultSet.next(); + try { + resultSet.next(); + fail("Expected exception"); + } catch (SpannerException ex) { + assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.OUT_OF_RANGE); + assertThat(ex.getMessage()).contains("Cannot parse regular expression"); + } } @Test diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITReadOnlyTxnTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITReadOnlyTxnTest.java index e6e473779d..db68b5ec88 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITReadOnlyTxnTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITReadOnlyTxnTest.java @@ -17,6 +17,8 @@ package com.google.cloud.spanner.it; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; import com.google.cloud.Timestamp; import com.google.cloud.spanner.Database; @@ -41,10 +43,8 @@ import org.junit.Before; import org.junit.BeforeClass; import org.junit.ClassRule; -import org.junit.Rule; import org.junit.Test; import org.junit.experimental.categories.Category; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -62,7 +62,6 @@ public class ITReadOnlyTxnTest { private static final String TABLE_NAME = "TestTable"; private static DatabaseClient sharedClient; private static List sharedHistory; - @Rule public ExpectedException expectedException = ExpectedException.none(); private List history; private DatabaseClient client; @@ -311,8 +310,15 @@ public void multiReadTimestamp() { @Test public void multiMinReadTimestamp() { // Cannot use bounded modes with multi-read transactions. - expectedException.expect(IllegalArgumentException.class); - client.readOnlyTransaction(TimestampBound.ofMinReadTimestamp(history.get(2).timestamp)); + try (ReadOnlyTransaction tx = + client.readOnlyTransaction(TimestampBound.ofMinReadTimestamp(history.get(2).timestamp))) { + try (ResultSet rs = tx.executeQuery(Statement.of("SELECT 1"))) { + rs.next(); + fail("Expected exception"); + } + } catch (IllegalArgumentException ex) { + assertNotNull(ex.getMessage()); + } } @Test @@ -338,7 +344,14 @@ public void multiExactStaleness() { @Test public void multiMaxStaleness() { // Cannot use bounded modes with multi-read transactions. - expectedException.expect(IllegalArgumentException.class); - client.readOnlyTransaction(TimestampBound.ofMaxStaleness(1, TimeUnit.SECONDS)); + try (ReadOnlyTransaction tx = + client.readOnlyTransaction(TimestampBound.ofMaxStaleness(1, TimeUnit.SECONDS))) { + try (ResultSet rs = tx.executeQuery(Statement.of("SELECT 1"))) { + rs.next(); + fail("Expected exception"); + } + } catch (IllegalArgumentException ex) { + assertNotNull(ex.getMessage()); + } } } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITReadTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITReadTest.java index b06d3ae152..87c9e0ae3f 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITReadTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITReadTest.java @@ -20,6 +20,7 @@ import static com.google.cloud.spanner.Type.StructField; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; +import static org.junit.Assert.fail; import com.google.cloud.spanner.Database; import com.google.cloud.spanner.DatabaseClient; @@ -50,10 +51,8 @@ import org.hamcrest.MatcherAssert; import org.junit.BeforeClass; import org.junit.ClassRule; -import org.junit.Rule; import org.junit.Test; import org.junit.experimental.categories.Category; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -76,7 +75,6 @@ public class ITReadTest { StructField.of("Key", Type.string()), StructField.of("StringValue", Type.string())); private static Database db; - @Rule public ExpectedException expectedException = ExpectedException.none(); private static DatabaseClient client; @BeforeClass @@ -292,26 +290,38 @@ public void invalidDatabase() { RemoteSpannerHelper helper = env.getTestHelper(); DatabaseClient invalidClient = helper.getClient().getDatabaseClient(DatabaseId.of(helper.getInstanceId(), "invalid")); - expectedException.expect(isSpannerException(ErrorCode.NOT_FOUND)); - invalidClient - .singleUse(TimestampBound.strong()) - .readRow(TABLE_NAME, Key.of("k99"), ALL_COLUMNS); + try { + invalidClient + .singleUse(TimestampBound.strong()) + .readRow(TABLE_NAME, Key.of("k99"), ALL_COLUMNS); + fail("Expected exception"); + } catch (SpannerException ex) { + assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.NOT_FOUND); + } } @Test public void tableNotFound() { - expectedException.expect(isSpannerException(ErrorCode.NOT_FOUND)); - expectedException.expectMessage("BadTableName"); - client.singleUse(TimestampBound.strong()).readRow("BadTableName", Key.of("k1"), ALL_COLUMNS); + try { + client.singleUse(TimestampBound.strong()).readRow("BadTableName", Key.of("k1"), ALL_COLUMNS); + fail("Expected exception"); + } catch (SpannerException ex) { + assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.NOT_FOUND); + assertThat(ex.getMessage()).contains("BadTableName"); + } } @Test public void columnNotFound() { - expectedException.expect(isSpannerException(ErrorCode.NOT_FOUND)); - expectedException.expectMessage("BadColumnName"); - client - .singleUse(TimestampBound.strong()) - .readRow(TABLE_NAME, Key.of("k1"), Arrays.asList("Key", "BadColumnName")); + try { + client + .singleUse(TimestampBound.strong()) + .readRow(TABLE_NAME, Key.of("k1"), Arrays.asList("Key", "BadColumnName")); + fail("Expected exception"); + } catch (SpannerException ex) { + assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.NOT_FOUND); + assertThat(ex.getMessage()).contains("BadColumnName"); + } } @Test @@ -322,10 +332,13 @@ public void cursorErrorDeferred() { client .singleUse(TimestampBound.strong()) .read("BadTableName", KeySet.singleKey(Key.of("k1")), ALL_COLUMNS); - - expectedException.expect(isSpannerException(ErrorCode.NOT_FOUND)); - expectedException.expectMessage("BadTableName"); - resultSet.next(); + try { + resultSet.next(); + fail("Expected exception"); + } catch (SpannerException ex) { + assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.NOT_FOUND); + assertThat(ex.getMessage()).contains("BadTableName"); + } } @Test @@ -345,6 +358,7 @@ public void run() { try { work.run(); + fail("missing expected exception"); } catch (SpannerException e) { MatcherAssert.assertThat(e, isSpannerException(ErrorCode.CANCELLED)); } @@ -368,6 +382,7 @@ public void run() { try { work.run(); + fail("missing expected exception"); } catch (SpannerException e) { MatcherAssert.assertThat(e, isSpannerException(ErrorCode.DEADLINE_EXCEEDED)); } finally { diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITSpannerOptionsTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITSpannerOptionsTest.java index 34e6c43944..db8c725fc5 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITSpannerOptionsTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITSpannerOptionsTest.java @@ -19,20 +19,13 @@ import static com.google.common.truth.Truth.assertThat; import com.google.cloud.spanner.Database; -import com.google.cloud.spanner.DatabaseAdminClient; import com.google.cloud.spanner.DatabaseClient; -import com.google.cloud.spanner.InstanceAdminClient; import com.google.cloud.spanner.IntegrationTestEnv; import com.google.cloud.spanner.ParallelIntegrationTest; import com.google.cloud.spanner.ResultSet; import com.google.cloud.spanner.Spanner; import com.google.cloud.spanner.SpannerOptions; import com.google.cloud.spanner.Statement; -import com.google.common.base.Stopwatch; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.TimeUnit; -import java.util.regex.Pattern; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.ClassRule; @@ -57,142 +50,19 @@ public static void tearDown() throws Exception { db.drop(); } - private static final int NUMBER_OF_TEST_RUNS = 2; - private static final int DEFAULT_NUM_CHANNELS = 4; - private static final int NUM_THREADS_PER_CHANNEL = 4; - private static final String SPANNER_THREAD_NAME = "Cloud-Spanner-TransportChannel"; - private static final String THREAD_PATTERN = "%s-[0-9]+"; - - @Test - public void testCloseAllThreadsWhenClosingSpanner() throws InterruptedException { - int baseThreadCount = getNumberOfThreadsWithName(SPANNER_THREAD_NAME); - for (int i = 0; i < NUMBER_OF_TEST_RUNS; i++) { - waitForStartup(); - assertThat(getNumberOfThreadsWithName(SPANNER_THREAD_NAME)).isAtMost(baseThreadCount); - // Create Spanner instance. - // We make a copy of the options instance, as SpannerOptions caches any service object - // that has been handed out. - SpannerOptions options = env.getTestHelper().getOptions().toBuilder().build(); - Spanner spanner = options.getService(); - // Get a database client and do a query. This should initiate threads for the Spanner service. - DatabaseClient client = spanner.getDatabaseClient(db.getId()); - List resultSets = new ArrayList<>(); - // SpannerStub affiliates a channel with a session, so we need to use multiple sessions - // to ensure we also hit multiple channels. - for (int i2 = 0; i2 < options.getSessionPoolOptions().getMaxSessions(); i2++) { - ResultSet rs = - client - .singleUse() - .executeQuery(Statement.of("SELECT 1 AS COL1 UNION ALL SELECT 2 AS COL2")); - // Execute ResultSet#next() to send the query to Spanner. - rs.next(); - // Delay closing the result set in order to force the use of multiple sessions. - // As each session is linked to one transport channel, using multiple different - // sessions should initialize multiple transport channels. - resultSets.add(rs); - // Check whether the number of expected threads has been reached. - if (getNumberOfThreadsWithName(SPANNER_THREAD_NAME) - == DEFAULT_NUM_CHANNELS * NUM_THREADS_PER_CHANNEL + baseThreadCount) { - break; - } - } - for (ResultSet rs : resultSets) { - rs.close(); - } - // Check the number of threads after the query. Doing a request should initialize a thread - // pool for the underlying SpannerClient. - assertThat(getNumberOfThreadsWithName(SPANNER_THREAD_NAME)) - .isEqualTo(DEFAULT_NUM_CHANNELS * NUM_THREADS_PER_CHANNEL + baseThreadCount); - - // Then do a request to the InstanceAdmin service and check the number of threads. - // Doing a request should initialize a thread pool for the underlying InstanceAdminClient. - for (int i2 = 0; i2 < DEFAULT_NUM_CHANNELS * 2; i2++) { - InstanceAdminClient instanceAdminClient = spanner.getInstanceAdminClient(); - instanceAdminClient.listInstances(); - } - assertThat(getNumberOfThreadsWithName(SPANNER_THREAD_NAME)) - .isEqualTo(2 * DEFAULT_NUM_CHANNELS * NUM_THREADS_PER_CHANNEL + baseThreadCount); - - // Then do a request to the DatabaseAdmin service and check the number of threads. - // Doing a request should initialize a thread pool for the underlying DatabaseAdminClient. - for (int i2 = 0; i2 < DEFAULT_NUM_CHANNELS * 2; i2++) { - DatabaseAdminClient databaseAdminClient = spanner.getDatabaseAdminClient(); - databaseAdminClient.listDatabases(db.getId().getInstanceId().getInstance()); - } - assertThat(getNumberOfThreadsWithName(SPANNER_THREAD_NAME)) - .isEqualTo(3 * DEFAULT_NUM_CHANNELS * NUM_THREADS_PER_CHANNEL + baseThreadCount); - - // Now close the Spanner instance and check whether the threads are shutdown or not. - spanner.close(); - // Wait a little to allow the threads to actually shutdown. - Stopwatch watch = Stopwatch.createStarted(); - while (getNumberOfThreadsWithName(SPANNER_THREAD_NAME) > baseThreadCount - && watch.elapsed(TimeUnit.SECONDS) < 2) { - Thread.sleep(50L); - } - assertThat(getNumberOfThreadsWithName(SPANNER_THREAD_NAME)).isAtMost(baseThreadCount); - } - } - @Test - public void testMultipleSpannersFromSameSpannerOptions() throws InterruptedException { - waitForStartup(); - int baseThreadCount = getNumberOfThreadsWithName(SPANNER_THREAD_NAME); - SpannerOptions options = env.getTestHelper().getOptions().toBuilder().build(); - try (Spanner spanner1 = options.getService()) { - // Having both in the try-with-resources block is not possible, as it is the same instance. - // One will be closed before the other, and the closing of the second instance would fail. - Spanner spanner2 = options.getService(); - assertThat(spanner1).isSameInstanceAs(spanner2); - DatabaseClient client1 = spanner1.getDatabaseClient(db.getId()); - DatabaseClient client2 = spanner2.getDatabaseClient(db.getId()); - assertThat(client1).isSameInstanceAs(client2); - try (ResultSet rs1 = - client1 - .singleUse() - .executeQuery(Statement.of("SELECT 1 AS COL1 UNION ALL SELECT 2 AS COL2")); - ResultSet rs2 = - client2 - .singleUse() - .executeQuery(Statement.of("SELECT 1 AS COL1 UNION ALL SELECT 2 AS COL2")); ) { - while (rs1.next() && rs2.next()) { - // Do nothing, just consume the result sets. + public void testCompression() { + for (String compressorName : new String[] {"gzip", "identity", null}) { + SpannerOptions options = + env.getTestHelper().getOptions().toBuilder().setCompressorName(compressorName).build(); + try (Spanner spanner = options.getService()) { + DatabaseClient client = spanner.getDatabaseClient(db.getId()); + try (ResultSet rs = client.singleUse().executeQuery(Statement.of("SELECT 1 AS COL1"))) { + assertThat(rs.next()).isTrue(); + assertThat(rs.getLong(0)).isEqualTo(1L); + assertThat(rs.next()).isFalse(); } } } - Stopwatch watch = Stopwatch.createStarted(); - while (getNumberOfThreadsWithName(SPANNER_THREAD_NAME) > baseThreadCount - && watch.elapsed(TimeUnit.SECONDS) < 2) { - Thread.sleep(50L); - } - assertThat(getNumberOfThreadsWithName(SPANNER_THREAD_NAME)).isAtMost(baseThreadCount); - } - - private void waitForStartup() throws InterruptedException { - // Wait until the IT environment has already started all base worker threads. - int threadCount; - Stopwatch watch = Stopwatch.createStarted(); - do { - threadCount = getNumberOfThreadsWithName(SPANNER_THREAD_NAME); - Thread.sleep(100L); - } while (getNumberOfThreadsWithName(SPANNER_THREAD_NAME) > threadCount - && watch.elapsed(TimeUnit.SECONDS) < 5); - } - - private int getNumberOfThreadsWithName(String serviceName) { - Pattern pattern = Pattern.compile(String.format(THREAD_PATTERN, serviceName)); - ThreadGroup group = Thread.currentThread().getThreadGroup(); - while (group.getParent() != null) { - group = group.getParent(); - } - Thread[] threads = new Thread[100 * NUMBER_OF_TEST_RUNS]; - int numberOfThreads = group.enumerate(threads); - int res = 0; - for (int i = 0; i < numberOfThreads; i++) { - if (pattern.matcher(threads[i].getName()).matches()) { - res++; - } - } - return res; } } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionManagerAsyncTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionManagerAsyncTest.java new file mode 100644 index 0000000000..c802493dec --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionManagerAsyncTest.java @@ -0,0 +1,318 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.spanner.it; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; +import static org.junit.Assume.assumeFalse; + +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutures; +import com.google.cloud.spanner.AbortedException; +import com.google.cloud.spanner.AsyncTransactionManager; +import com.google.cloud.spanner.AsyncTransactionManager.AsyncTransactionFunction; +import com.google.cloud.spanner.AsyncTransactionManager.AsyncTransactionStep; +import com.google.cloud.spanner.AsyncTransactionManager.TransactionContextFuture; +import com.google.cloud.spanner.Database; +import com.google.cloud.spanner.DatabaseClient; +import com.google.cloud.spanner.ErrorCode; +import com.google.cloud.spanner.IntegrationTestEnv; +import com.google.cloud.spanner.Key; +import com.google.cloud.spanner.KeySet; +import com.google.cloud.spanner.Mutation; +import com.google.cloud.spanner.SpannerException; +import com.google.cloud.spanner.Struct; +import com.google.cloud.spanner.TransactionContext; +import com.google.cloud.spanner.TransactionManager.TransactionState; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.MoreExecutors; +import java.util.Arrays; +import java.util.Collection; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class ITTransactionManagerAsyncTest { + + @Parameter public Executor executor; + + @Parameters(name = "executor = {0}") + public static Collection data() { + return Arrays.asList( + new Object[][] { + {MoreExecutors.directExecutor()}, + {Executors.newSingleThreadExecutor()}, + {Executors.newFixedThreadPool(4)} + }); + } + + @ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv(); + private static Database db; + private static DatabaseClient client; + + @BeforeClass + public static void setUpDatabase() { + // Empty database. + db = + env.getTestHelper() + .createTestDatabase( + "CREATE TABLE T (" + + " K STRING(MAX) NOT NULL," + + " BoolValue BOOL," + + ") PRIMARY KEY (K)"); + client = env.getTestHelper().getDatabaseClient(db); + } + + @Before + public void clearTable() { + client.write(ImmutableList.of(Mutation.delete("T", KeySet.all()))); + } + + @Test + public void testSimpleInsert() throws ExecutionException, InterruptedException { + try (AsyncTransactionManager manager = client.transactionManagerAsync()) { + TransactionContextFuture txn = manager.beginAsync(); + while (true) { + assertThat(manager.getState()).isEqualTo(TransactionState.STARTED); + try { + txn.then( + new AsyncTransactionFunction() { + @Override + public ApiFuture apply(TransactionContext txn, Void input) + throws Exception { + txn.buffer( + Mutation.newInsertBuilder("T") + .set("K") + .to("Key1") + .set("BoolValue") + .to(true) + .build()); + return ApiFutures.immediateFuture(null); + } + }, + executor) + .commitAsync() + .get(); + assertThat(manager.getState()).isEqualTo(TransactionState.COMMITTED); + Struct row = + client.singleUse().readRow("T", Key.of("Key1"), Arrays.asList("K", "BoolValue")); + assertThat(row.getString(0)).isEqualTo("Key1"); + assertThat(row.getBoolean(1)).isTrue(); + break; + } catch (AbortedException e) { + Thread.sleep(e.getRetryDelayInMillis() / 1000); + txn = manager.resetForRetryAsync(); + } + } + } + } + + @Test + public void testInvalidInsert() throws InterruptedException { + try (AsyncTransactionManager manager = client.transactionManagerAsync()) { + TransactionContextFuture txn = manager.beginAsync(); + while (true) { + try { + txn.then( + new AsyncTransactionFunction() { + @Override + public ApiFuture apply(TransactionContext txn, Void input) + throws Exception { + txn.buffer( + Mutation.newInsertBuilder("InvalidTable") + .set("K") + .to("Key1") + .set("BoolValue") + .to(true) + .build()); + return ApiFutures.immediateFuture(null); + } + }, + executor) + .commitAsync() + .get(); + fail("Expected exception"); + } catch (AbortedException e) { + Thread.sleep(e.getRetryDelayInMillis() / 1000); + txn = manager.resetForRetryAsync(); + } catch (ExecutionException e) { + assertThat(e.getCause()).isInstanceOf(SpannerException.class); + SpannerException se = (SpannerException) e.getCause(); + assertThat(se.getErrorCode()).isEqualTo(ErrorCode.NOT_FOUND); + // expected + break; + } + } + assertThat(manager.getState()).isEqualTo(TransactionState.COMMIT_FAILED); + // We cannot retry for non aborted errors. + try { + manager.resetForRetryAsync(); + fail("Expected exception"); + } catch (IllegalStateException ex) { + assertNotNull(ex.getMessage()); + } + } + } + + @Test + public void testRollback() throws InterruptedException { + try (AsyncTransactionManager manager = client.transactionManagerAsync()) { + TransactionContextFuture txn = manager.beginAsync(); + while (true) { + txn.then( + new AsyncTransactionFunction() { + @Override + public ApiFuture apply(TransactionContext txn, Void input) throws Exception { + txn.buffer( + Mutation.newInsertBuilder("T") + .set("K") + .to("Key2") + .set("BoolValue") + .to(true) + .build()); + return ApiFutures.immediateFuture(null); + } + }, + executor); + try { + manager.rollbackAsync(); + break; + } catch (AbortedException e) { + Thread.sleep(e.getRetryDelayInMillis() / 1000); + txn = manager.resetForRetryAsync(); + } + } + assertThat(manager.getState()).isEqualTo(TransactionState.ROLLED_BACK); + // Row should not have been inserted. + assertThat(client.singleUse().readRow("T", Key.of("Key2"), Arrays.asList("K", "BoolValue"))) + .isNull(); + } + } + + @Test + public void testAbortAndRetry() throws InterruptedException, ExecutionException { + assumeFalse( + "Emulator does not support more than 1 simultanous transaction. " + + "This test would therefore loop indefinetly on the emulator.", + env.getTestHelper().isEmulator()); + + client.write( + Arrays.asList( + Mutation.newInsertBuilder("T").set("K").to("Key3").set("BoolValue").to(true).build())); + try (AsyncTransactionManager manager1 = client.transactionManagerAsync()) { + TransactionContextFuture txn1 = manager1.beginAsync(); + AsyncTransactionManager manager2; + TransactionContextFuture txn2; + AsyncTransactionStep txn2Step1; + while (true) { + try { + AsyncTransactionStep txn1Step1 = + txn1.then( + new AsyncTransactionFunction() { + @Override + public ApiFuture apply(TransactionContext txn, Void input) + throws Exception { + return txn.readRowAsync("T", Key.of("Key3"), Arrays.asList("K", "BoolValue")); + } + }, + executor); + manager2 = client.transactionManagerAsync(); + txn2 = manager2.beginAsync(); + txn2Step1 = + txn2.then( + new AsyncTransactionFunction() { + @Override + public ApiFuture apply(TransactionContext txn, Void input) + throws Exception { + return txn.readRowAsync("T", Key.of("Key3"), Arrays.asList("K", "BoolValue")); + } + }, + executor); + + AsyncTransactionStep txn1Step2 = + txn1Step1.then( + new AsyncTransactionFunction() { + @Override + public ApiFuture apply(TransactionContext txn, Struct input) + throws Exception { + txn.buffer( + Mutation.newUpdateBuilder("T") + .set("K") + .to("Key3") + .set("BoolValue") + .to(false) + .build()); + return ApiFutures.immediateFuture(null); + } + }, + executor); + + txn2Step1.get(); + txn1Step2.commitAsync().get(); + break; + } catch (AbortedException e) { + Thread.sleep(e.getRetryDelayInMillis() / 1000); + // It is possible that it was txn2 that aborted. + // In that case we should just retry without resetting anything. + if (manager1.getState() == TransactionState.ABORTED) { + txn1 = manager1.resetForRetryAsync(); + } + } + } + + // txn2 should have been aborted. + try { + txn2Step1.commitAsync().get(); + fail("Expected to abort"); + } catch (AbortedException e) { + assertThat(manager2.getState()).isEqualTo(TransactionState.ABORTED); + txn2 = manager2.resetForRetryAsync(); + } + AsyncTransactionStep txn2Step2 = + txn2.then( + new AsyncTransactionFunction() { + @Override + public ApiFuture apply(TransactionContext txn, Void input) throws Exception { + txn.buffer( + Mutation.newUpdateBuilder("T") + .set("K") + .to("Key3") + .set("BoolValue") + .to(true) + .build()); + return ApiFutures.immediateFuture(null); + } + }, + executor); + txn2Step2.commitAsync().get(); + Struct row = client.singleUse().readRow("T", Key.of("Key3"), Arrays.asList("K", "BoolValue")); + assertThat(row.getString(0)).isEqualTo("Key3"); + assertThat(row.getBoolean(1)).isTrue(); + manager2.close(); + } + } +} diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionManagerTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionManagerTest.java index b9776363c4..281977af6a 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionManagerTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionManagerTest.java @@ -17,6 +17,7 @@ package com.google.cloud.spanner.it; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.fail; import static org.junit.Assume.assumeFalse; @@ -35,10 +36,8 @@ import java.util.Arrays; import org.junit.BeforeClass; import org.junit.ClassRule; -import org.junit.Rule; import org.junit.Test; import org.junit.experimental.categories.Category; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -48,7 +47,6 @@ public class ITTransactionManagerTest { @ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv(); private static Database db; - @Rule public ExpectedException expectedException = ExpectedException.none(); private static DatabaseClient client; @BeforeClass @@ -115,8 +113,12 @@ public void invalidInsert() throws InterruptedException { } assertThat(manager.getState()).isEqualTo(TransactionState.COMMIT_FAILED); // We cannot retry for non aborted errors. - expectedException.expect(IllegalStateException.class); - manager.resetForRetry(); + try { + manager.resetForRetry(); + fail("Expected exception"); + } catch (IllegalStateException ex) { + assertNotNull(ex.getMessage()); + } } } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionTest.java index cc87023be0..5e3c1483e7 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITTransactionTest.java @@ -397,7 +397,7 @@ public Void run(TransactionContext transaction) throws SpannerException { .run( new TransactionCallable() { @Override - public Void run(TransactionContext transaction) throws Exception { + public Void run(TransactionContext transaction) { return null; } }); @@ -477,7 +477,12 @@ public void nestedSingleUseReadTxnThrows() { new TransactionCallable() { @Override public Void run(TransactionContext transaction) throws SpannerException { - client.singleUseReadOnlyTransaction(); + try (ResultSet rs = + client + .singleUseReadOnlyTransaction() + .executeQuery(Statement.of("SELECT 1"))) { + rs.next(); + } return null; } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITVPCNegativeTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITVPCNegativeTest.java index 1d64a3dfd4..63c1560e2f 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITVPCNegativeTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITVPCNegativeTest.java @@ -38,6 +38,7 @@ import com.google.cloud.spanner.IntegrationTest; import com.google.cloud.spanner.KeySet; import com.google.cloud.spanner.Options; +import com.google.cloud.spanner.ResultSet; import com.google.cloud.spanner.SessionPoolOptions; import com.google.cloud.spanner.Spanner; import com.google.cloud.spanner.SpannerException; @@ -184,11 +185,15 @@ public void deniedGetDatabase() { @Test public void deniedRead() { + // Getting a session and starting a read is non-blocking and will not cause an exception. Trying + // to get results from the result set will. + ResultSet rs = + databaseClient + .singleUse() + .read("nonexistent-table", KeySet.all(), Arrays.asList("nonexistent-col")); try { // Tests that the initial create session request returns a permission denied. - databaseClient - .singleUse() - .read("nonexistent-table", KeySet.all(), Arrays.asList("nonexistent-col")); + rs.next(); fail("Expected PERMISSION_DENIED SpannerException"); } catch (SpannerException e) { checkExceptionForVPCError(e); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITWriteTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITWriteTest.java index b41a998c65..d7a007eaf6 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITWriteTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITWriteTest.java @@ -18,6 +18,7 @@ import static com.google.cloud.spanner.SpannerMatchers.isSpannerException; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.fail; import com.google.cloud.ByteArray; @@ -49,10 +50,8 @@ import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Ignore; -import org.junit.Rule; import org.junit.Test; import org.junit.experimental.categories.Category; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -96,7 +95,6 @@ private static String uniqueString() { return String.format("k%04d", seq++); } - @Rule public ExpectedException expectedException = ExpectedException.none(); private String lastKey; private Timestamp write(Mutation m) { @@ -163,8 +161,12 @@ public void writeAlreadyExists() { @Ignore // TODO(user): Fix this - backend currently accepts empty mutation. @Test public void emptyWrite() { - expectedException.expect(isSpannerException(ErrorCode.INVALID_ARGUMENT)); - client.write(Arrays.asList()); + try { + client.write(Arrays.asList()); + fail("Expected exception"); + } catch (SpannerException ex) { + assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + } } @Test @@ -373,9 +375,12 @@ public void writeBoolArray() { Struct row = readLastRow("BoolArrayValue"); assertThat(row.isNull(0)).isFalse(); assertThat(row.getBooleanList(0)).containsExactly(true, null, false).inOrder(); - - expectedException.expect(NullPointerException.class); - row.getBooleanArray(0); + try { + row.getBooleanArray(0); + fail("Expected exception"); + } catch (NullPointerException ex) { + assertNotNull(ex.getMessage()); + } } @Test @@ -407,9 +412,12 @@ public void writeInt64Array() { Struct row = readLastRow("Int64ArrayValue"); assertThat(row.isNull(0)).isFalse(); assertThat(row.getLongList(0)).containsExactly(1L, 2L, null).inOrder(); - - expectedException.expect(NullPointerException.class); - row.getLongArray(0); + try { + row.getLongArray(0); + fail("Expected exception"); + } catch (NullPointerException ex) { + assertNotNull(ex.getMessage()); + } } @Test @@ -445,9 +453,12 @@ public void writeFloat64Array() { Struct row = readLastRow("Float64ArrayValue"); assertThat(row.isNull(0)).isFalse(); assertThat(row.getDoubleList(0)).containsExactly(null, 1.0, 2.0).inOrder(); - - expectedException.expect(NullPointerException.class); - row.getDoubleArray(0); + try { + row.getDoubleArray(0); + fail("Expected exception"); + } catch (NullPointerException ex) { + assertNotNull(ex.getMessage()); + } } @Test @@ -570,29 +581,39 @@ public void writeDateArray() { public void tableNotFound() { // TODO(user): More precise matchers! Customer code needs to discern table not found, column // not found, etc. - expectedException.expect(isSpannerException(ErrorCode.NOT_FOUND)); - write( - Mutation.newInsertBuilder("TableThatDoesNotExist") - .set("K") - .to(uniqueString()) - .set("StringuniqueString(Value") - .to("V1") - .build()); + try { + write( + Mutation.newInsertBuilder("TableThatDoesNotExist") + .set("K") + .to(uniqueString()) + .set("StringuniqueString(Value") + .to("V1") + .build()); + fail("Expected exception"); + } catch (SpannerException ex) { + assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.NOT_FOUND); + } } @Test public void columnNotFound() { - expectedException.expect(isSpannerException(ErrorCode.NOT_FOUND)); - write(baseInsert().set("ColumnThatDoesNotExist").to("V1").build()); + try { + write(baseInsert().set("ColumnThatDoesNotExist").to("V1").build()); + fail("Expected exception"); + } catch (SpannerException ex) { + assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.NOT_FOUND); + } } @Test public void incorrectType() { - expectedException.expect(isSpannerException(ErrorCode.FAILED_PRECONDITION)); - // Attempt to set 'V' to INT64, not STRING. - // NOTE: an interest effect of not sending type metadata is that BYTES and INT64 are accepted - // here... - write(baseInsert().set("StringValue").to(1.234).build()); + try { + write(baseInsert().set("StringValue").to(1.234).build()); + fail("Expected exception"); + } catch (SpannerException ex) { + assertThat(ex.getErrorCode()).isEqualTo(ErrorCode.FAILED_PRECONDITION); + assertThat(ex.getMessage()).contains("STRING"); + } } @Test diff --git a/grpc-google-cloud-spanner-admin-database-v1/pom.xml b/grpc-google-cloud-spanner-admin-database-v1/pom.xml index 6d197be081..4c6378e2b0 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 - 1.55.2-SNAPSHOT + 1.58.1-SNAPSHOT grpc-google-cloud-spanner-admin-database-v1 GRPC library for grpc-google-cloud-spanner-admin-database-v1 com.google.cloud google-cloud-spanner-parent - 1.55.2-SNAPSHOT + 1.58.1-SNAPSHOT @@ -73,7 +73,7 @@ org.apache.maven.plugins maven-dependency-plugin - com.google.auto.value:auto-value-annotations + com.google.auto.value:auto-value-annotations,javax.annotation:javax.annotation-api diff --git a/grpc-google-cloud-spanner-admin-instance-v1/pom.xml b/grpc-google-cloud-spanner-admin-instance-v1/pom.xml index 60b6118f53..2b4f72478d 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 - 1.55.2-SNAPSHOT + 1.58.1-SNAPSHOT grpc-google-cloud-spanner-admin-instance-v1 GRPC library for grpc-google-cloud-spanner-admin-instance-v1 com.google.cloud google-cloud-spanner-parent - 1.55.2-SNAPSHOT + 1.58.1-SNAPSHOT @@ -73,7 +73,7 @@ org.apache.maven.plugins maven-dependency-plugin - com.google.auto.value:auto-value-annotations + com.google.auto.value:auto-value-annotations,javax.annotation:javax.annotation-api diff --git a/grpc-google-cloud-spanner-v1/pom.xml b/grpc-google-cloud-spanner-v1/pom.xml index ac288a3444..3e6e1a0642 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 - 1.55.2-SNAPSHOT + 1.58.1-SNAPSHOT grpc-google-cloud-spanner-v1 GRPC library for grpc-google-cloud-spanner-v1 com.google.cloud google-cloud-spanner-parent - 1.55.2-SNAPSHOT + 1.58.1-SNAPSHOT @@ -65,7 +65,7 @@ org.apache.maven.plugins maven-dependency-plugin - com.google.auto.value:auto-value-annotations + com.google.auto.value:auto-value-annotations,javax.annotation:javax.annotation-api diff --git a/pom.xml b/pom.xml index 0e2274df67..4cb4ec4544 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.google.cloud google-cloud-spanner-parent pom - 1.55.2-SNAPSHOT + 1.58.1-SNAPSHOT Google Cloud Spanner Parent https://github.com/googleapis/java-spanner @@ -14,7 +14,7 @@ com.google.cloud google-cloud-shared-config - 0.8.0 + 0.9.2 @@ -70,46 +70,59 @@ com.google.api.grpc proto-google-cloud-spanner-admin-instance-v1 - 1.55.2-SNAPSHOT + 1.58.1-SNAPSHOT com.google.api.grpc proto-google-cloud-spanner-v1 - 1.55.2-SNAPSHOT + 1.58.1-SNAPSHOT com.google.api.grpc proto-google-cloud-spanner-admin-database-v1 - 1.55.2-SNAPSHOT + 1.58.1-SNAPSHOT com.google.api.grpc grpc-google-cloud-spanner-v1 - 1.55.2-SNAPSHOT + 1.58.1-SNAPSHOT com.google.api.grpc grpc-google-cloud-spanner-admin-instance-v1 - 1.55.2-SNAPSHOT + 1.58.1-SNAPSHOT com.google.api.grpc grpc-google-cloud-spanner-admin-database-v1 - 1.55.2-SNAPSHOT + 1.58.1-SNAPSHOT com.google.cloud google-cloud-spanner - 1.55.2-SNAPSHOT + 1.58.1-SNAPSHOT com.google.cloud google-cloud-shared-dependencies - 0.4.0 + 0.8.3 pom import + + + junit + junit + 4.13 + test + + + com.google.truth + truth + 1.0.1 + test + @@ -187,7 +200,7 @@ https://developers.google.com/protocol-buffers/docs/reference/java/ https://googleapis.dev/java/google-auth-library/latest/ https://googleapis.dev/java/gax/latest/ - https://googleapis.github.io/api-common-java/1.8.1/apidocs/ + https://googleapis.github.io/api-common-java/ diff --git a/proto-google-cloud-spanner-admin-database-v1/pom.xml b/proto-google-cloud-spanner-admin-database-v1/pom.xml index 4a89149a26..cbf704fe66 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 - 1.55.2-SNAPSHOT + 1.58.1-SNAPSHOT proto-google-cloud-spanner-admin-database-v1 PROTO library for proto-google-cloud-spanner-admin-database-v1 com.google.cloud google-cloud-spanner-parent - 1.55.2-SNAPSHOT + 1.58.1-SNAPSHOT diff --git a/proto-google-cloud-spanner-admin-database-v1/src/main/java/com/google/spanner/admin/database/v1/BackupProto.java b/proto-google-cloud-spanner-admin-database-v1/src/main/java/com/google/spanner/admin/database/v1/BackupProto.java index 330156f140..2aaecf7bcd 100644 --- a/proto-google-cloud-spanner-admin-database-v1/src/main/java/com/google/spanner/admin/database/v1/BackupProto.java +++ b/proto-google-cloud-spanner-admin-database-v1/src/main/java/com/google/spanner/admin/database/v1/BackupProto.java @@ -130,12 +130,14 @@ public static com.google.protobuf.Descriptors.FileDescriptor getDescriptor() { + ".Operation\022\027\n\017next_page_token\030\002 \001(\t\"f\n\nB" + "ackupInfo\022\016\n\006backup\030\001 \001(\t\022/\n\013create_time" + "\030\002 \001(\0132\032.google.protobuf.Timestamp\022\027\n\017so" - + "urce_database\030\003 \001(\tB\321\001\n$com.google.spann" + + "urce_database\030\003 \001(\tB\377\001\n$com.google.spann" + "er.admin.database.v1B\013BackupProtoP\001ZHgoo" + "gle.golang.org/genproto/googleapis/spann" + "er/admin/database/v1;database\252\002&Google.C" + "loud.Spanner.Admin.Database.V1\312\002&Google\\" - + "Cloud\\Spanner\\Admin\\Database\\V1b\006proto3" + + "Cloud\\Spanner\\Admin\\Database\\V1\352\002+Google" + + "::Cloud::Spanner::Admin::Database::V1b\006p" + + "roto3" }; descriptor = com.google.protobuf.Descriptors.FileDescriptor.internalBuildGeneratedFileFrom( diff --git a/proto-google-cloud-spanner-admin-database-v1/src/main/java/com/google/spanner/admin/database/v1/CommonProto.java b/proto-google-cloud-spanner-admin-database-v1/src/main/java/com/google/spanner/admin/database/v1/CommonProto.java index 0ea75df21d..657d73efc9 100644 --- a/proto-google-cloud-spanner-admin-database-v1/src/main/java/com/google/spanner/admin/database/v1/CommonProto.java +++ b/proto-google-cloud-spanner-admin-database-v1/src/main/java/com/google/spanner/admin/database/v1/CommonProto.java @@ -48,12 +48,13 @@ public static com.google.protobuf.Descriptors.FileDescriptor getDescriptor() { + "\030\n\020progress_percent\030\001 \001(\005\022.\n\nstart_time\030" + "\002 \001(\0132\032.google.protobuf.Timestamp\022,\n\010end" + "_time\030\003 \001(\0132\032.google.protobuf.TimestampB" - + "\321\001\n$com.google.spanner.admin.database.v1" + + "\377\001\n$com.google.spanner.admin.database.v1" + "B\013CommonProtoP\001ZHgoogle.golang.org/genpr" + "oto/googleapis/spanner/admin/database/v1" + ";database\252\002&Google.Cloud.Spanner.Admin.D" + "atabase.V1\312\002&Google\\Cloud\\Spanner\\Admin\\" - + "Database\\V1b\006proto3" + + "Database\\V1\352\002+Google::Cloud::Spanner::Ad" + + "min::Database::V1b\006proto3" }; descriptor = com.google.protobuf.Descriptors.FileDescriptor.internalBuildGeneratedFileFrom( diff --git a/proto-google-cloud-spanner-admin-database-v1/src/main/java/com/google/spanner/admin/database/v1/SpannerDatabaseAdminProto.java b/proto-google-cloud-spanner-admin-database-v1/src/main/java/com/google/spanner/admin/database/v1/SpannerDatabaseAdminProto.java index 910a1a9c13..9af8119d2c 100644 --- a/proto-google-cloud-spanner-admin-database-v1/src/main/java/com/google/spanner/admin/database/v1/SpannerDatabaseAdminProto.java +++ b/proto-google-cloud-spanner-admin-database-v1/src/main/java/com/google/spanner/admin/database/v1/SpannerDatabaseAdminProto.java @@ -180,7 +180,7 @@ public static com.google.protobuf.Descriptors.FileDescriptor getDescriptor() { + "a\022\014\n\004name\030\001 \001(\t\022E\n\010progress\030\002 \001(\01323.goog" + "le.spanner.admin.database.v1.OperationPr" + "ogress*5\n\021RestoreSourceType\022\024\n\020TYPE_UNSP" - + "ECIFIED\020\000\022\n\n\006BACKUP\020\0012\362\036\n\rDatabaseAdmin\022" + + "ECIFIED\020\000\022\n\n\006BACKUP\020\0012\223\037\n\rDatabaseAdmin\022" + "\300\001\n\rListDatabases\0226.google.spanner.admin" + ".database.v1.ListDatabasesRequest\0327.goog" + "le.spanner.admin.database.v1.ListDatabas" @@ -233,61 +233,63 @@ public static com.google.protobuf.Descriptors.FileDescriptor getDescriptor() { + "es/*}:testIamPermissions:\001*ZG\"B/v1/{reso" + "urce=projects/*/instances/*/backups/*}:t" + "estIamPermissions:\001*\332A\024resource,permissi" - + "ons\022\376\001\n\014CreateBackup\0225.google.spanner.ad" + + "ons\022\237\002\n\014CreateBackup\0225.google.spanner.ad" + "min.database.v1.CreateBackupRequest\032\035.go" - + "ogle.longrunning.Operation\"\227\001\202\323\344\223\0025\"+/v1" + + "ogle.longrunning.Operation\"\270\001\202\323\344\223\0025\"+/v1" + "/{parent=projects/*/instances/*}/backups" - + ":\006backup\332A\027parent,backup,backup_id\312A?\n\006B" - + "ackup\0225google.spanner.admin.database.v1." - + "CreateBackupMetadata\022\245\001\n\tGetBackup\0222.goo" - + "gle.spanner.admin.database.v1.GetBackupR" - + "equest\032(.google.spanner.admin.database.v" - + "1.Backup\":\202\323\344\223\002-\022+/v1/{name=projects/*/i" - + "nstances/*/backups/*}\332A\004name\022\310\001\n\014UpdateB" - + "ackup\0225.google.spanner.admin.database.v1" - + ".UpdateBackupRequest\032(.google.spanner.ad" - + "min.database.v1.Backup\"W\202\323\344\223\002<22/v1/{bac" - + "kup.name=projects/*/instances/*/backups/" - + "*}:\006backup\332A\022backup,update_mask\022\231\001\n\014Dele" - + "teBackup\0225.google.spanner.admin.database" - + ".v1.DeleteBackupRequest\032\026.google.protobu" - + "f.Empty\":\202\323\344\223\002-*+/v1/{name=projects/*/in" - + "stances/*/backups/*}\332A\004name\022\270\001\n\013ListBack" - + "ups\0224.google.spanner.admin.database.v1.L" - + "istBackupsRequest\0325.google.spanner.admin" - + ".database.v1.ListBackupsResponse\"<\202\323\344\223\002-" - + "\022+/v1/{parent=projects/*/instances/*}/ba" - + "ckups\332A\006parent\022\261\002\n\017RestoreDatabase\0228.goo" - + "gle.spanner.admin.database.v1.RestoreDat" - + "abaseRequest\032\035.google.longrunning.Operat" - + "ion\"\304\001\202\323\344\223\002:\"5/v1/{parent=projects/*/ins" - + "tances/*}/databases:restore:\001*\332A\031parent," - + "database_id,backup\312Ae\n)google.spanner.ad" - + "min.database.v1.Database\0228google.spanner" - + ".admin.database.v1.RestoreDatabaseMetada" - + "ta\022\344\001\n\026ListDatabaseOperations\022?.google.s" - + "panner.admin.database.v1.ListDatabaseOpe" - + "rationsRequest\032@.google.spanner.admin.da" - + "tabase.v1.ListDatabaseOperationsResponse" - + "\"G\202\323\344\223\0028\0226/v1/{parent=projects/*/instanc" - + "es/*}/databaseOperations\332A\006parent\022\334\001\n\024Li" - + "stBackupOperations\022=.google.spanner.admi" - + "n.database.v1.ListBackupOperationsReques" - + "t\032>.google.spanner.admin.database.v1.Lis" - + "tBackupOperationsResponse\"E\202\323\344\223\0026\0224/v1/{" - + "parent=projects/*/instances/*}/backupOpe" - + "rations\332A\006parent\032x\312A\026spanner.googleapis." - + "com\322A\\https://www.googleapis.com/auth/cl" - + "oud-platform,https://www.googleapis.com/" - + "auth/spanner.adminB\254\002\n$com.google.spanne" - + "r.admin.database.v1B\031SpannerDatabaseAdmi" - + "nProtoP\001ZHgoogle.golang.org/genproto/goo" - + "gleapis/spanner/admin/database/v1;databa" - + "se\252\002&Google.Cloud.Spanner.Admin.Database" - + ".V1\312\002&Google\\Cloud\\Spanner\\Admin\\Databas" - + "e\\V1\352AJ\n\037spanner.googleapis.com/Instance" - + "\022\'projects/{project}/instances/{instance" - + "}b\006proto3" + + ":\006backup\332A\027parent,backup,backup_id\312A`\n\'g" + + "oogle.spanner.admin.database.v1.Backup\0225" + + "google.spanner.admin.database.v1.CreateB" + + "ackupMetadata\022\245\001\n\tGetBackup\0222.google.spa" + + "nner.admin.database.v1.GetBackupRequest\032" + + "(.google.spanner.admin.database.v1.Backu" + + "p\":\202\323\344\223\002-\022+/v1/{name=projects/*/instance" + + "s/*/backups/*}\332A\004name\022\310\001\n\014UpdateBackup\0225" + + ".google.spanner.admin.database.v1.Update" + + "BackupRequest\032(.google.spanner.admin.dat" + + "abase.v1.Backup\"W\202\323\344\223\002<22/v1/{backup.nam" + + "e=projects/*/instances/*/backups/*}:\006bac" + + "kup\332A\022backup,update_mask\022\231\001\n\014DeleteBacku" + + "p\0225.google.spanner.admin.database.v1.Del" + + "eteBackupRequest\032\026.google.protobuf.Empty" + + "\":\202\323\344\223\002-*+/v1/{name=projects/*/instances" + + "/*/backups/*}\332A\004name\022\270\001\n\013ListBackups\0224.g" + + "oogle.spanner.admin.database.v1.ListBack" + + "upsRequest\0325.google.spanner.admin.databa" + + "se.v1.ListBackupsResponse\"<\202\323\344\223\002-\022+/v1/{" + + "parent=projects/*/instances/*}/backups\332A" + + "\006parent\022\261\002\n\017RestoreDatabase\0228.google.spa" + + "nner.admin.database.v1.RestoreDatabaseRe" + + "quest\032\035.google.longrunning.Operation\"\304\001\202" + + "\323\344\223\002:\"5/v1/{parent=projects/*/instances/" + + "*}/databases:restore:\001*\332A\031parent,databas" + + "e_id,backup\312Ae\n)google.spanner.admin.dat" + + "abase.v1.Database\0228google.spanner.admin." + + "database.v1.RestoreDatabaseMetadata\022\344\001\n\026" + + "ListDatabaseOperations\022?.google.spanner." + + "admin.database.v1.ListDatabaseOperations" + + "Request\032@.google.spanner.admin.database." + + "v1.ListDatabaseOperationsResponse\"G\202\323\344\223\002" + + "8\0226/v1/{parent=projects/*/instances/*}/d" + + "atabaseOperations\332A\006parent\022\334\001\n\024ListBacku" + + "pOperations\022=.google.spanner.admin.datab" + + "ase.v1.ListBackupOperationsRequest\032>.goo" + + "gle.spanner.admin.database.v1.ListBackup" + + "OperationsResponse\"E\202\323\344\223\0026\0224/v1/{parent=" + + "projects/*/instances/*}/backupOperations" + + "\332A\006parent\032x\312A\026spanner.googleapis.com\322A\\h" + + "ttps://www.googleapis.com/auth/cloud-pla" + + "tform,https://www.googleapis.com/auth/sp" + + "anner.adminB\332\002\n$com.google.spanner.admin" + + ".database.v1B\031SpannerDatabaseAdminProtoP" + + "\001ZHgoogle.golang.org/genproto/googleapis" + + "/spanner/admin/database/v1;database\252\002&Go" + + "ogle.Cloud.Spanner.Admin.Database.V1\312\002&G" + + "oogle\\Cloud\\Spanner\\Admin\\Database\\V1\352\002+" + + "Google::Cloud::Spanner::Admin::Database:" + + ":V1\352AJ\n\037spanner.googleapis.com/Instance\022" + + "\'projects/{project}/instances/{instance}" + + "b\006proto3" }; descriptor = com.google.protobuf.Descriptors.FileDescriptor.internalBuildGeneratedFileFrom( diff --git a/proto-google-cloud-spanner-admin-database-v1/src/main/proto/google/spanner/admin/database/v1/backup.proto b/proto-google-cloud-spanner-admin-database-v1/src/main/proto/google/spanner/admin/database/v1/backup.proto index b883adf34c..e33faddddf 100644 --- a/proto-google-cloud-spanner-admin-database-v1/src/main/proto/google/spanner/admin/database/v1/backup.proto +++ b/proto-google-cloud-spanner-admin-database-v1/src/main/proto/google/spanner/admin/database/v1/backup.proto @@ -30,6 +30,7 @@ option java_multiple_files = true; option java_outer_classname = "BackupProto"; option java_package = "com.google.spanner.admin.database.v1"; option php_namespace = "Google\\Cloud\\Spanner\\Admin\\Database\\V1"; +option ruby_package = "Google::Cloud::Spanner::Admin::Database::V1"; // A backup of a Cloud Spanner database. message Backup { diff --git a/proto-google-cloud-spanner-admin-database-v1/src/main/proto/google/spanner/admin/database/v1/common.proto b/proto-google-cloud-spanner-admin-database-v1/src/main/proto/google/spanner/admin/database/v1/common.proto index 4914cb8ac7..27ecb0a98b 100644 --- a/proto-google-cloud-spanner-admin-database-v1/src/main/proto/google/spanner/admin/database/v1/common.proto +++ b/proto-google-cloud-spanner-admin-database-v1/src/main/proto/google/spanner/admin/database/v1/common.proto @@ -26,6 +26,7 @@ option java_multiple_files = true; option java_outer_classname = "CommonProto"; option java_package = "com.google.spanner.admin.database.v1"; option php_namespace = "Google\\Cloud\\Spanner\\Admin\\Database\\V1"; +option ruby_package = "Google::Cloud::Spanner::Admin::Database::V1"; // Encapsulates progress related information for a Cloud Spanner long // running operation. diff --git a/proto-google-cloud-spanner-admin-database-v1/src/main/proto/google/spanner/admin/database/v1/spanner_database_admin.proto b/proto-google-cloud-spanner-admin-database-v1/src/main/proto/google/spanner/admin/database/v1/spanner_database_admin.proto index d48adc8aba..af440c1a36 100644 --- a/proto-google-cloud-spanner-admin-database-v1/src/main/proto/google/spanner/admin/database/v1/spanner_database_admin.proto +++ b/proto-google-cloud-spanner-admin-database-v1/src/main/proto/google/spanner/admin/database/v1/spanner_database_admin.proto @@ -34,6 +34,7 @@ option java_multiple_files = true; option java_outer_classname = "SpannerDatabaseAdminProto"; option java_package = "com.google.spanner.admin.database.v1"; option php_namespace = "Google\\Cloud\\Spanner\\Admin\\Database\\V1"; +option ruby_package = "Google::Cloud::Spanner::Admin::Database::V1"; option (google.api.resource_definition) = { type: "spanner.googleapis.com/Instance" pattern: "projects/{project}/instances/{instance}" @@ -206,7 +207,7 @@ service DatabaseAdmin { }; option (google.api.method_signature) = "parent,backup,backup_id"; option (google.longrunning.operation_info) = { - response_type: "Backup" + response_type: "google.spanner.admin.database.v1.Backup" metadata_type: "google.spanner.admin.database.v1.CreateBackupMetadata" }; } diff --git a/proto-google-cloud-spanner-admin-instance-v1/pom.xml b/proto-google-cloud-spanner-admin-instance-v1/pom.xml index 40fa0553c8..6b5568b5ec 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 - 1.55.2-SNAPSHOT + 1.58.1-SNAPSHOT proto-google-cloud-spanner-admin-instance-v1 PROTO library for proto-google-cloud-spanner-admin-instance-v1 com.google.cloud google-cloud-spanner-parent - 1.55.2-SNAPSHOT + 1.58.1-SNAPSHOT diff --git a/proto-google-cloud-spanner-admin-instance-v1/src/main/java/com/google/spanner/admin/instance/v1/SpannerInstanceAdminProto.java b/proto-google-cloud-spanner-admin-instance-v1/src/main/java/com/google/spanner/admin/instance/v1/SpannerInstanceAdminProto.java index 62e2a4dca1..64fee15512 100644 --- a/proto-google-cloud-spanner-admin-instance-v1/src/main/java/com/google/spanner/admin/instance/v1/SpannerInstanceAdminProto.java +++ b/proto-google-cloud-spanner-admin-instance-v1/src/main/java/com/google/spanner/admin/instance/v1/SpannerInstanceAdminProto.java @@ -222,13 +222,14 @@ public static com.google.protobuf.Descriptors.FileDescriptor getDescriptor() { + "ce,permissions\032x\312A\026spanner.googleapis.co" + "m\322A\\https://www.googleapis.com/auth/clou" + "d-platform,https://www.googleapis.com/au" - + "th/spanner.adminB\337\001\n$com.google.spanner." + + "th/spanner.adminB\215\002\n$com.google.spanner." + "admin.instance.v1B\031SpannerInstanceAdminP" + "rotoP\001ZHgoogle.golang.org/genproto/googl" + "eapis/spanner/admin/instance/v1;instance" + "\252\002&Google.Cloud.Spanner.Admin.Instance.V" + "1\312\002&Google\\Cloud\\Spanner\\Admin\\Instance\\" - + "V1b\006proto3" + + "V1\352\002+Google::Cloud::Spanner::Admin::Inst" + + "ance::V1b\006proto3" }; descriptor = com.google.protobuf.Descriptors.FileDescriptor.internalBuildGeneratedFileFrom( diff --git a/proto-google-cloud-spanner-admin-instance-v1/src/main/proto/google/spanner/admin/instance/v1/spanner_instance_admin.proto b/proto-google-cloud-spanner-admin-instance-v1/src/main/proto/google/spanner/admin/instance/v1/spanner_instance_admin.proto index 6a068baca2..54767bf263 100644 --- a/proto-google-cloud-spanner-admin-instance-v1/src/main/proto/google/spanner/admin/instance/v1/spanner_instance_admin.proto +++ b/proto-google-cloud-spanner-admin-instance-v1/src/main/proto/google/spanner/admin/instance/v1/spanner_instance_admin.proto @@ -33,6 +33,7 @@ option java_multiple_files = true; option java_outer_classname = "SpannerInstanceAdminProto"; option java_package = "com.google.spanner.admin.instance.v1"; option php_namespace = "Google\\Cloud\\Spanner\\Admin\\Instance\\V1"; +option ruby_package = "Google::Cloud::Spanner::Admin::Instance::V1"; // Cloud Spanner Instance Admin API // diff --git a/proto-google-cloud-spanner-v1/pom.xml b/proto-google-cloud-spanner-v1/pom.xml index 179428c2c5..58c8d47066 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 - 1.55.2-SNAPSHOT + 1.58.1-SNAPSHOT proto-google-cloud-spanner-v1 PROTO library for proto-google-cloud-spanner-v1 com.google.cloud google-cloud-spanner-parent - 1.55.2-SNAPSHOT + 1.58.1-SNAPSHOT diff --git a/proto-google-cloud-spanner-v1/src/main/java/com/google/spanner/v1/KeysProto.java b/proto-google-cloud-spanner-v1/src/main/java/com/google/spanner/v1/KeysProto.java index 8021353174..9c9b6318d1 100644 --- a/proto-google-cloud-spanner-v1/src/main/java/com/google/spanner/v1/KeysProto.java +++ b/proto-google-cloud-spanner-v1/src/main/java/com/google/spanner/v1/KeysProto.java @@ -55,11 +55,12 @@ public static com.google.protobuf.Descriptors.FileDescriptor getDescriptor() { + "H\001B\020\n\016start_key_typeB\016\n\014end_key_type\"l\n\006" + "KeySet\022(\n\004keys\030\001 \003(\0132\032.google.protobuf.L" + "istValue\022+\n\006ranges\030\002 \003(\0132\033.google.spanne" - + "r.v1.KeyRange\022\013\n\003all\030\003 \001(\010B\222\001\n\025com.googl" + + "r.v1.KeyRange\022\013\n\003all\030\003 \001(\010B\257\001\n\025com.googl" + "e.spanner.v1B\tKeysProtoP\001Z8google.golang" + ".org/genproto/googleapis/spanner/v1;span" + "ner\252\002\027Google.Cloud.Spanner.V1\312\002\027Google\\C" - + "loud\\Spanner\\V1b\006proto3" + + "loud\\Spanner\\V1\352\002\032Google::Cloud::Spanner" + + "::V1b\006proto3" }; descriptor = com.google.protobuf.Descriptors.FileDescriptor.internalBuildGeneratedFileFrom( diff --git a/proto-google-cloud-spanner-v1/src/main/java/com/google/spanner/v1/MutationProto.java b/proto-google-cloud-spanner-v1/src/main/java/com/google/spanner/v1/MutationProto.java index 8dd9e0420c..27ce2b4f3a 100644 --- a/proto-google-cloud-spanner-v1/src/main/java/com/google/spanner/v1/MutationProto.java +++ b/proto-google-cloud-spanner-v1/src/main/java/com/google/spanner/v1/MutationProto.java @@ -63,11 +63,11 @@ public static com.google.protobuf.Descriptors.FileDescriptor getDescriptor() { + "lues\030\003 \003(\0132\032.google.protobuf.ListValue\032C" + "\n\006Delete\022\r\n\005table\030\001 \001(\t\022*\n\007key_set\030\002 \001(\013" + "2\031.google.spanner.v1.KeySetB\013\n\toperation" - + "B\226\001\n\025com.google.spanner.v1B\rMutationProt" + + "B\263\001\n\025com.google.spanner.v1B\rMutationProt" + "oP\001Z8google.golang.org/genproto/googleap" + "is/spanner/v1;spanner\252\002\027Google.Cloud.Spa" - + "nner.V1\312\002\027Google\\Cloud\\Spanner\\V1b\006proto" - + "3" + + "nner.V1\312\002\027Google\\Cloud\\Spanner\\V1\352\002\032Goog" + + "le::Cloud::Spanner::V1b\006proto3" }; descriptor = com.google.protobuf.Descriptors.FileDescriptor.internalBuildGeneratedFileFrom( diff --git a/proto-google-cloud-spanner-v1/src/main/java/com/google/spanner/v1/QueryPlanProto.java b/proto-google-cloud-spanner-v1/src/main/java/com/google/spanner/v1/QueryPlanProto.java index 4276b984cc..03c05431d0 100644 --- a/proto-google-cloud-spanner-v1/src/main/java/com/google/spanner/v1/QueryPlanProto.java +++ b/proto-google-cloud-spanner-v1/src/main/java/com/google/spanner/v1/QueryPlanProto.java @@ -76,11 +76,12 @@ public static com.google.protobuf.Descriptors.FileDescriptor getDescriptor() { + "ue\030\002 \001(\005:\0028\001\"8\n\004Kind\022\024\n\020KIND_UNSPECIFIED" + "\020\000\022\016\n\nRELATIONAL\020\001\022\n\n\006SCALAR\020\002\"<\n\tQueryP" + "lan\022/\n\nplan_nodes\030\001 \003(\0132\033.google.spanner" - + ".v1.PlanNodeB\227\001\n\025com.google.spanner.v1B\016" + + ".v1.PlanNodeB\264\001\n\025com.google.spanner.v1B\016" + "QueryPlanProtoP\001Z8google.golang.org/genp" + "roto/googleapis/spanner/v1;spanner\252\002\027Goo" + "gle.Cloud.Spanner.V1\312\002\027Google\\Cloud\\Span" - + "ner\\V1b\006proto3" + + "ner\\V1\352\002\032Google::Cloud::Spanner::V1b\006pro" + + "to3" }; descriptor = com.google.protobuf.Descriptors.FileDescriptor.internalBuildGeneratedFileFrom( diff --git a/proto-google-cloud-spanner-v1/src/main/java/com/google/spanner/v1/ResultSetProto.java b/proto-google-cloud-spanner-v1/src/main/java/com/google/spanner/v1/ResultSetProto.java index 08e001c03b..48157af205 100644 --- a/proto-google-cloud-spanner-v1/src/main/java/com/google/spanner/v1/ResultSetProto.java +++ b/proto-google-cloud-spanner-v1/src/main/java/com/google/spanner/v1/ResultSetProto.java @@ -74,11 +74,12 @@ public static com.google.protobuf.Descriptors.FileDescriptor getDescriptor() { + "gle.spanner.v1.QueryPlan\022,\n\013query_stats\030" + "\002 \001(\0132\027.google.protobuf.Struct\022\031\n\017row_co" + "unt_exact\030\003 \001(\003H\000\022\037\n\025row_count_lower_bou" - + "nd\030\004 \001(\003H\000B\013\n\trow_countB\232\001\n\025com.google.s" + + "nd\030\004 \001(\003H\000B\013\n\trow_countB\267\001\n\025com.google.s" + "panner.v1B\016ResultSetProtoP\001Z8google.gola" + "ng.org/genproto/googleapis/spanner/v1;sp" + "anner\370\001\001\252\002\027Google.Cloud.Spanner.V1\312\002\027Goo" - + "gle\\Cloud\\Spanner\\V1b\006proto3" + + "gle\\Cloud\\Spanner\\V1\352\002\032Google::Cloud::Sp" + + "anner::V1b\006proto3" }; descriptor = com.google.protobuf.Descriptors.FileDescriptor.internalBuildGeneratedFileFrom( diff --git a/proto-google-cloud-spanner-v1/src/main/java/com/google/spanner/v1/SpannerProto.java b/proto-google-cloud-spanner-v1/src/main/java/com/google/spanner/v1/SpannerProto.java index eb528acbc1..0932ab5d52 100644 --- a/proto-google-cloud-spanner-v1/src/main/java/com/google/spanner/v1/SpannerProto.java +++ b/proto-google-cloud-spanner-v1/src/main/java/com/google/spanner/v1/SpannerProto.java @@ -332,13 +332,14 @@ public static com.google.protobuf.Descriptors.FileDescriptor getDescriptor() { + ":\001*\032w\312A\026spanner.googleapis.com\322A[https:/" + "/www.googleapis.com/auth/cloud-platform," + "https://www.googleapis.com/auth/spanner." - + "dataB\367\001\n\025com.google.spanner.v1B\014SpannerP" + + "dataB\224\002\n\025com.google.spanner.v1B\014SpannerP" + "rotoP\001Z8google.golang.org/genproto/googl" + "eapis/spanner/v1;spanner\252\002\027Google.Cloud." - + "Spanner.V1\312\002\027Google\\Cloud\\Spanner\\V1\352A_\n" - + "\037spanner.googleapis.com/Database\022 com.google.cloud google-cloud-spanner - 1.55.1 + 1.58.0 @@ -53,7 +53,7 @@ org.codehaus.mojo build-helper-maven-plugin - 3.1.0 + 3.2.0 add-snippets-source diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml index 4ac5ce4190..26e1b47cbe 100644 --- a/samples/snapshot/pom.xml +++ b/samples/snapshot/pom.xml @@ -28,7 +28,7 @@ com.google.cloud google-cloud-spanner - 1.55.2-SNAPSHOT + 1.58.1-SNAPSHOT @@ -52,7 +52,7 @@ org.codehaus.mojo build-helper-maven-plugin - 3.1.0 + 3.2.0 add-snippets-source diff --git a/samples/snippets/pom.xml b/samples/snippets/pom.xml index a585cbae96..9452eb4d39 100644 --- a/samples/snippets/pom.xml +++ b/samples/snippets/pom.xml @@ -30,7 +30,7 @@ com.google.cloud libraries-bom - 5.7.0 + 8.1.0 pom import diff --git a/synth.metadata b/synth.metadata index 92758d2ac0..745753aaf8 100644 --- a/synth.metadata +++ b/synth.metadata @@ -19,7 +19,7 @@ "git": { "name": "synthtool", "remote": "https://github.com/googleapis/synthtool.git", - "sha": "987270824bd26f6a8c716d5e2022057b8ae7b26e" + "sha": "4f2c9f752a94042472fc03c5bd9e06e89817d2bd" } } ], diff --git a/versions.txt b/versions.txt index a6a70950eb..117645dc8e 100644 --- a/versions.txt +++ b/versions.txt @@ -1,10 +1,10 @@ # Format: # module:released-version:current-version -proto-google-cloud-spanner-admin-instance-v1:1.55.1:1.55.2-SNAPSHOT -proto-google-cloud-spanner-v1:1.55.1:1.55.2-SNAPSHOT -proto-google-cloud-spanner-admin-database-v1:1.55.1:1.55.2-SNAPSHOT -grpc-google-cloud-spanner-v1:1.55.1:1.55.2-SNAPSHOT -grpc-google-cloud-spanner-admin-instance-v1:1.55.1:1.55.2-SNAPSHOT -grpc-google-cloud-spanner-admin-database-v1:1.55.1:1.55.2-SNAPSHOT -google-cloud-spanner:1.55.1:1.55.2-SNAPSHOT \ No newline at end of file +proto-google-cloud-spanner-admin-instance-v1:1.58.0:1.58.1-SNAPSHOT +proto-google-cloud-spanner-v1:1.58.0:1.58.1-SNAPSHOT +proto-google-cloud-spanner-admin-database-v1:1.58.0:1.58.1-SNAPSHOT +grpc-google-cloud-spanner-v1:1.58.0:1.58.1-SNAPSHOT +grpc-google-cloud-spanner-admin-instance-v1:1.58.0:1.58.1-SNAPSHOT +grpc-google-cloud-spanner-admin-database-v1:1.58.0:1.58.1-SNAPSHOT +google-cloud-spanner:1.58.0:1.58.1-SNAPSHOT \ No newline at end of file