diff --git a/.github/workflows/hermetic_library_generation.yaml b/.github/workflows/hermetic_library_generation.yaml index 9399ebef23..46b80edc1c 100644 --- a/.github/workflows/hermetic_library_generation.yaml +++ b/.github/workflows/hermetic_library_generation.yaml @@ -37,7 +37,7 @@ jobs: with: fetch-depth: 0 token: ${{ secrets.CLOUD_JAVA_BOT_TOKEN }} - - uses: googleapis/sdk-platform-java/.github/scripts@v2.47.0 + - uses: googleapis/sdk-platform-java/.github/scripts@v2.49.0 if: env.SHOULD_RUN == 'true' with: base_ref: ${{ github.base_ref }} diff --git a/.github/workflows/unmanaged_dependency_check.yaml b/.github/workflows/unmanaged_dependency_check.yaml index 67c14d6183..fcd6c3e06b 100644 --- a/.github/workflows/unmanaged_dependency_check.yaml +++ b/.github/workflows/unmanaged_dependency_check.yaml @@ -14,6 +14,6 @@ jobs: shell: bash run: .kokoro/build.sh - name: Unmanaged dependency check - uses: googleapis/sdk-platform-java/java-shared-dependencies/unmanaged-dependency-check@google-cloud-shared-dependencies/v3.37.0 + uses: googleapis/sdk-platform-java/java-shared-dependencies/unmanaged-dependency-check@google-cloud-shared-dependencies/v3.39.0 with: bom-path: google-cloud-bigtable-bom/pom.xml diff --git a/.kokoro/presubmit/graalvm-native-17.cfg b/.kokoro/presubmit/graalvm-native-17.cfg index e698054b2d..b3a8c1bddd 100644 --- a/.kokoro/presubmit/graalvm-native-17.cfg +++ b/.kokoro/presubmit/graalvm-native-17.cfg @@ -3,7 +3,7 @@ # Configure the docker image for kokoro-trampoline. env_vars: { key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-public-resources/graalvm_sdk_platform_b:3.37.0" + value: "gcr.io/cloud-devrel-public-resources/graalvm_sdk_platform_b:3.39.0" } env_vars: { diff --git a/.kokoro/presubmit/graalvm-native.cfg b/.kokoro/presubmit/graalvm-native.cfg index ca9e250966..1caa83bcf5 100644 --- a/.kokoro/presubmit/graalvm-native.cfg +++ b/.kokoro/presubmit/graalvm-native.cfg @@ -3,7 +3,7 @@ # Configure the docker image for kokoro-trampoline. env_vars: { key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-public-resources/graalvm_sdk_platform_a:3.37.0" + value: "gcr.io/cloud-devrel-public-resources/graalvm_sdk_platform_a:3.39.0" } env_vars: { diff --git a/CHANGELOG.md b/CHANGELOG.md index 45ee54f09e..e6b1fbdc35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## [2.46.0](https://github.com/googleapis/java-bigtable/compare/v2.45.1...v2.46.0) (2024-10-29) + + +### Features + +* Test proxy support SSL backend ([#2381](https://github.com/googleapis/java-bigtable/issues/2381)) ([3cbf4ab](https://github.com/googleapis/java-bigtable/commit/3cbf4abe79d61daba0704abfccfb5558b026e6b7)) + + +### Bug Fixes + +* Fix client blocking latency ([#2346](https://github.com/googleapis/java-bigtable/issues/2346)) ([3801961](https://github.com/googleapis/java-bigtable/commit/380196174fb9b8cd97beb79d4faf49b30561be7f)) +* Fix first response latencies ([#2382](https://github.com/googleapis/java-bigtable/issues/2382)) ([8b2953e](https://github.com/googleapis/java-bigtable/commit/8b2953ed9c69c23b3e0c5c35d0538dc83f9dad80)) + + +### Dependencies + +* Update sdk-platform-java dependencies ([#2384](https://github.com/googleapis/java-bigtable/issues/2384)) ([81d7215](https://github.com/googleapis/java-bigtable/commit/81d72150b60d29e4e2ac17c6cb1fbdc89be0e16e)) + ## [2.45.1](https://github.com/googleapis/java-bigtable/compare/v2.45.0...v2.45.1) (2024-10-14) diff --git a/README.md b/README.md index c2463cc12f..380b7b9930 100644 --- a/README.md +++ b/README.md @@ -49,20 +49,20 @@ If you are using Maven without the BOM, add this to your dependencies: If you are using Gradle 5.x or later, add this to your dependencies: ```Groovy -implementation platform('com.google.cloud:libraries-bom:26.48.0') +implementation platform('com.google.cloud:libraries-bom:26.49.0') implementation 'com.google.cloud:google-cloud-bigtable' ``` If you are using Gradle without BOM, add this to your dependencies: ```Groovy -implementation 'com.google.cloud:google-cloud-bigtable:2.45.1' +implementation 'com.google.cloud:google-cloud-bigtable:2.46.0' ``` If you are using SBT, add this to your dependencies: ```Scala -libraryDependencies += "com.google.cloud" % "google-cloud-bigtable" % "2.45.1" +libraryDependencies += "com.google.cloud" % "google-cloud-bigtable" % "2.46.0" ``` ## Authentication @@ -543,7 +543,7 @@ Java is a registered trademark of Oracle and/or its affiliates. [kokoro-badge-link-5]: http://storage.googleapis.com/cloud-devrel-public/java/badges/java-bigtable/java11.html [stability-image]: https://img.shields.io/badge/stability-stable-green [maven-version-image]: https://img.shields.io/maven-central/v/com.google.cloud/google-cloud-bigtable.svg -[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-bigtable/2.45.1 +[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-bigtable/2.46.0 [authentication]: https://github.com/googleapis/google-cloud-java#authentication [auth-scopes]: https://developers.google.com/identity/protocols/oauth2/scopes [predefined-iam-roles]: https://cloud.google.com/iam/docs/understanding-roles#predefined_roles diff --git a/generation_config.yaml b/generation_config.yaml index d3a36ac301..83a8a8a096 100644 --- a/generation_config.yaml +++ b/generation_config.yaml @@ -1,6 +1,6 @@ -gapic_generator_version: 2.47.0 -googleapis_commitish: de509e38d37a2a9d8b95e1ce78831189f4f3c0f4 -libraries_bom_version: 26.48.0 +gapic_generator_version: 2.49.0 +googleapis_commitish: ba8ea80f25d19bde8501cd51f314391f8d39bde8 +libraries_bom_version: 26.49.0 template_excludes: - .gitignore - .kokoro/presubmit/integration.cfg diff --git a/google-cloud-bigtable-bom/pom.xml b/google-cloud-bigtable-bom/pom.xml index 678fc4bb9b..c5fe60b4ab 100644 --- a/google-cloud-bigtable-bom/pom.xml +++ b/google-cloud-bigtable-bom/pom.xml @@ -3,12 +3,12 @@ 4.0.0 com.google.cloud google-cloud-bigtable-bom - 2.45.1 + 2.46.1-SNAPSHOT pom com.google.cloud sdk-platform-java-config - 3.37.0 + 3.39.0 @@ -63,37 +63,37 @@ com.google.cloud google-cloud-bigtable - 2.45.1 + 2.46.1-SNAPSHOT com.google.cloud google-cloud-bigtable-emulator - 0.182.1 + 0.183.1-SNAPSHOT com.google.cloud google-cloud-bigtable-emulator-core - 0.182.1 + 0.183.1-SNAPSHOT com.google.api.grpc grpc-google-cloud-bigtable-admin-v2 - 2.45.1 + 2.46.1-SNAPSHOT com.google.api.grpc grpc-google-cloud-bigtable-v2 - 2.45.1 + 2.46.1-SNAPSHOT com.google.api.grpc proto-google-cloud-bigtable-admin-v2 - 2.45.1 + 2.46.1-SNAPSHOT com.google.api.grpc proto-google-cloud-bigtable-v2 - 2.45.1 + 2.46.1-SNAPSHOT diff --git a/google-cloud-bigtable-deps-bom/pom.xml b/google-cloud-bigtable-deps-bom/pom.xml index 660269361c..be0e83e515 100644 --- a/google-cloud-bigtable-deps-bom/pom.xml +++ b/google-cloud-bigtable-deps-bom/pom.xml @@ -7,13 +7,13 @@ com.google.cloud sdk-platform-java-config - 3.37.0 + 3.39.0 com.google.cloud google-cloud-bigtable-deps-bom - 2.45.1 + 2.46.1-SNAPSHOT pom @@ -77,7 +77,7 @@ com.google.cloud gapic-libraries-bom - 1.46.0 + 1.47.0 pom import diff --git a/google-cloud-bigtable-emulator-core/pom.xml b/google-cloud-bigtable-emulator-core/pom.xml index 517b18d76f..700cfaf3b8 100644 --- a/google-cloud-bigtable-emulator-core/pom.xml +++ b/google-cloud-bigtable-emulator-core/pom.xml @@ -7,11 +7,11 @@ google-cloud-bigtable-parent com.google.cloud - 2.45.1 + 2.46.1-SNAPSHOT google-cloud-bigtable-emulator-core - 0.182.1 + 0.183.1-SNAPSHOT A Java wrapper for the Cloud Bigtable emulator. diff --git a/google-cloud-bigtable-emulator/pom.xml b/google-cloud-bigtable-emulator/pom.xml index 97aed04a3d..06583c3726 100644 --- a/google-cloud-bigtable-emulator/pom.xml +++ b/google-cloud-bigtable-emulator/pom.xml @@ -5,7 +5,7 @@ 4.0.0 google-cloud-bigtable-emulator - 0.182.1 + 0.183.1-SNAPSHOT Google Cloud Java - Bigtable Emulator https://github.com/googleapis/java-bigtable @@ -14,7 +14,7 @@ com.google.cloud google-cloud-bigtable-parent - 2.45.1 + 2.46.1-SNAPSHOT scm:git:git@github.com:googleapis/java-bigtable.git @@ -81,14 +81,14 @@ com.google.cloud google-cloud-bigtable-deps-bom - 2.45.1 + 2.46.1-SNAPSHOT pom import com.google.cloud google-cloud-bigtable-bom - 2.45.1 + 2.46.1-SNAPSHOT pom import @@ -99,7 +99,7 @@ com.google.cloud google-cloud-bigtable-emulator-core - 0.182.1 + 0.183.1-SNAPSHOT diff --git a/google-cloud-bigtable/pom.xml b/google-cloud-bigtable/pom.xml index 2e5b017159..9b09f63093 100644 --- a/google-cloud-bigtable/pom.xml +++ b/google-cloud-bigtable/pom.xml @@ -2,7 +2,7 @@ 4.0.0 google-cloud-bigtable - 2.45.1 + 2.46.1-SNAPSHOT jar Google Cloud Bigtable https://github.com/googleapis/java-bigtable @@ -12,11 +12,11 @@ com.google.cloud google-cloud-bigtable-parent - 2.45.1 + 2.46.1-SNAPSHOT - 2.45.1 + 2.46.1-SNAPSHOT google-cloud-bigtable @@ -52,14 +52,14 @@ com.google.cloud google-cloud-bigtable-deps-bom - 2.45.1 + 2.46.1-SNAPSHOT pom import com.google.cloud google-cloud-bigtable-bom - 2.45.1 + 2.46.1-SNAPSHOT pom import diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/Version.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/Version.java index 796d1234be..4ed97314ce 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/Version.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/Version.java @@ -20,6 +20,6 @@ @InternalApi("For internal use only") public final class Version { // {x-version-update-start:google-cloud-bigtable:current} - public static String VERSION = "2.45.1"; + public static String VERSION = "2.46.1-SNAPSHOT"; // {x-version-update-end} } diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableUnaryOperationCallable.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableUnaryOperationCallable.java new file mode 100644 index 0000000000..78d507665e --- /dev/null +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableUnaryOperationCallable.java @@ -0,0 +1,213 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.bigtable.data.v2.stub; + +import com.google.api.core.AbstractApiFuture; +import com.google.api.core.ApiFuture; +import com.google.api.gax.grpc.GrpcStatusCode; +import com.google.api.gax.rpc.ApiCallContext; +import com.google.api.gax.rpc.InternalException; +import com.google.api.gax.rpc.ResponseObserver; +import com.google.api.gax.rpc.ServerStreamingCallable; +import com.google.api.gax.rpc.StreamController; +import com.google.api.gax.rpc.UnaryCallable; +import com.google.api.gax.tracing.ApiTracerFactory; +import com.google.api.gax.tracing.SpanName; +import com.google.cloud.bigtable.data.v2.stub.metrics.BigtableTracer; +import com.google.common.base.Preconditions; +import com.google.common.util.concurrent.Futures; +import io.grpc.Status; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Helper to convert a fake {@link ServerStreamingCallable} (ie only up to 1 response) into a {@link + * UnaryCallable}. It is intended to be the outermost callable of a chain. + * + *

Responsibilities: + * + *

    + *
  • Operation level metrics + *
  • Configuring the default call context + *
  • Converting the result to a future + */ +class BigtableUnaryOperationCallable extends UnaryCallable { + private static final Logger LOGGER = + Logger.getLogger(BigtableUnaryOperationCallable.class.getName()); + Logger logger = LOGGER; + + private final ServerStreamingCallable inner; + private final ApiCallContext defaultCallContext; + private final ApiTracerFactory tracerFactory; + private final SpanName spanName; + private final boolean allowNoResponse; + + public BigtableUnaryOperationCallable( + ServerStreamingCallable inner, + ApiCallContext defaultCallContext, + ApiTracerFactory tracerFactory, + SpanName spanName, + boolean allowNoResponse) { + this.inner = inner; + this.defaultCallContext = defaultCallContext; + this.tracerFactory = tracerFactory; + this.spanName = spanName; + this.allowNoResponse = allowNoResponse; + } + + @Override + public ApiFuture futureCall(ReqT req, ApiCallContext apiCallContext) { + apiCallContext = defaultCallContext.merge(apiCallContext); + + BigtableTracer apiTracer = + (BigtableTracer) + tracerFactory.newTracer( + apiCallContext.getTracer(), spanName, ApiTracerFactory.OperationType.Unary); + + apiCallContext = apiCallContext.withTracer(apiTracer); + + UnaryFuture f = new UnaryFuture(apiTracer, allowNoResponse); + inner.call(req, f, apiCallContext); + return f; + } + + class UnaryFuture extends AbstractApiFuture implements ResponseObserver { + private final BigtableTracer tracer; + private final boolean allowNoResponse; + + private StreamController controller; + private final AtomicBoolean upstreamCancelled = new AtomicBoolean(); + + private UnaryFuture(BigtableTracer tracer, boolean allowNoResponse) { + this.tracer = Preconditions.checkNotNull(tracer, "tracer can't be null"); + this.allowNoResponse = allowNoResponse; + } + + @Override + public void onStart(StreamController controller) { + this.controller = controller; + controller.disableAutoInboundFlowControl(); + // Request 2 to detect protocol bugs + controller.request(2); + } + + /** + * Immediately cancel the future state and try to cancel the underlying operation. Will return + * false if the future is already resolved. + */ + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + if (super.cancel(mayInterruptIfRunning)) { + cancelUpstream(); + return true; + } + return false; + } + + private void cancelUpstream() { + if (upstreamCancelled.compareAndSet(false, true)) { + controller.cancel(); + } + } + + @Override + public void onResponse(RespT resp) { + tracer.responseReceived(); + + if (set(resp)) { + tracer.operationFinishEarly(); + return; + } + + // At this point we are guaranteed that the future has been resolved. However we need to check + // why. + // We know it's not because it was resolved with the current response. Moreover, since the + // future + // is resolved, our only means to flag the error is to log. + // So there are 3 possibilities: + // 1. user cancelled the future + // 2. this is an extra response and the previous one resolved the future + // 3. we got a response after the rpc failed (this should never happen and would be a bad bug) + + if (isCancelled()) { + return; + } + + try { + RespT prev = Futures.getDone(this); + String msg = + String.format( + "Received response after future is resolved for a %s unary operation. previous: %s, New response: %s", + spanName, prev, resp); + logger.log(Level.WARNING, msg); + } catch (ExecutionException e) { + // Should never happen + String msg = + String.format( + "Received response after future resolved as a failure for a %s unary operation. New response: %s", + spanName, resp); + logger.log(Level.WARNING, msg, e.getCause()); + } + + cancelUpstream(); + } + + @Override + public void onError(Throwable throwable) { + if (this.setException(throwable)) { + tracer.operationFailed(throwable); + } else if (isCancelled()) { + tracer.operationCancelled(); + } else { + // At this point the has been resolved, so we ignore the error + tracer.operationSucceeded(); + } + } + + @Override + public void onComplete() { + if (allowNoResponse && set(null)) { + tracer.operationSucceeded(); + return; + + // Under normal circumstances the future wouldve been resolved in onResponse or via + // set(null) if it expected for + // the rpc to not have a response. So if aren't done, the only reason is that we didn't get + // a response + // but were expecting one + } else if (!isDone()) { + String msg = spanName + " unary operation completed without a response message"; + InternalException e = + new InternalException(msg, null, GrpcStatusCode.of(Status.Code.INTERNAL), false); + + if (setException(e)) { + tracer.operationFailed(e); + return; + } + } + + // check cancellation race + if (isCancelled()) { + tracer.operationCancelled(); + return; + } + + tracer.operationSucceeded(); + } + } +} diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/CheckAndMutateRowCallable.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/CheckAndMutateRowCallable.java deleted file mode 100644 index 549e10f44b..0000000000 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/CheckAndMutateRowCallable.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2018 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.bigtable.data.v2.stub; - -import com.google.api.core.ApiFunction; -import com.google.api.core.ApiFuture; -import com.google.api.core.ApiFutures; -import com.google.api.gax.rpc.ApiCallContext; -import com.google.api.gax.rpc.UnaryCallable; -import com.google.bigtable.v2.CheckAndMutateRowRequest; -import com.google.bigtable.v2.CheckAndMutateRowResponse; -import com.google.cloud.bigtable.data.v2.internal.RequestContext; -import com.google.cloud.bigtable.data.v2.models.ConditionalRowMutation; -import com.google.common.util.concurrent.MoreExecutors; - -/** Simple wrapper for CheckAndMutateRow to wrap the request and response protobufs. */ -class CheckAndMutateRowCallable extends UnaryCallable { - private final UnaryCallable inner; - private final RequestContext requestContext; - - CheckAndMutateRowCallable( - UnaryCallable inner, - RequestContext requestContext) { - this.inner = inner; - this.requestContext = requestContext; - } - - @Override - public ApiFuture futureCall(ConditionalRowMutation request, ApiCallContext context) { - ApiFuture rawResponse = - inner.futureCall(request.toProto(requestContext), context); - - return ApiFutures.transform( - rawResponse, - new ApiFunction() { - @Override - public Boolean apply(CheckAndMutateRowResponse checkAndMutateRowResponse) { - return checkAndMutateRowResponse.getPredicateMatched(); - } - }, - MoreExecutors.directExecutor()); - } -} diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStub.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStub.java index 91c63c2b85..266041a543 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStub.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStub.java @@ -21,6 +21,8 @@ import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.INSTANCE_ID_KEY; import com.google.api.core.ApiFunction; +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutures; import com.google.api.core.BetaApi; import com.google.api.core.InternalApi; import com.google.api.gax.batching.Batcher; @@ -39,6 +41,9 @@ import com.google.api.gax.retrying.RetryAlgorithm; import com.google.api.gax.retrying.RetryingExecutorWithContext; import com.google.api.gax.retrying.ScheduledRetryingExecutor; +import com.google.api.gax.retrying.SimpleStreamResumptionStrategy; +import com.google.api.gax.retrying.StreamResumptionStrategy; +import com.google.api.gax.rpc.ApiCallContext; import com.google.api.gax.rpc.Callables; import com.google.api.gax.rpc.ClientContext; import com.google.api.gax.rpc.RequestParamsExtractor; @@ -55,22 +60,17 @@ import com.google.auth.Credentials; import com.google.auth.oauth2.ServiceAccountJwtAccessCredentials; import com.google.bigtable.v2.BigtableGrpc; -import com.google.bigtable.v2.CheckAndMutateRowRequest; import com.google.bigtable.v2.CheckAndMutateRowResponse; import com.google.bigtable.v2.ExecuteQueryRequest; import com.google.bigtable.v2.ExecuteQueryResponse; import com.google.bigtable.v2.GenerateInitialChangeStreamPartitionsRequest; import com.google.bigtable.v2.GenerateInitialChangeStreamPartitionsResponse; -import com.google.bigtable.v2.MutateRowRequest; -import com.google.bigtable.v2.MutateRowResponse; import com.google.bigtable.v2.MutateRowsRequest; import com.google.bigtable.v2.MutateRowsResponse; import com.google.bigtable.v2.PingAndWarmRequest; import com.google.bigtable.v2.PingAndWarmResponse; import com.google.bigtable.v2.ReadChangeStreamRequest; import com.google.bigtable.v2.ReadChangeStreamResponse; -import com.google.bigtable.v2.ReadModifyWriteRowRequest; -import com.google.bigtable.v2.ReadModifyWriteRowResponse; import com.google.bigtable.v2.ReadRowsRequest; import com.google.bigtable.v2.ReadRowsResponse; import com.google.bigtable.v2.RowRange; @@ -98,12 +98,14 @@ import com.google.cloud.bigtable.data.v2.models.RowMutation; import com.google.cloud.bigtable.data.v2.models.RowMutationEntry; import com.google.cloud.bigtable.data.v2.models.SampleRowKeysRequest; +import com.google.cloud.bigtable.data.v2.models.TableId; import com.google.cloud.bigtable.data.v2.models.TargetId; import com.google.cloud.bigtable.data.v2.models.sql.Statement; import com.google.cloud.bigtable.data.v2.stub.changestream.ChangeStreamRecordMergingCallable; import com.google.cloud.bigtable.data.v2.stub.changestream.GenerateInitialChangeStreamPartitionsUserCallable; import com.google.cloud.bigtable.data.v2.stub.changestream.ReadChangeStreamResumptionStrategy; import com.google.cloud.bigtable.data.v2.stub.changestream.ReadChangeStreamUserCallable; +import com.google.cloud.bigtable.data.v2.stub.metrics.BigtableTracer; import com.google.cloud.bigtable.data.v2.stub.metrics.BigtableTracerStreamingCallable; import com.google.cloud.bigtable.data.v2.stub.metrics.BigtableTracerUnaryCallable; import com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsTracerFactory; @@ -137,12 +139,15 @@ import com.google.cloud.bigtable.gaxx.retrying.ApiResultRetryAlgorithm; import com.google.cloud.bigtable.gaxx.retrying.RetryInfoRetryAlgorithm; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Functions; import com.google.common.base.MoreObjects; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.util.concurrent.MoreExecutors; import com.google.protobuf.ByteString; import io.grpc.ManagedChannelBuilder; +import io.grpc.MethodDescriptor; import io.opencensus.stats.Stats; import io.opencensus.stats.StatsRecorder; import io.opencensus.tags.TagKey; @@ -154,11 +159,13 @@ import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; +import java.time.Duration; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; +import java.util.function.Function; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nonnull; @@ -194,7 +201,7 @@ public class EnhancedBigtableStub implements AutoCloseable { private final ServerStreamingCallable readRowsCallable; private final UnaryCallable readRowCallable; private final UnaryCallable> bulkReadRowsCallable; - private final UnaryCallable> sampleRowKeysCallable; + @Deprecated private final UnaryCallable> sampleRowKeysCallable; private final UnaryCallable> sampleRowKeysCallableWithRequest; private final UnaryCallable mutateRowCallable; @@ -539,7 +546,12 @@ public ServerStreamingCallable createReadRowsCallable( new TracedServerStreamingCallable<>( readRowsUserCallable, clientContext.getTracerFactory(), span); - return traced.withDefaultCallContext(clientContext.getDefaultCallContext()); + return traced.withDefaultCallContext( + clientContext + .getDefaultCallContext() + .withOption( + BigtableTracer.OPERATION_TIMEOUT_KEY, + settings.readRowsSettings().getRetrySettings().getTotalTimeout())); } /** @@ -557,27 +569,63 @@ public ServerStreamingCallable createReadRowsCallable( *
*/ public UnaryCallable createReadRowCallable(RowAdapter rowAdapter) { - ServerStreamingCallable readRowsCallable = - createReadRowsBaseCallable( - ServerStreamingCallSettings.newBuilder() - .setRetryableCodes(settings.readRowSettings().getRetryableCodes()) - .setRetrySettings(settings.readRowSettings().getRetrySettings()) - .setIdleTimeout(settings.readRowSettings().getRetrySettings().getTotalTimeout()) - .build(), - rowAdapter); - - ReadRowsUserCallable readRowCallable = - new ReadRowsUserCallable<>(readRowsCallable, requestContext); - - ReadRowsFirstCallable firstRow = new ReadRowsFirstCallable<>(readRowCallable); - - UnaryCallable traced = - new TracedUnaryCallable<>( - firstRow, clientContext.getTracerFactory(), getSpanName("ReadRow")); - - return traced.withDefaultCallContext(clientContext.getDefaultCallContext()); + if (!settings.getEnableSkipTrailers()) { + ServerStreamingCallable readRowsCallable = + createReadRowsBaseCallable( + ServerStreamingCallSettings.newBuilder() + .setRetryableCodes(settings.readRowSettings().getRetryableCodes()) + .setRetrySettings(settings.readRowSettings().getRetrySettings()) + .setIdleTimeout(settings.readRowSettings().getRetrySettings().getTotalTimeout()) + .build(), + rowAdapter); + + ReadRowsUserCallable readRowCallable = + new ReadRowsUserCallable<>(readRowsCallable, requestContext); + ReadRowsFirstCallable firstRow = new ReadRowsFirstCallable<>(readRowCallable); + UnaryCallable traced = + new TracedUnaryCallable<>( + firstRow, clientContext.getTracerFactory(), getSpanName("ReadRow")); + return traced.withDefaultCallContext( + clientContext + .getDefaultCallContext() + .withOption( + BigtableTracer.OPERATION_TIMEOUT_KEY, + settings.readRowSettings().getRetrySettings().getTotalTimeout())); + } else { + ServerStreamingCallable readRowsCallable = + createReadRowsBaseCallable( + ServerStreamingCallSettings.newBuilder() + .setRetryableCodes(settings.readRowSettings().getRetryableCodes()) + .setRetrySettings(settings.readRowSettings().getRetrySettings()) + .setIdleTimeoutDuration(Duration.ZERO) + .setWaitTimeoutDuration(Duration.ZERO) + .build(), + rowAdapter, + new SimpleStreamResumptionStrategy<>()); + ServerStreamingCallable readRowCallable = + new TransformingServerStreamingCallable<>( + readRowsCallable, + (query) -> query.limit(1).toProto(requestContext), + Functions.identity()); + + return new BigtableUnaryOperationCallable<>( + readRowCallable, + clientContext + .getDefaultCallContext() + .withOption( + BigtableTracer.OPERATION_TIMEOUT_KEY, + settings.readRowSettings().getRetrySettings().getTotalTimeout()), + clientContext.getTracerFactory(), + getSpanName("ReadRow"), + /*allowNoResponses=*/ true); + } } + private ServerStreamingCallable createReadRowsBaseCallable( + ServerStreamingCallSettings readRowsSettings, RowAdapter rowAdapter) { + return createReadRowsBaseCallable( + readRowsSettings, rowAdapter, new ReadRowsResumptionStrategy(rowAdapter)); + } /** * Creates a callable chain to handle ReadRows RPCs. The chain will: * @@ -594,29 +642,17 @@ public UnaryCallable createReadRowCallable(RowAdapter *

NOTE: the caller is responsible for adding tracing & metrics. */ private ServerStreamingCallable createReadRowsBaseCallable( - ServerStreamingCallSettings readRowsSettings, RowAdapter rowAdapter) { - + ServerStreamingCallSettings readRowsSettings, + RowAdapter rowAdapter, + StreamResumptionStrategy resumptionStrategy) { ServerStreamingCallable base = GrpcRawCallableFactory.createServerStreamingCallable( GrpcCallSettings.newBuilder() .setMethodDescriptor(BigtableGrpc.getReadRowsMethod()) .setParamsExtractor( - new RequestParamsExtractor() { - @Override - public Map extract(ReadRowsRequest readRowsRequest) { - String tableName = readRowsRequest.getTableName(); - String authorizedViewName = readRowsRequest.getAuthorizedViewName(); - if (tableName.isEmpty()) { - tableName = - NameUtil.extractTableNameFromAuthorizedViewName(authorizedViewName); - } - return ImmutableMap.of( - "table_name", - tableName, - "app_profile_id", - readRowsRequest.getAppProfileId()); - } - }) + r -> + composeRequestParams( + r.getAppProfileId(), r.getTableName(), r.getAuthorizedViewName())) .build(), readRowsSettings.getRetryableCodes()); @@ -636,7 +672,7 @@ public Map extract(ReadRowsRequest readRowsRequest) { // ReadRowsRequest -> ReadRowsResponse callable). ServerStreamingCallSettings innerSettings = ServerStreamingCallSettings.newBuilder() - .setResumptionStrategy(new ReadRowsResumptionStrategy<>(rowAdapter)) + .setResumptionStrategy(resumptionStrategy) .setRetryableCodes(readRowsSettings.getRetryableCodes()) .setRetrySettings(readRowsSettings.getRetrySettings()) .setIdleTimeout(readRowsSettings.getIdleTimeout()) @@ -694,15 +730,49 @@ private UnaryCallable> createBulkReadRowsCallable( UnaryCallable> traced = new TracedUnaryCallable<>(tracedBatcher, clientContext.getTracerFactory(), span); - return traced.withDefaultCallContext(clientContext.getDefaultCallContext()); + return traced.withDefaultCallContext( + clientContext + .getDefaultCallContext() + .withOption( + BigtableTracer.OPERATION_TIMEOUT_KEY, + settings.bulkReadRowsSettings().getRetrySettings().getTotalTimeout())); } /** - * Helper function that should only be used by createSampleRowKeysCallable() and - * createSampleRowKeysWithRequestCallable(). + * Simple wrapper around {@link #createSampleRowKeysCallableWithRequest()} to provide backwards + * compatibility + * + * @deprecated + */ + @Deprecated + private UnaryCallable> createSampleRowKeysCallable() { + UnaryCallable> baseCallable = + createSampleRowKeysCallableWithRequest(); + return new UnaryCallable>() { + @Override + public ApiFuture> futureCall(String s, ApiCallContext apiCallContext) { + return baseCallable.futureCall(SampleRowKeysRequest.create(TableId.of(s)), apiCallContext); + } + }; + } + + /** + * Creates a callable chain to handle SampleRowKeys RPcs. The chain will: + * + *

    + *
  • Convert a {@link SampleRowKeysRequest} to a {@link + * com.google.bigtable.v2.SampleRowKeysRequest}. + *
  • Dispatch the request to the GAPIC's {@link BigtableStub#sampleRowKeysCallable()}. + *
  • Spool responses into a list. + *
  • Retry on failure. + *
  • Convert the responses into {@link KeyOffset}s. + *
  • Add tracing & metrics. + *
*/ - private UnaryCallable> - createSampleRowKeysBaseCallable() { + private UnaryCallable> + createSampleRowKeysCallableWithRequest() { + String methodName = "SampleRowKeys"; + ServerStreamingCallable base = GrpcRawCallableFactory.createServerStreamingCallable( @@ -711,25 +781,9 @@ private UnaryCallable> createBulkReadRowsCallable( newBuilder() .setMethodDescriptor(BigtableGrpc.getSampleRowKeysMethod()) .setParamsExtractor( - new RequestParamsExtractor() { - @Override - public Map extract( - com.google.bigtable.v2.SampleRowKeysRequest sampleRowKeysRequest) { - String tableName = sampleRowKeysRequest.getTableName(); - String authorizedViewName = - sampleRowKeysRequest.getAuthorizedViewName(); - if (tableName.isEmpty()) { - tableName = - NameUtil.extractTableNameFromAuthorizedViewName( - authorizedViewName); - } - return ImmutableMap.of( - "table_name", - tableName, - "app_profile_id", - sampleRowKeysRequest.getAppProfileId()); - } - }) + r -> + composeRequestParams( + r.getAppProfileId(), r.getTableName(), r.getAuthorizedViewName())) .build(), settings.sampleRowKeysSettings().getRetryableCodes()); @@ -745,51 +799,15 @@ public Map extract( UnaryCallable> retryable = withRetries(withBigtableTracer, settings.sampleRowKeysSettings()); - return retryable; - } - - /** - * Creates a callable chain to handle SampleRowKeys RPcs. The chain will: - * - *
    - *
  • Convert a table id to a {@link com.google.bigtable.v2.SampleRowKeysRequest}. - *
  • Dispatch the request to the GAPIC's {@link BigtableStub#sampleRowKeysCallable()}. - *
  • Spool responses into a list. - *
  • Retry on failure. - *
  • Convert the responses into {@link KeyOffset}s. - *
  • Add tracing & metrics. - *
- */ - private UnaryCallable> createSampleRowKeysCallable() { - String methodName = "SampleRowKeys"; - - UnaryCallable> - baseCallable = createSampleRowKeysBaseCallable(); return createUserFacingUnaryCallable( - methodName, new SampleRowKeysCallable(baseCallable, requestContext)); - } - - /** - * Creates a callable chain to handle SampleRowKeys RPcs. The chain will: - * - *
    - *
  • Convert a {@link SampleRowKeysRequest} to a {@link - * com.google.bigtable.v2.SampleRowKeysRequest}. - *
  • Dispatch the request to the GAPIC's {@link BigtableStub#sampleRowKeysCallable()}. - *
  • Spool responses into a list. - *
  • Retry on failure. - *
  • Convert the responses into {@link KeyOffset}s. - *
  • Add tracing & metrics. - *
- */ - private UnaryCallable> - createSampleRowKeysCallableWithRequest() { - String methodName = "SampleRowKeys"; - - UnaryCallable> - baseCallable = createSampleRowKeysBaseCallable(); - return createUserFacingUnaryCallable( - methodName, new SampleRowKeysCallableWithRequest(baseCallable, requestContext)); + methodName, + new SampleRowKeysCallableWithRequest(retryable, requestContext) + .withDefaultCallContext( + clientContext + .getDefaultCallContext() + .withOption( + BigtableTracer.OPERATION_TIMEOUT_KEY, + settings.sampleRowKeysSettings().getRetrySettings().getTotalTimeout()))); } /** @@ -801,42 +819,14 @@ private UnaryCallable> createSampleRowKeysCallable() { * */ private UnaryCallable createMutateRowCallable() { - String methodName = "MutateRow"; - UnaryCallable base = - GrpcRawCallableFactory.createUnaryCallable( - GrpcCallSettings.newBuilder() - .setMethodDescriptor(BigtableGrpc.getMutateRowMethod()) - .setParamsExtractor( - new RequestParamsExtractor() { - @Override - public Map extract(MutateRowRequest mutateRowRequest) { - String tableName = mutateRowRequest.getTableName(); - String authorizedViewName = mutateRowRequest.getAuthorizedViewName(); - if (tableName.isEmpty()) { - tableName = - NameUtil.extractTableNameFromAuthorizedViewName(authorizedViewName); - } - return ImmutableMap.of( - "table_name", - tableName, - "app_profile_id", - mutateRowRequest.getAppProfileId()); - } - }) - .build(), - settings.mutateRowSettings().getRetryableCodes()); - - UnaryCallable withStatsHeaders = - new StatsHeadersUnaryCallable<>(base); - - UnaryCallable withBigtableTracer = - new BigtableTracerUnaryCallable<>(withStatsHeaders); - - UnaryCallable retrying = - withRetries(withBigtableTracer, settings.mutateRowSettings()); - - return createUserFacingUnaryCallable( - methodName, new MutateRowCallable(retrying, requestContext)); + return createUnaryCallable( + BigtableGrpc.getMutateRowMethod(), + req -> + composeRequestParams( + req.getAppProfileId(), req.getTableName(), req.getAuthorizedViewName()), + settings.mutateRowSettings(), + req -> req.toProto(requestContext), + resp -> null); } /** @@ -863,22 +853,9 @@ private UnaryCallable createMutateRowsBas GrpcCallSettings.newBuilder() .setMethodDescriptor(BigtableGrpc.getMutateRowsMethod()) .setParamsExtractor( - new RequestParamsExtractor() { - @Override - public Map extract(MutateRowsRequest mutateRowsRequest) { - String tableName = mutateRowsRequest.getTableName(); - String authorizedViewName = mutateRowsRequest.getAuthorizedViewName(); - if (tableName.isEmpty()) { - tableName = - NameUtil.extractTableNameFromAuthorizedViewName(authorizedViewName); - } - return ImmutableMap.of( - "table_name", - tableName, - "app_profile_id", - mutateRowsRequest.getAppProfileId()); - } - }) + r -> + composeRequestParams( + r.getAppProfileId(), r.getTableName(), r.getAuthorizedViewName())) .build(), settings.bulkMutateRowsSettings().getRetryableCodes()); @@ -953,7 +930,12 @@ public Map extract(MutateRowsRequest mutateRowsRequest) { new TracedUnaryCallable<>( tracedBatcherUnaryCallable, clientContext.getTracerFactory(), spanName); - return traced.withDefaultCallContext(clientContext.getDefaultCallContext()); + return traced.withDefaultCallContext( + clientContext + .getDefaultCallContext() + .withOption( + BigtableTracer.OPERATION_TIMEOUT_KEY, + settings.bulkMutateRowsSettings().getRetrySettings().getTotalTimeout())); } /** @@ -1056,44 +1038,14 @@ public Batcher newBulkReadRowsBatcher( * */ private UnaryCallable createCheckAndMutateRowCallable() { - String methodName = "CheckAndMutateRow"; - UnaryCallable base = - GrpcRawCallableFactory.createUnaryCallable( - GrpcCallSettings.newBuilder() - .setMethodDescriptor(BigtableGrpc.getCheckAndMutateRowMethod()) - .setParamsExtractor( - new RequestParamsExtractor() { - @Override - public Map extract( - CheckAndMutateRowRequest checkAndMutateRowRequest) { - String tableName = checkAndMutateRowRequest.getTableName(); - String authorizedViewName = - checkAndMutateRowRequest.getAuthorizedViewName(); - if (tableName.isEmpty()) { - tableName = - NameUtil.extractTableNameFromAuthorizedViewName(authorizedViewName); - } - return ImmutableMap.of( - "table_name", - tableName, - "app_profile_id", - checkAndMutateRowRequest.getAppProfileId()); - } - }) - .build(), - settings.checkAndMutateRowSettings().getRetryableCodes()); - - UnaryCallable withStatsHeaders = - new StatsHeadersUnaryCallable<>(base); - - UnaryCallable withBigtableTracer = - new BigtableTracerUnaryCallable<>(withStatsHeaders); - - UnaryCallable retrying = - withRetries(withBigtableTracer, settings.checkAndMutateRowSettings()); - - return createUserFacingUnaryCallable( - methodName, new CheckAndMutateRowCallable(retrying, requestContext)); + return createUnaryCallable( + BigtableGrpc.getCheckAndMutateRowMethod(), + req -> + composeRequestParams( + req.getAppProfileId(), req.getTableName(), req.getAuthorizedViewName()), + settings.checkAndMutateRowSettings(), + req -> req.toProto(requestContext), + CheckAndMutateRowResponse::getPredicateMatched); } /** @@ -1107,39 +1059,16 @@ public Map extract( * */ private UnaryCallable createReadModifyWriteRowCallable() { - UnaryCallable base = - GrpcRawCallableFactory.createUnaryCallable( - GrpcCallSettings.newBuilder() - .setMethodDescriptor(BigtableGrpc.getReadModifyWriteRowMethod()) - .setParamsExtractor( - new RequestParamsExtractor() { - @Override - public Map extract(ReadModifyWriteRowRequest request) { - String tableName = request.getTableName(); - String authorizedViewName = request.getAuthorizedViewName(); - if (tableName.isEmpty()) { - tableName = - NameUtil.extractTableNameFromAuthorizedViewName(authorizedViewName); - } - return ImmutableMap.of( - "table_name", tableName, "app_profile_id", request.getAppProfileId()); - } - }) - .build(), - settings.readModifyWriteRowSettings().getRetryableCodes()); - - UnaryCallable withStatsHeaders = - new StatsHeadersUnaryCallable<>(base); - - String methodName = "ReadModifyWriteRow"; - UnaryCallable withBigtableTracer = - new BigtableTracerUnaryCallable<>(withStatsHeaders); - - UnaryCallable retrying = - withRetries(withBigtableTracer, settings.readModifyWriteRowSettings()); - - return createUserFacingUnaryCallable( - methodName, new ReadModifyWriteRowCallable(retrying, requestContext)); + DefaultRowAdapter rowAdapter = new DefaultRowAdapter(); + + return createUnaryCallable( + BigtableGrpc.getReadModifyWriteRowMethod(), + req -> + composeRequestParams( + req.getAppProfileId(), req.getTableName(), req.getAuthorizedViewName()), + settings.readModifyWriteRowSettings(), + req -> req.toProto(requestContext), + resp -> rowAdapter.createRowFromProto(resp.getRow())); } /** @@ -1168,18 +1097,7 @@ public Map extract(ReadModifyWriteRowRequest request) { .setMethodDescriptor( BigtableGrpc.getGenerateInitialChangeStreamPartitionsMethod()) .setParamsExtractor( - new RequestParamsExtractor() { - @Override - public Map extract( - GenerateInitialChangeStreamPartitionsRequest - generateInitialChangeStreamPartitionsRequest) { - return ImmutableMap.of( - "table_name", - generateInitialChangeStreamPartitionsRequest.getTableName(), - "app_profile_id", - generateInitialChangeStreamPartitionsRequest.getAppProfileId()); - } - }) + r -> composeRequestParams(r.getAppProfileId(), r.getTableName(), "")) .build(), settings.generateInitialChangeStreamPartitionsSettings().getRetryableCodes()); @@ -1222,7 +1140,15 @@ public Map extract( ServerStreamingCallable traced = new TracedServerStreamingCallable<>(retrying, clientContext.getTracerFactory(), span); - return traced.withDefaultCallContext(clientContext.getDefaultCallContext()); + return traced.withDefaultCallContext( + clientContext + .getDefaultCallContext() + .withOption( + BigtableTracer.OPERATION_TIMEOUT_KEY, + settings + .generateInitialChangeStreamPartitionsSettings() + .getRetrySettings() + .getTotalTimeout())); } /** @@ -1248,15 +1174,7 @@ public Map extract( GrpcCallSettings.newBuilder() .setMethodDescriptor(BigtableGrpc.getReadChangeStreamMethod()) .setParamsExtractor( - new RequestParamsExtractor() { - @Override - public Map extract( - ReadChangeStreamRequest readChangeStreamRequest) { - return ImmutableMap.of( - "table_name", readChangeStreamRequest.getTableName(), - "app_profile_id", readChangeStreamRequest.getAppProfileId()); - } - }) + r -> composeRequestParams(r.getAppProfileId(), r.getTableName(), "")) .build(), settings.readChangeStreamSettings().getRetryableCodes()); @@ -1302,7 +1220,12 @@ public Map extract( new TracedServerStreamingCallable<>( readChangeStreamUserCallable, clientContext.getTracerFactory(), span); - return traced.withDefaultCallContext(clientContext.getDefaultCallContext()); + return traced.withDefaultCallContext( + clientContext + .getDefaultCallContext() + .withOption( + BigtableTracer.OPERATION_TIMEOUT_KEY, + settings.readChangeStreamSettings().getRetrySettings().getTotalTimeout())); } /** @@ -1388,7 +1311,13 @@ public Map extract(ExecuteQueryRequest executeQueryRequest) { new TracedServerStreamingCallable<>(retries, clientContext.getTracerFactory(), span); return new ExecuteQueryCallable( - traced.withDefaultCallContext(clientContext.getDefaultCallContext()), requestContext); + traced.withDefaultCallContext( + clientContext + .getDefaultCallContext() + .withOption( + BigtableTracer.OPERATION_TIMEOUT_KEY, + settings.executeQuerySettings().getRetrySettings().getTotalTimeout())), + requestContext); } /** @@ -1404,6 +1333,124 @@ private UnaryCallable createUserFacin return traced.withDefaultCallContext(clientContext.getDefaultCallContext()); } + private Map composeRequestParams( + String appProfileId, String tableName, String authorizedViewName) { + if (tableName.isEmpty() && !authorizedViewName.isEmpty()) { + tableName = NameUtil.extractTableNameFromAuthorizedViewName(authorizedViewName); + } + return ImmutableMap.of("table_name", tableName, "app_profile_id", appProfileId); + } + + private UnaryCallable createUnaryCallable( + MethodDescriptor methodDescriptor, + RequestParamsExtractor headerParamsFn, + UnaryCallSettings callSettings, + Function requestTransformer, + Function responseTranformer) { + if (settings.getEnableSkipTrailers()) { + return createUnaryCallableNew( + methodDescriptor, headerParamsFn, callSettings, requestTransformer, responseTranformer); + } else { + return createUnaryCallableOld( + methodDescriptor, headerParamsFn, callSettings, requestTransformer, responseTranformer); + } + } + + private UnaryCallable createUnaryCallableOld( + MethodDescriptor methodDescriptor, + RequestParamsExtractor headerParamsFn, + UnaryCallSettings callSettings, + Function requestTransformer, + Function responseTranformer) { + + UnaryCallable base = + GrpcRawCallableFactory.createUnaryCallable( + GrpcCallSettings.newBuilder() + .setMethodDescriptor(methodDescriptor) + .setParamsExtractor(headerParamsFn) + .build(), + callSettings.getRetryableCodes()); + + UnaryCallable withStatsHeaders = new StatsHeadersUnaryCallable<>(base); + + UnaryCallable withBigtableTracer = + new BigtableTracerUnaryCallable<>(withStatsHeaders); + + UnaryCallable retrying = withRetries(withBigtableTracer, callSettings); + + UnaryCallable transformed = + new UnaryCallable() { + @Override + public ApiFuture futureCall(ReqT reqT, ApiCallContext apiCallContext) { + ApiFuture f = + retrying.futureCall(requestTransformer.apply(reqT), apiCallContext); + return ApiFutures.transform( + f, responseTranformer::apply, MoreExecutors.directExecutor()); + } + }; + + UnaryCallable traced = + new TracedUnaryCallable<>( + transformed, + clientContext.getTracerFactory(), + getSpanName(methodDescriptor.getBareMethodName())); + + return traced.withDefaultCallContext( + clientContext + .getDefaultCallContext() + .withOption( + BigtableTracer.OPERATION_TIMEOUT_KEY, + callSettings.getRetrySettings().getTotalTimeout())); + } + + private UnaryCallable createUnaryCallableNew( + MethodDescriptor methodDescriptor, + RequestParamsExtractor headerParamsFn, + UnaryCallSettings callSettings, + Function requestTransformer, + Function responseTranformer) { + + ServerStreamingCallable base = + GrpcRawCallableFactory.createServerStreamingCallable( + GrpcCallSettings.newBuilder() + .setMethodDescriptor(methodDescriptor) + .setParamsExtractor(headerParamsFn) + .build(), + callSettings.getRetryableCodes()); + + base = new StatsHeadersServerStreamingCallable<>(base); + + base = new BigtableTracerStreamingCallable<>(base); + + base = withRetries(base, convertUnaryToServerStreamingSettings(callSettings)); + + ServerStreamingCallable transformed = + new TransformingServerStreamingCallable<>(base, requestTransformer, responseTranformer); + + return new BigtableUnaryOperationCallable<>( + transformed, + clientContext + .getDefaultCallContext() + .withOption( + BigtableTracer.OPERATION_TIMEOUT_KEY, + callSettings.getRetrySettings().getTotalTimeout()), + clientContext.getTracerFactory(), + getSpanName(methodDescriptor.getBareMethodName()), + /* allowNoResponse= */ false); + } + + private static + ServerStreamingCallSettings convertUnaryToServerStreamingSettings( + UnaryCallSettings unarySettings) { + return ServerStreamingCallSettings.newBuilder() + .setResumptionStrategy(new SimpleStreamResumptionStrategy<>()) + .setRetryableCodes(unarySettings.getRetryableCodes()) + .setRetrySettings(unarySettings.getRetrySettings()) + .setIdleTimeoutDuration(Duration.ZERO) + .setWaitTimeoutDuration(Duration.ZERO) + .build(); + } + private UnaryCallable createPingAndWarmCallable() { UnaryCallable pingAndWarm = GrpcRawCallableFactory.createUnaryCallable( @@ -1420,7 +1467,12 @@ public Map extract(PingAndWarmRequest request) { }) .build(), Collections.emptySet()); - return pingAndWarm.withDefaultCallContext(clientContext.getDefaultCallContext()); + return pingAndWarm.withDefaultCallContext( + clientContext + .getDefaultCallContext() + .withOption( + BigtableTracer.OPERATION_TIMEOUT_KEY, + settings.pingAndWarmSettings().getRetrySettings().getTotalTimeout())); } private UnaryCallable withRetries( @@ -1470,6 +1522,8 @@ public UnaryCallable readRowCallable() { return readRowCallable; } + /** Deprecated, please use {@link #sampleRowKeysCallableWithRequest} */ + @Deprecated public UnaryCallable> sampleRowKeysCallable() { return sampleRowKeysCallable; } diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java index 2a3d0ddba4..1425e7b362 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java @@ -62,6 +62,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.logging.Logger; import javax.annotation.Nonnull; @@ -105,7 +106,14 @@ public class EnhancedBigtableStubSettings extends StubSettings IDEMPOTENT_RETRY_CODES = ImmutableSet.of(Code.DEADLINE_EXCEEDED, Code.UNAVAILABLE); @@ -232,6 +240,7 @@ public class EnhancedBigtableStubSettings extends StubSettings jwtAudienceMapping; private final boolean enableRoutingCookie; private final boolean enableRetryInfo; + private final boolean enableSkipTrailers; private final ServerStreamingCallSettings readRowsSettings; private final UnaryCallSettings readRowSettings; @@ -279,6 +288,7 @@ private EnhancedBigtableStubSettings(Builder builder) { jwtAudienceMapping = builder.jwtAudienceMapping; enableRoutingCookie = builder.enableRoutingCookie; enableRetryInfo = builder.enableRetryInfo; + enableSkipTrailers = builder.enableSkipTrailers; metricsProvider = builder.metricsProvider; metricsEndpoint = builder.metricsEndpoint; @@ -365,6 +375,10 @@ public boolean getEnableRetryInfo() { return enableRetryInfo; } + boolean getEnableSkipTrailers() { + return enableSkipTrailers; + } + /** * Gets the Google Cloud Monitoring endpoint for publishing client side metrics. If it's null, * client will publish metrics to the default monitoring endpoint. @@ -376,10 +390,9 @@ public String getMetricsEndpoint() { /** Returns a builder for the default ChannelProvider for this service. */ public static InstantiatingGrpcChannelProvider.Builder defaultGrpcTransportProviderBuilder() { - Boolean isDirectpathEnabled = Boolean.parseBoolean(System.getenv(CBT_ENABLE_DIRECTPATH)); InstantiatingGrpcChannelProvider.Builder grpcTransportProviderBuilder = BigtableStubSettings.defaultGrpcTransportProviderBuilder(); - if (isDirectpathEnabled) { + if (DIRECT_PATH_ENABLED) { // Attempts direct access to CBT service over gRPC to improve throughput, // whether the attempt is allowed is totally controlled by service owner. grpcTransportProviderBuilder @@ -676,6 +689,7 @@ public static class Builder extends StubSettings.Builder jwtAudienceMapping; private boolean enableRoutingCookie; private boolean enableRetryInfo; + private boolean enableSkipTrailers; private final ServerStreamingCallSettings.Builder readRowsSettings; private final UnaryCallSettings.Builder readRowSettings; @@ -714,6 +728,7 @@ private Builder() { setCredentialsProvider(defaultCredentialsProviderBuilder().build()); this.enableRoutingCookie = true; this.enableRetryInfo = true; + this.enableSkipTrailers = SKIP_TRAILERS; metricsProvider = DefaultMetricsProvider.INSTANCE; // Defaults provider @@ -830,7 +845,11 @@ private Builder() { .setWaitTimeout(Duration.ofMinutes(5)); featureFlags = - FeatureFlags.newBuilder().setReverseScans(true).setLastScannedRowResponses(true); + FeatureFlags.newBuilder() + .setReverseScans(true) + .setLastScannedRowResponses(true) + .setDirectAccessRequested(DIRECT_PATH_ENABLED) + .setTrafficDirectorEnabled(DIRECT_PATH_ENABLED); } private Builder(EnhancedBigtableStubSettings settings) { @@ -1074,6 +1093,11 @@ public boolean getEnableRetryInfo() { return enableRetryInfo; } + Builder setEnableSkipTrailers(boolean enabled) { + this.enableSkipTrailers = enabled; + return this; + } + /** Returns the builder for the settings used for calls to readRows. */ public ServerStreamingCallSettings.Builder readRowsSettings() { return readRowsSettings; @@ -1201,6 +1225,7 @@ public String toString() { .add("jwtAudienceMapping", jwtAudienceMapping) .add("enableRoutingCookie", enableRoutingCookie) .add("enableRetryInfo", enableRetryInfo) + .add("enableSkipTrailers", enableSkipTrailers) .add("readRowsSettings", readRowsSettings) .add("readRowSettings", readRowSettings) .add("sampleRowKeysSettings", sampleRowKeysSettings) diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/MutateRowCallable.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/MutateRowCallable.java deleted file mode 100644 index 36f47c2d1f..0000000000 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/MutateRowCallable.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2018 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.bigtable.data.v2.stub; - -import com.google.api.core.ApiFunction; -import com.google.api.core.ApiFuture; -import com.google.api.core.ApiFutures; -import com.google.api.gax.rpc.ApiCallContext; -import com.google.api.gax.rpc.UnaryCallable; -import com.google.bigtable.v2.MutateRowRequest; -import com.google.bigtable.v2.MutateRowResponse; -import com.google.cloud.bigtable.data.v2.internal.RequestContext; -import com.google.cloud.bigtable.data.v2.models.RowMutation; -import com.google.common.util.concurrent.MoreExecutors; - -/** Simple wrapper for MutateRow to wrap the request and response protobufs. */ -class MutateRowCallable extends UnaryCallable { - private final UnaryCallable inner; - private final RequestContext requestContext; - - MutateRowCallable( - UnaryCallable inner, RequestContext requestContext) { - - this.inner = inner; - this.requestContext = requestContext; - } - - @Override - public ApiFuture futureCall(RowMutation request, ApiCallContext context) { - ApiFuture rawResponse = - inner.futureCall(request.toProto(requestContext), context); - - return ApiFutures.transform( - rawResponse, - new ApiFunction() { - @Override - public Void apply(MutateRowResponse mutateRowResponse) { - return null; - } - }, - MoreExecutors.directExecutor()); - } -} diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/ReadModifyWriteRowCallable.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/ReadModifyWriteRowCallable.java deleted file mode 100644 index 09e133678e..0000000000 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/ReadModifyWriteRowCallable.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2018 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.bigtable.data.v2.stub; - -import com.google.api.core.ApiFunction; -import com.google.api.core.ApiFuture; -import com.google.api.core.ApiFutures; -import com.google.api.gax.rpc.ApiCallContext; -import com.google.api.gax.rpc.UnaryCallable; -import com.google.bigtable.v2.ReadModifyWriteRowRequest; -import com.google.bigtable.v2.ReadModifyWriteRowResponse; -import com.google.cloud.bigtable.data.v2.internal.RequestContext; -import com.google.cloud.bigtable.data.v2.models.DefaultRowAdapter; -import com.google.cloud.bigtable.data.v2.models.ReadModifyWriteRow; -import com.google.cloud.bigtable.data.v2.models.Row; -import com.google.common.util.concurrent.MoreExecutors; - -/** Simple wrapper for ReadModifyWriteRow to wrap the request and response protobufs. */ -class ReadModifyWriteRowCallable extends UnaryCallable { - private final UnaryCallable inner; - private final RequestContext requestContext; - private final DefaultRowAdapter rowAdapter; - - ReadModifyWriteRowCallable( - UnaryCallable inner, - RequestContext requestContext) { - this.inner = inner; - this.requestContext = requestContext; - this.rowAdapter = new DefaultRowAdapter(); - } - - @Override - public ApiFuture futureCall(ReadModifyWriteRow request, ApiCallContext context) { - ApiFuture rawResponse = - inner.futureCall(request.toProto(requestContext), context); - - return ApiFutures.transform( - rawResponse, - new ApiFunction() { - @Override - public Row apply(ReadModifyWriteRowResponse readModifyWriteRowResponse) { - return convertResponse(readModifyWriteRowResponse); - } - }, - MoreExecutors.directExecutor()); - } - - private Row convertResponse(ReadModifyWriteRowResponse response) { - return rowAdapter.createRowFromProto(response.getRow()); - } -} diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/SafeResponseObserver.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/SafeResponseObserver.java index 7c65bdf95a..0133dd3c2b 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/SafeResponseObserver.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/SafeResponseObserver.java @@ -83,7 +83,7 @@ public final void onResponse(ResponseT response) { @Override public final void onError(Throwable throwable) { if (!isClosed.compareAndSet(false, true)) { - logException("Received error after the stream is closed"); + logException("Received error after the stream is closed", throwable); return; } @@ -113,6 +113,10 @@ private void logException(String message) { LOGGER.log(Level.WARNING, message, new IllegalStateException(message)); } + private void logException(String message, Throwable cause) { + LOGGER.log(Level.WARNING, message, new IllegalStateException(message, cause)); + } + protected abstract void onStartImpl(StreamController streamController); protected abstract void onResponseImpl(ResponseT response); diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/SampleRowKeysCallable.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/SampleRowKeysCallable.java deleted file mode 100644 index 7658e41492..0000000000 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/SampleRowKeysCallable.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2018 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.bigtable.data.v2.stub; - -import com.google.api.core.ApiFunction; -import com.google.api.core.ApiFuture; -import com.google.api.core.ApiFutures; -import com.google.api.gax.rpc.ApiCallContext; -import com.google.api.gax.rpc.UnaryCallable; -import com.google.bigtable.v2.SampleRowKeysRequest; -import com.google.bigtable.v2.SampleRowKeysResponse; -import com.google.cloud.bigtable.data.v2.internal.NameUtil; -import com.google.cloud.bigtable.data.v2.internal.RequestContext; -import com.google.cloud.bigtable.data.v2.models.KeyOffset; -import com.google.common.collect.ImmutableList; -import com.google.common.util.concurrent.MoreExecutors; -import java.util.List; - -/** Simple wrapper for SampleRowKeys to wrap the request and response protobufs. */ -class SampleRowKeysCallable extends UnaryCallable> { - private final RequestContext requestContext; - private final UnaryCallable> inner; - - SampleRowKeysCallable( - UnaryCallable> inner, - RequestContext requestContext) { - - this.requestContext = requestContext; - this.inner = inner; - } - - @Override - public ApiFuture> futureCall(String tableId, ApiCallContext context) { - String tableName = - NameUtil.formatTableName( - requestContext.getProjectId(), requestContext.getInstanceId(), tableId); - - SampleRowKeysRequest request = - SampleRowKeysRequest.newBuilder() - .setTableName(tableName) - .setAppProfileId(requestContext.getAppProfileId()) - .build(); - - ApiFuture> rawResponse = inner.futureCall(request, context); - - return ApiFutures.transform( - rawResponse, - new ApiFunction, List>() { - @Override - public List apply(List rawResponse) { - return convert(rawResponse); - } - }, - MoreExecutors.directExecutor()); - } - - private static List convert(List rawResponse) { - ImmutableList.Builder results = ImmutableList.builder(); - - for (SampleRowKeysResponse element : rawResponse) { - results.add(KeyOffset.create(element.getRowKey(), element.getOffsetBytes())); - } - - return results.build(); - } -} diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/TransformingServerStreamingCallable.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/TransformingServerStreamingCallable.java new file mode 100644 index 0000000000..29b104965e --- /dev/null +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/TransformingServerStreamingCallable.java @@ -0,0 +1,72 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.bigtable.data.v2.stub; + +import com.google.api.gax.rpc.ApiCallContext; +import com.google.api.gax.rpc.ResponseObserver; +import com.google.api.gax.rpc.ServerStreamingCallable; +import com.google.api.gax.rpc.StreamController; +import java.util.function.Function; + +/** Callable to help crossing api boundary lines between models and protos */ +class TransformingServerStreamingCallable + extends ServerStreamingCallable { + private final ServerStreamingCallable inner; + private final Function requestTransformer; + private final Function responseTransformer; + + public TransformingServerStreamingCallable( + ServerStreamingCallable inner, + Function requestTransformer, + Function responseTransformer) { + this.inner = inner; + this.requestTransformer = requestTransformer; + this.responseTransformer = responseTransformer; + } + + @Override + public void call( + OuterReqT outerReqT, + ResponseObserver outerObserver, + ApiCallContext apiCallContext) { + InnerReqT innerReq = requestTransformer.apply(outerReqT); + + inner.call( + innerReq, + new SafeResponseObserver(outerObserver) { + @Override + public void onStartImpl(StreamController streamController) { + outerObserver.onStart(streamController); + } + + @Override + public void onResponseImpl(InnerRespT innerResp) { + outerObserver.onResponse(responseTransformer.apply(innerResp)); + } + + @Override + public void onErrorImpl(Throwable throwable) { + outerObserver.onError(throwable); + } + + @Override + public void onCompleteImpl() { + outerObserver.onComplete(); + } + }, + apiCallContext); + } +} diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableCloudMonitoringExporter.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableCloudMonitoringExporter.java index fd54313e8d..8aa53fa198 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableCloudMonitoringExporter.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableCloudMonitoringExporter.java @@ -23,6 +23,7 @@ import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.METER_NAME; import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.OPERATION_LATENCIES_NAME; import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.PER_CONNECTION_ERROR_COUNT_NAME; +import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.REMAINING_DEADLINE_NAME; import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.RETRY_COUNT_NAME; import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.SERVER_LATENCIES_NAME; @@ -115,7 +116,8 @@ public final class BigtableCloudMonitoringExporter implements MetricExporter { CLIENT_BLOCKING_LATENCIES_NAME, APPLICATION_BLOCKING_LATENCIES_NAME, RETRY_COUNT_NAME, - CONNECTIVITY_ERROR_COUNT_NAME) + CONNECTIVITY_ERROR_COUNT_NAME, + REMAINING_DEADLINE_NAME) .stream() .map(m -> METER_NAME + m) .collect(ImmutableList.toImmutableList()); diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableTracer.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableTracer.java index d0e307d510..fb6a84a88d 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableTracer.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableTracer.java @@ -16,10 +16,12 @@ package com.google.cloud.bigtable.data.v2.stub.metrics; import com.google.api.core.BetaApi; +import com.google.api.core.InternalApi; import com.google.api.gax.rpc.ApiCallContext; import com.google.api.gax.tracing.ApiTracer; import com.google.api.gax.tracing.BaseApiTracer; import javax.annotation.Nullable; +import org.threeten.bp.Duration; /** * A Bigtable specific {@link ApiTracer} that includes additional contexts. This class is a base @@ -30,6 +32,10 @@ public class BigtableTracer extends BaseApiTracer { private volatile int attempt = 0; + @InternalApi("for internal use only") + public static final ApiCallContext.Key OPERATION_TIMEOUT_KEY = + ApiCallContext.Key.create("OPERATION_TIMEOUT"); + @Override public void attemptStarted(int attemptNumber) { this.attempt = attemptNumber; @@ -52,6 +58,13 @@ public void afterResponse(long applicationLatency) { // noop } + /** + * Used by BigtableUnaryOperationCallable to signal that the user visible portion of the RPC is + * complete and that metrics should freeze the timers and then publish the frozen values when the + * internal portion of the operation completes. + */ + public void operationFinishEarly() {} + /** * Get the attempt number of the current call. Attempt number for the current call is passed in * and should be recorded in {@link #attemptStarted(int)}. With the getter we can access it from @@ -93,4 +106,12 @@ public void grpcChannelQueuedLatencies(long queuedTimeMs) { public void grpcMessageSent() { // noop } + + /** + * Record the operation timeout from user settings for calculating remaining deadline. This will + * be called in BuiltinMetricsTracer. + */ + public void setOperationTimeout(Duration operationTimeout) { + // noop + } } diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableTracerStreamingCallable.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableTracerStreamingCallable.java index 167cd0dc2e..b977a0a2c7 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableTracerStreamingCallable.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableTracerStreamingCallable.java @@ -16,6 +16,7 @@ package com.google.cloud.bigtable.data.v2.stub.metrics; import com.google.api.core.InternalApi; +import com.google.api.gax.grpc.GrpcCallContext; import com.google.api.gax.grpc.GrpcResponseMetadata; import com.google.api.gax.rpc.ApiCallContext; import com.google.api.gax.rpc.ResponseObserver; @@ -26,6 +27,7 @@ import com.google.common.base.Stopwatch; import java.util.concurrent.TimeUnit; import javax.annotation.Nonnull; +import org.threeten.bp.Duration; /** * This callable will @@ -62,6 +64,11 @@ public void call( BigtableTracerResponseObserver innerObserver = new BigtableTracerResponseObserver<>( responseObserver, (BigtableTracer) context.getTracer(), responseMetadata); + GrpcCallContext callContext = (GrpcCallContext) context; + Duration deadline = callContext.getOption(BigtableTracer.OPERATION_TIMEOUT_KEY); + if (deadline != null) { + ((BigtableTracer) context.getTracer()).setOperationTimeout(deadline); + } innerCallable.call( request, innerObserver, diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableTracerUnaryCallable.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableTracerUnaryCallable.java index 7dfca8b753..1f000c4639 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableTracerUnaryCallable.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableTracerUnaryCallable.java @@ -19,12 +19,14 @@ import com.google.api.core.ApiFutureCallback; import com.google.api.core.ApiFutures; import com.google.api.core.InternalApi; +import com.google.api.gax.grpc.GrpcCallContext; import com.google.api.gax.grpc.GrpcResponseMetadata; import com.google.api.gax.rpc.ApiCallContext; import com.google.api.gax.rpc.UnaryCallable; import com.google.common.base.Preconditions; import com.google.common.util.concurrent.MoreExecutors; import javax.annotation.Nonnull; +import org.threeten.bp.Duration; /** * This callable will: @@ -58,6 +60,11 @@ public ApiFuture futureCall(RequestT request, ApiCallContext context) BigtableTracerUnaryCallback callback = new BigtableTracerUnaryCallback( (BigtableTracer) context.getTracer(), responseMetadata); + GrpcCallContext callContext = (GrpcCallContext) context; + Duration deadline = callContext.getOption(BigtableTracer.OPERATION_TIMEOUT_KEY); + if (deadline != null) { + ((BigtableTracer) context.getTracer()).setOperationTimeout(deadline); + } ApiFuture future = innerCallable.futureCall( request, diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsConstants.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsConstants.java index d85300828b..62ac0f1153 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsConstants.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsConstants.java @@ -58,6 +58,7 @@ public class BuiltinMetricsConstants { static final String SERVER_LATENCIES_NAME = "server_latencies"; static final String FIRST_RESPONSE_LATENCIES_NAME = "first_response_latencies"; static final String APPLICATION_BLOCKING_LATENCIES_NAME = "application_latencies"; + static final String REMAINING_DEADLINE_NAME = "remaining_deadline"; static final String CLIENT_BLOCKING_LATENCIES_NAME = "throttling_latencies"; static final String PER_CONNECTION_ERROR_COUNT_NAME = "per_connection_error_count"; @@ -214,6 +215,16 @@ public static Map getAllViews() { ImmutableSet.builder() .add(BIGTABLE_PROJECT_ID_KEY, INSTANCE_ID_KEY, APP_PROFILE_KEY, CLIENT_NAME_KEY) .build()); + defineView( + views, + REMAINING_DEADLINE_NAME, + AGGREGATION_WITH_MILLIS_HISTOGRAM, + InstrumentType.HISTOGRAM, + "ms", + ImmutableSet.builder() + .addAll(COMMON_ATTRIBUTES) + .add(STREAMING_KEY, STATUS_KEY) + .build()); return views.build(); } diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracer.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracer.java index 8012edfaba..20e7eb3716 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracer.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracer.java @@ -37,6 +37,8 @@ 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; import javax.annotation.Nullable; import org.threeten.bp.Duration; @@ -46,11 +48,14 @@ */ class BuiltinMetricsTracer extends BigtableTracer { + private static final Logger logger = Logger.getLogger(BuiltinMetricsTracer.class.getName()); + private static final String NAME = "java-bigtable/" + Version.VERSION; private final OperationType operationType; private final SpanName spanName; // Operation level metrics + private final AtomicBoolean operationFinishedEarly = new AtomicBoolean(); private final AtomicBoolean opFinished = new AtomicBoolean(); private final Stopwatch operationTimer = Stopwatch.createStarted(); private final Stopwatch firstResponsePerOpTimer = Stopwatch.createStarted(); @@ -67,7 +72,6 @@ class BuiltinMetricsTracer extends BigtableTracer { // Stopwatch is not thread safe so this is a workaround to check if the stopwatch changes is // flushed to memory. private final Stopwatch serverLatencyTimer = Stopwatch.createUnstarted(); - private boolean serverLatencyTimerIsRunning = false; private final Object timerLock = new Object(); private boolean flowControlIsDisabled = false; @@ -86,6 +90,9 @@ class BuiltinMetricsTracer extends BigtableTracer { private Long serverLatencies = null; private final AtomicLong grpcMessageSentDelay = new AtomicLong(0); + private Duration operationTimeout = Duration.ofMillis(0); + private long remainingOperationTimeout = 0; + // OpenCensus (and server) histogram buckets use [start, end), however OpenTelemetry uses (start, // end]. To work around this, we measure all the latencies in nanoseconds and convert them // to milliseconds and use DoubleHistogram. This should minimize the chance of a data @@ -96,6 +103,7 @@ class BuiltinMetricsTracer extends BigtableTracer { private final DoubleHistogram firstResponseLatenciesHistogram; private final DoubleHistogram clientBlockingLatenciesHistogram; private final DoubleHistogram applicationBlockingLatenciesHistogram; + private final DoubleHistogram remainingDeadlineHistogram; private final LongCounter connectivityErrorCounter; private final LongCounter retryCounter; @@ -109,6 +117,7 @@ class BuiltinMetricsTracer extends BigtableTracer { DoubleHistogram firstResponseLatenciesHistogram, DoubleHistogram clientBlockingLatenciesHistogram, DoubleHistogram applicationBlockingLatenciesHistogram, + DoubleHistogram deadlineHistogram, LongCounter connectivityErrorCounter, LongCounter retryCounter) { this.operationType = operationType; @@ -121,6 +130,7 @@ class BuiltinMetricsTracer extends BigtableTracer { this.firstResponseLatenciesHistogram = firstResponseLatenciesHistogram; this.clientBlockingLatenciesHistogram = clientBlockingLatenciesHistogram; this.applicationBlockingLatenciesHistogram = applicationBlockingLatenciesHistogram; + this.remainingDeadlineHistogram = deadlineHistogram; this.connectivityErrorCounter = connectivityErrorCounter; this.retryCounter = retryCounter; } @@ -133,6 +143,13 @@ public void close() {} }; } + @Override + public void operationFinishEarly() { + operationFinishedEarly.set(true); + attemptTimer.stop(); + operationTimer.stop(); + } + @Override public void operationSucceeded() { recordOperationCompletion(null); @@ -163,12 +180,16 @@ public void attemptStarted(Object request, int attemptNumber) { } if (!flowControlIsDisabled) { synchronized (timerLock) { - if (!serverLatencyTimerIsRunning) { + if (!serverLatencyTimer.isRunning()) { serverLatencyTimer.start(); - serverLatencyTimerIsRunning = true; } } } + // OperationTimeout is only set after the first attempt. + if (attemptCount > 1) { + remainingOperationTimeout = + operationTimeout.toMillis() - operationTimer.elapsed(TimeUnit.MILLISECONDS); + } } @Override @@ -194,13 +215,17 @@ public void attemptPermanentFailure(Throwable throwable) { @Override public void onRequest(int requestCount) { requestLeft.accumulateAndGet(requestCount, IntMath::saturatedAdd); + + if (operationFinishedEarly.get()) { + return; + } + if (flowControlIsDisabled) { // On request is only called when auto flow control is disabled. When auto flow control is // disabled, server latency is measured between onRequest and onResponse. synchronized (timerLock) { - if (!serverLatencyTimerIsRunning) { + if (!serverLatencyTimer.isRunning()) { serverLatencyTimer.start(); - serverLatencyTimerIsRunning = true; } } } @@ -208,6 +233,10 @@ public void onRequest(int requestCount) { @Override public void responseReceived() { + if (operationFinishedEarly.get()) { + return; + } + if (firstResponsePerOpTimer.isRunning()) { firstResponsePerOpTimer.stop(); } @@ -219,10 +248,9 @@ public void responseReceived() { // latency is measured between afterResponse and responseReceived. // In all the cases, we want to stop the serverLatencyTimer here. synchronized (timerLock) { - if (serverLatencyTimerIsRunning) { + if (serverLatencyTimer.isRunning()) { totalServerLatencyNano.addAndGet(serverLatencyTimer.elapsed(TimeUnit.NANOSECONDS)); serverLatencyTimer.reset(); - serverLatencyTimerIsRunning = false; } } } @@ -230,14 +258,16 @@ public void responseReceived() { @Override public void afterResponse(long applicationLatency) { if (!flowControlIsDisabled || requestLeft.decrementAndGet() > 0) { + if (operationFinishedEarly.get()) { + return; + } // When auto flow control is enabled, request will never be called, so server latency is // measured between after the last response is processed and before the next response is // received. If flow control is disabled but requestLeft is greater than 0, // also start the timer to count the time between afterResponse and responseReceived. synchronized (timerLock) { - if (!serverLatencyTimerIsRunning) { + if (!serverLatencyTimer.isRunning()) { serverLatencyTimer.start(); - serverLatencyTimerIsRunning = true; } } } @@ -271,16 +301,28 @@ public void grpcMessageSent() { grpcMessageSentDelay.set(attemptTimer.elapsed(TimeUnit.NANOSECONDS)); } + /* + This is called by BigtableTracerCallables that sets operation timeout from user settings. + */ + @Override + public void setOperationTimeout(Duration operationTimeout) { + this.operationTimeout = operationTimeout; + } + @Override public void disableFlowControl() { flowControlIsDisabled = true; } private void recordOperationCompletion(@Nullable Throwable status) { + if (operationFinishedEarly.get()) { + status = null; // force an ok + } + if (!opFinished.compareAndSet(false, true)) { return; } - operationTimer.stop(); + long operationLatencyNano = operationTimer.elapsed(TimeUnit.NANOSECONDS); boolean isStreaming = operationType == OperationType.ServerStreaming; String statusStr = Util.extractStatus(status); @@ -299,8 +341,6 @@ private void recordOperationCompletion(@Nullable Throwable status) { .put(STATUS_KEY, statusStr) .build(); - long operationLatencyNano = operationTimer.elapsed(TimeUnit.NANOSECONDS); - // Only record when retry count is greater than 0 so the retry // graph will be less confusing if (attemptCount > 1) { @@ -321,14 +361,16 @@ private void recordOperationCompletion(@Nullable Throwable status) { } private void recordAttemptCompletion(@Nullable Throwable status) { + if (operationFinishedEarly.get()) { + status = null; // force an ok + } // If the attempt failed, the time spent in retry should be counted in application latency. // Stop the stopwatch and decrement requestLeft. synchronized (timerLock) { - if (serverLatencyTimerIsRunning) { + if (serverLatencyTimer.isRunning()) { requestLeft.decrementAndGet(); totalServerLatencyNano.addAndGet(serverLatencyTimer.elapsed(TimeUnit.NANOSECONDS)); serverLatencyTimer.reset(); - serverLatencyTimerIsRunning = false; } } @@ -361,6 +403,17 @@ private void recordAttemptCompletion(@Nullable Throwable status) { attemptLatenciesHistogram.record( convertToMs(attemptTimer.elapsed(TimeUnit.NANOSECONDS)), attributes); + if (attemptCount <= 1) { + remainingDeadlineHistogram.record(operationTimeout.toMillis(), attributes); + } else if (remainingOperationTimeout >= 0) { + remainingDeadlineHistogram.record(remainingOperationTimeout, attributes); + } else if (operationTimeout.toMillis() != 0) { + // If the operationTimeout is set but remaining deadline is < 0, log a warning. This should + // never happen. + logger.log( + Level.WARNING, "The remaining deadline was less than 0: " + remainingOperationTimeout); + } + if (serverLatencies != null) { serverLatenciesHistogram.record(serverLatencies, attributes); connectivityErrorCounter.add(0, attributes); diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracerFactory.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracerFactory.java index f0ac656978..18d3a3ace9 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracerFactory.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracerFactory.java @@ -22,6 +22,7 @@ import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.FIRST_RESPONSE_LATENCIES_NAME; import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.METER_NAME; import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.OPERATION_LATENCIES_NAME; +import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.REMAINING_DEADLINE_NAME; import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.RETRY_COUNT_NAME; import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.SERVER_LATENCIES_NAME; @@ -55,6 +56,7 @@ public class BuiltinMetricsTracerFactory extends BaseApiTracerFactory { private final DoubleHistogram firstResponseLatenciesHistogram; private final DoubleHistogram clientBlockingLatenciesHistogram; private final DoubleHistogram applicationBlockingLatenciesHistogram; + private final DoubleHistogram remainingDeadlineHistogram; private final LongCounter connectivityErrorCounter; private final LongCounter retryCounter; @@ -108,6 +110,13 @@ public static BuiltinMetricsTracerFactory create( "The latency of the client application consuming available response data.") .setUnit(MILLISECOND) .build(); + remainingDeadlineHistogram = + meter + .histogramBuilder(REMAINING_DEADLINE_NAME) + .setDescription( + "The remaining deadline when the request is sent to grpc. This will either be the operation timeout, or the remaining deadline from operation timeout after retries and back offs.") + .setUnit(MILLISECOND) + .build(); connectivityErrorCounter = meter .counterBuilder(CONNECTIVITY_ERROR_COUNT_NAME) @@ -135,6 +144,7 @@ public ApiTracer newTracer(ApiTracer parent, SpanName spanName, OperationType op firstResponseLatenciesHistogram, clientBlockingLatenciesHistogram, applicationBlockingLatenciesHistogram, + remainingDeadlineHistogram, connectivityErrorCounter, retryCounter); } diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/CompositeTracer.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/CompositeTracer.java index d89aa90c6b..2cee944aa4 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/CompositeTracer.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/CompositeTracer.java @@ -62,6 +62,13 @@ public void close() { }; } + @Override + public void operationFinishEarly() { + for (BigtableTracer tracer : bigtableTracers) { + tracer.operationFinishEarly(); + } + } + @Override public void operationSucceeded() { for (ApiTracer child : children) { @@ -232,4 +239,11 @@ public void grpcMessageSent() { tracer.grpcMessageSent(); } } + + @Override + public void setOperationTimeout(Duration operationTimeout) { + for (BigtableTracer tracer : bigtableTracers) { + tracer.setOperationTimeout(operationTimeout); + } + } } diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/MetricsTracer.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/MetricsTracer.java index 0ffabe2606..a2c5bdac1f 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/MetricsTracer.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/MetricsTracer.java @@ -84,6 +84,12 @@ public void close() {} }; } + @Override + public void operationFinishEarly() { + attemptTimer.stop(); + operationTimer.stop(); + } + @Override public void operationSucceeded() { recordOperationCompletion(null); @@ -103,7 +109,6 @@ private void recordOperationCompletion(@Nullable Throwable throwable) { if (!opFinished.compareAndSet(false, true)) { return; } - operationTimer.stop(); long elapsed = operationTimer.elapsed(TimeUnit.MILLISECONDS); diff --git a/google-cloud-bigtable/src/main/resources/META-INF/native-image/com.google.cloud.bigtable.admin.v2/reflect-config.json b/google-cloud-bigtable/src/main/resources/META-INF/native-image/com.google.cloud.bigtable.admin.v2/reflect-config.json index 5b9d183faa..e725f7653b 100644 --- a/google-cloud-bigtable/src/main/resources/META-INF/native-image/com.google.cloud.bigtable.admin.v2/reflect-config.json +++ b/google-cloud-bigtable/src/main/resources/META-INF/native-image/com.google.cloud.bigtable.admin.v2/reflect-config.json @@ -395,6 +395,24 @@ "allDeclaredClasses": true, "allPublicClasses": true }, + { + "name": "com.google.api.SelectiveGapicGeneration", + "queryAllDeclaredConstructors": true, + "queryAllPublicConstructors": true, + "queryAllDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredClasses": true, + "allPublicClasses": true + }, + { + "name": "com.google.api.SelectiveGapicGeneration$Builder", + "queryAllDeclaredConstructors": true, + "queryAllPublicConstructors": true, + "queryAllDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredClasses": true, + "allPublicClasses": true + }, { "name": "com.google.bigtable.admin.v2.AppProfile", "queryAllDeclaredConstructors": true, diff --git a/google-cloud-bigtable/src/main/resources/META-INF/native-image/com.google.cloud.bigtable.data.v2/reflect-config.json b/google-cloud-bigtable/src/main/resources/META-INF/native-image/com.google.cloud.bigtable.data.v2/reflect-config.json index 7114460ddb..4b89db83f8 100644 --- a/google-cloud-bigtable/src/main/resources/META-INF/native-image/com.google.cloud.bigtable.data.v2/reflect-config.json +++ b/google-cloud-bigtable/src/main/resources/META-INF/native-image/com.google.cloud.bigtable.data.v2/reflect-config.json @@ -431,6 +431,24 @@ "allDeclaredClasses": true, "allPublicClasses": true }, + { + "name": "com.google.api.SelectiveGapicGeneration", + "queryAllDeclaredConstructors": true, + "queryAllPublicConstructors": true, + "queryAllDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredClasses": true, + "allPublicClasses": true + }, + { + "name": "com.google.api.SelectiveGapicGeneration$Builder", + "queryAllDeclaredConstructors": true, + "queryAllPublicConstructors": true, + "queryAllDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredClasses": true, + "allPublicClasses": true + }, { "name": "com.google.bigtable.v2.ArrayValue", "queryAllDeclaredConstructors": true, diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/BigtableUnaryOperationCallableTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/BigtableUnaryOperationCallableTest.java new file mode 100644 index 0000000000..0b11ce3219 --- /dev/null +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/BigtableUnaryOperationCallableTest.java @@ -0,0 +1,158 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.bigtable.data.v2.stub; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import com.google.api.core.ApiFuture; +import com.google.api.gax.grpc.GrpcCallContext; +import com.google.api.gax.tracing.ApiTracerFactory; +import com.google.api.gax.tracing.SpanName; +import com.google.cloud.bigtable.data.v2.stub.metrics.BigtableTracer; +import com.google.cloud.bigtable.gaxx.testing.FakeStreamingApi; +import com.google.cloud.bigtable.gaxx.testing.MockStreamingApi.MockServerStreamingCall; +import com.google.cloud.bigtable.gaxx.testing.MockStreamingApi.MockServerStreamingCallable; +import com.google.common.collect.ImmutableList; +import java.util.concurrent.ExecutionException; +import java.util.logging.Logger; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +@RunWith(JUnit4.class) +public class BigtableUnaryOperationCallableTest { + @Rule public final MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock private ApiTracerFactory tracerFactory; + @Mock private BigtableTracer tracer; + + @Before + public void setUp() throws Exception { + Mockito.when(tracerFactory.newTracer(Mockito.any(), Mockito.any(), Mockito.any())) + .thenReturn(tracer); + } + + @Test + public void testFutureResolve() throws Exception { + BigtableUnaryOperationCallable callable = + new BigtableUnaryOperationCallable<>( + new FakeStreamingApi.ServerStreamingStashCallable<>(ImmutableList.of("value")), + GrpcCallContext.createDefault(), + tracerFactory, + SpanName.of("Fake", "method"), + false); + + ApiFuture f = callable.futureCall("fake"); + assertThat(f.get()).isEqualTo("value"); + } + + @Test + public void testMultipleResponses() throws Exception { + MockServerStreamingCallable inner = new MockServerStreamingCallable<>(); + + BigtableUnaryOperationCallable callable = + new BigtableUnaryOperationCallable<>( + inner, + GrpcCallContext.createDefault(), + tracerFactory, + SpanName.of("Fake", "method"), + false); + callable.logger = Mockito.mock(Logger.class); + + ApiFuture f = callable.futureCall("fake"); + MockServerStreamingCall call = inner.popLastCall(); + call.getController().getObserver().onResponse("first"); + call.getController().getObserver().onResponse("second"); + + ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(String.class); + verify(callable.logger).log(Mockito.any(), msgCaptor.capture()); + assertThat(msgCaptor.getValue()) + .isEqualTo( + "Received response after future is resolved for a Fake.method unary operation. previous: first, New response: second"); + + assertThat(call.getController().isCancelled()).isTrue(); + } + + @Test + public void testCancel() { + MockServerStreamingCallable inner = new MockServerStreamingCallable<>(); + BigtableUnaryOperationCallable callable = + new BigtableUnaryOperationCallable<>( + inner, + GrpcCallContext.createDefault(), + tracerFactory, + SpanName.of("Fake", "method"), + false); + ApiFuture f = callable.futureCall("req"); + f.cancel(true); + + MockServerStreamingCall call = inner.popLastCall(); + assertThat(call.getController().isCancelled()).isTrue(); + } + + @Test + public void testMissingResponse() { + MockServerStreamingCallable inner = new MockServerStreamingCallable<>(); + BigtableUnaryOperationCallable callable = + new BigtableUnaryOperationCallable<>( + inner, + GrpcCallContext.createDefault(), + tracerFactory, + SpanName.of("Fake", "method"), + false); + ApiFuture f = callable.futureCall("req"); + MockServerStreamingCall call = inner.popLastCall(); + call.getController().getObserver().onComplete(); + + Throwable cause = Assert.assertThrows(ExecutionException.class, f::get).getCause(); + assertThat(cause) + .hasMessageThat() + .isEqualTo("Fake.method unary operation completed without a response message"); + } + + @Test + public void testTracing() throws Exception { + MockServerStreamingCallable inner = new MockServerStreamingCallable<>(); + BigtableUnaryOperationCallable callable = + new BigtableUnaryOperationCallable<>( + inner, + GrpcCallContext.createDefault(), + tracerFactory, + SpanName.of("Fake", "method"), + false); + ApiFuture f = callable.futureCall("req"); + MockServerStreamingCall call = inner.popLastCall(); + call.getController().getObserver().onResponse("value"); + call.getController().getObserver().onComplete(); + + f.get(); + verify(tracer).responseReceived(); + verify(tracer).operationSucceeded(); + + // afterResponse is the responsibility of BigtableTracerStreamingCallable + verify(tracer, never()).afterResponse(Mockito.anyLong()); + } +} diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/CheckAndMutateRowCallableTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/CheckAndMutateRowCallableTest.java deleted file mode 100644 index 5441f1d1f8..0000000000 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/CheckAndMutateRowCallableTest.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright 2018 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.bigtable.data.v2.stub; - -import static com.google.common.truth.Truth.assertThat; - -import com.google.api.core.ApiFuture; -import com.google.api.core.SettableApiFuture; -import com.google.api.gax.grpc.GrpcStatusCode; -import com.google.api.gax.rpc.ApiCallContext; -import com.google.api.gax.rpc.NotFoundException; -import com.google.api.gax.rpc.UnaryCallable; -import com.google.bigtable.v2.CheckAndMutateRowRequest; -import com.google.bigtable.v2.CheckAndMutateRowResponse; -import com.google.bigtable.v2.Mutation.DeleteFromRow; -import com.google.cloud.bigtable.data.v2.internal.NameUtil; -import com.google.cloud.bigtable.data.v2.internal.RequestContext; -import com.google.cloud.bigtable.data.v2.models.ConditionalRowMutation; -import com.google.cloud.bigtable.data.v2.models.Mutation; -import com.google.protobuf.ByteString; -import io.grpc.Status.Code; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@RunWith(JUnit4.class) -public class CheckAndMutateRowCallableTest { - - private final RequestContext requestContext = - RequestContext.create("my-project", "my-instance", "my-app-profile"); - private FakeCallable inner; - private CheckAndMutateRowCallable callable; - - @Before - public void setUp() { - inner = new FakeCallable(); - callable = new CheckAndMutateRowCallable(inner, requestContext); - } - - @Test - public void requestIsCorrect() { - callable.futureCall( - ConditionalRowMutation.create("my-table", "row-key").then(Mutation.create().deleteRow())); - - assertThat(inner.request) - .isEqualTo( - CheckAndMutateRowRequest.newBuilder() - .setTableName( - NameUtil.formatTableName( - requestContext.getProjectId(), requestContext.getInstanceId(), "my-table")) - .setRowKey(ByteString.copyFromUtf8("row-key")) - .setAppProfileId(requestContext.getAppProfileId()) - .addTrueMutations( - com.google.bigtable.v2.Mutation.newBuilder() - .setDeleteFromRow(DeleteFromRow.getDefaultInstance())) - .build()); - } - - @Test - public void responseCorrectlyTransformed() throws Exception { - ApiFuture result = - callable.futureCall( - ConditionalRowMutation.create("my-table", "row-key") - .then(Mutation.create().deleteRow())); - - inner.response.set(CheckAndMutateRowResponse.newBuilder().setPredicateMatched(true).build()); - - assertThat(result.get(1, TimeUnit.SECONDS)).isEqualTo(true); - } - - @Test - public void errorIsPropagated() throws Exception { - ApiFuture result = - callable.futureCall( - ConditionalRowMutation.create("my-table", "row-key") - .then(Mutation.create().deleteRow())); - - Throwable expectedError = - new NotFoundException("fake error", null, GrpcStatusCode.of(Code.NOT_FOUND), false); - inner.response.setException(expectedError); - - Throwable actualError = null; - try { - result.get(1, TimeUnit.SECONDS); - } catch (ExecutionException e) { - actualError = e.getCause(); - } - - assertThat(actualError).isEqualTo(expectedError); - } - - static class FakeCallable - extends UnaryCallable { - CheckAndMutateRowRequest request; - ApiCallContext callContext; - SettableApiFuture response = SettableApiFuture.create(); - - @Override - public ApiFuture futureCall( - CheckAndMutateRowRequest request, ApiCallContext context) { - this.request = request; - this.callContext = context; - - return response; - } - } -} diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettingsTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettingsTest.java index 5280abe1fd..fdc6b5717e 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettingsTest.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettingsTest.java @@ -961,6 +961,7 @@ public void enableRetryInfoFalseValueTest() throws IOException { "jwtAudienceMapping", "enableRoutingCookie", "enableRetryInfo", + "enableSkipTrailers", "readRowsSettings", "readRowSettings", "sampleRowKeysSettings", diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubTest.java index 50d086b711..495250fe13 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubTest.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubTest.java @@ -37,6 +37,7 @@ import com.google.api.gax.grpc.GaxGrpcProperties; import com.google.api.gax.grpc.GrpcCallContext; import com.google.api.gax.grpc.GrpcTransportChannel; +import com.google.api.gax.rpc.FailedPreconditionException; import com.google.api.gax.rpc.FixedTransportChannelProvider; import com.google.api.gax.rpc.InstantiatingWatchdogProvider; import com.google.api.gax.rpc.ServerStream; @@ -44,15 +45,21 @@ import com.google.api.gax.rpc.WatchdogTimeoutException; import com.google.auth.oauth2.ServiceAccountJwtAccessCredentials; import com.google.bigtable.v2.BigtableGrpc; +import com.google.bigtable.v2.CheckAndMutateRowRequest; +import com.google.bigtable.v2.CheckAndMutateRowResponse; import com.google.bigtable.v2.ExecuteQueryRequest; import com.google.bigtable.v2.ExecuteQueryResponse; import com.google.bigtable.v2.FeatureFlags; +import com.google.bigtable.v2.MutateRowRequest; +import com.google.bigtable.v2.MutateRowResponse; import com.google.bigtable.v2.MutateRowsRequest; import com.google.bigtable.v2.MutateRowsResponse; import com.google.bigtable.v2.PingAndWarmRequest; import com.google.bigtable.v2.PingAndWarmResponse; import com.google.bigtable.v2.ReadChangeStreamRequest; import com.google.bigtable.v2.ReadChangeStreamResponse; +import com.google.bigtable.v2.ReadModifyWriteRowRequest; +import com.google.bigtable.v2.ReadModifyWriteRowResponse; import com.google.bigtable.v2.ReadRowsRequest; import com.google.bigtable.v2.ReadRowsResponse; import com.google.bigtable.v2.RowSet; @@ -62,7 +69,19 @@ import com.google.cloud.bigtable.data.v2.FakeServiceBuilder; import com.google.cloud.bigtable.data.v2.internal.RequestContext; import com.google.cloud.bigtable.data.v2.internal.SqlRow; -import com.google.cloud.bigtable.data.v2.models.*; +import com.google.cloud.bigtable.data.v2.models.BulkMutation; +import com.google.cloud.bigtable.data.v2.models.ChangeStreamRecord; +import com.google.cloud.bigtable.data.v2.models.ConditionalRowMutation; +import com.google.cloud.bigtable.data.v2.models.DefaultRowAdapter; +import com.google.cloud.bigtable.data.v2.models.Filters; +import com.google.cloud.bigtable.data.v2.models.Mutation; +import com.google.cloud.bigtable.data.v2.models.Query; +import com.google.cloud.bigtable.data.v2.models.ReadChangeStreamQuery; +import com.google.cloud.bigtable.data.v2.models.ReadModifyWriteRow; +import com.google.cloud.bigtable.data.v2.models.Row; +import com.google.cloud.bigtable.data.v2.models.RowMutation; +import com.google.cloud.bigtable.data.v2.models.RowMutationEntry; +import com.google.cloud.bigtable.data.v2.models.TableId; import com.google.cloud.bigtable.data.v2.models.sql.ResultSetMetadata; import com.google.cloud.bigtable.data.v2.models.sql.Statement; import com.google.cloud.bigtable.data.v2.stub.sql.ExecuteQueryCallable; @@ -75,6 +94,7 @@ import com.google.protobuf.StringValue; import com.google.rpc.Code; import com.google.rpc.Status; +import io.grpc.CallOptions; import io.grpc.Context; import io.grpc.Deadline; import io.grpc.ManagedChannel; @@ -105,6 +125,7 @@ import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -118,8 +139,9 @@ public class EnhancedBigtableStubTest { private static final String PROJECT_ID = "fake-project"; private static final String INSTANCE_ID = "fake-instance"; + private static final String TABLE_ID = "fake-table"; private static final String TABLE_NAME = - NameUtil.formatTableName(PROJECT_ID, INSTANCE_ID, "fake-table"); + NameUtil.formatTableName(PROJECT_ID, INSTANCE_ID, TABLE_ID); private static final String APP_PROFILE_ID = "app-profile-id"; private static final String WAIT_TIME_TABLE_ID = "test-wait-timeout"; private static final String WAIT_TIME_QUERY = "test-wait-timeout"; @@ -269,6 +291,101 @@ public void testFeatureFlags() throws InterruptedException, IOException, Executi assertThat(featureFlags.getLastScannedRowResponses()).isTrue(); } + @Test + public void testCheckAndMutateRequestResponseConversion() + throws ExecutionException, InterruptedException { + ConditionalRowMutation req = + ConditionalRowMutation.create(TableId.of("my-table"), "my-key") + .condition(Filters.FILTERS.pass()) + .then(Mutation.create().deleteRow()); + + ApiFuture f = enhancedBigtableStub.checkAndMutateRowCallable().futureCall(req, null); + f.get(); + + CheckAndMutateRowRequest protoReq = + fakeDataService.checkAndMutateRowRequests.poll(1, TimeUnit.SECONDS); + assertThat(protoReq) + .isEqualTo(req.toProto(RequestContext.create(PROJECT_ID, INSTANCE_ID, APP_PROFILE_ID))); + assertThat(f.get()).isEqualTo(true); + } + + @Test + public void testRMWRequestResponseConversion() throws ExecutionException, InterruptedException { + ReadModifyWriteRow req = + ReadModifyWriteRow.create(TableId.of("my-table"), "my-key").append("f", "q", "v"); + + ApiFuture f = enhancedBigtableStub.readModifyWriteRowCallable().futureCall(req, null); + f.get(); + + ReadModifyWriteRowRequest protoReq = fakeDataService.rmwRequests.poll(1, TimeUnit.SECONDS); + assertThat(protoReq) + .isEqualTo(req.toProto(RequestContext.create(PROJECT_ID, INSTANCE_ID, APP_PROFILE_ID))); + assertThat(f.get().getKey()).isEqualTo(ByteString.copyFromUtf8("my-key")); + } + + @Test + public void testMutateRowRequestResponseConversion() + throws ExecutionException, InterruptedException { + RowMutation req = RowMutation.create(TableId.of("my-table"), "my-key").deleteRow(); + CallOptions.Key testKey = CallOptions.Key.create("test-key"); + + GrpcCallContext ctx = + GrpcCallContext.createDefault() + .withCallOptions(CallOptions.DEFAULT.withOption(testKey, "callopt-value")); + ApiFuture f = enhancedBigtableStub.mutateRowCallable().futureCall(req, ctx); + f.get(); + + MutateRowRequest protoReq = fakeDataService.mutateRowRequests.poll(1, TimeUnit.SECONDS); + assertThat(protoReq) + .isEqualTo(req.toProto(RequestContext.create(PROJECT_ID, INSTANCE_ID, APP_PROFILE_ID))); + assertThat(f.get()).isEqualTo(null); + } + + @Test + public void testMutateRowRequestParams() throws ExecutionException, InterruptedException { + RowMutation req = RowMutation.create(TableId.of(TABLE_ID), "my-key").deleteRow(); + + ApiFuture f = enhancedBigtableStub.mutateRowCallable().futureCall(req, null); + f.get(); + + Metadata reqMetadata = metadataInterceptor.headers.poll(1, TimeUnit.SECONDS); + + // RequestParamsExtractor + String reqParams = + reqMetadata.get(Key.of("x-goog-request-params", Metadata.ASCII_STRING_MARSHALLER)); + assertThat(reqParams).contains("table_name=" + TABLE_NAME.replace("/", "%2F")); + assertThat(reqParams).contains(String.format("app_profile_id=%s", APP_PROFILE_ID)); + + // StatsHeadersUnaryCallable + assertThat(reqMetadata.keys()).contains("bigtable-client-attempt-epoch-usec"); + + assertThat(f.get()).isEqualTo(null); + } + + @Test + public void testMutateRowErrorPropagation() { + AtomicInteger invocationCount = new AtomicInteger(); + Mockito.doAnswer( + invocationOnMock -> { + StreamObserver observer = invocationOnMock.getArgument(1); + if (invocationCount.getAndIncrement() == 0) { + observer.onError(io.grpc.Status.UNAVAILABLE.asRuntimeException()); + } else { + observer.onError(io.grpc.Status.FAILED_PRECONDITION.asRuntimeException()); + } + return null; + }) + .when(fakeDataService) + .mutateRow(Mockito.any(), Mockito.any(StreamObserver.class)); + + RowMutation req = RowMutation.create(TableId.of(TABLE_ID), "my-key").deleteRow(); + ApiFuture f = enhancedBigtableStub.mutateRowCallable().futureCall(req, null); + + ExecutionException e = assertThrows(ExecutionException.class, f::get); + assertThat(e.getCause()).isInstanceOf(FailedPreconditionException.class); + assertThat(invocationCount.get()).isEqualTo(2); + } + @Test public void testCreateReadRowsCallable() throws InterruptedException { ServerStreamingCallable streamingCallable = @@ -751,6 +868,10 @@ private static class FakeDataService extends BigtableGrpc.BigtableImplBase { Queues.newLinkedBlockingDeque(); final BlockingQueue pingRequests = Queues.newLinkedBlockingDeque(); final BlockingQueue executeQueryRequests = Queues.newLinkedBlockingDeque(); + final BlockingQueue mutateRowRequests = Queues.newLinkedBlockingDeque(); + final BlockingQueue checkAndMutateRowRequests = + Queues.newLinkedBlockingDeque(); + final BlockingQueue rmwRequests = Queues.newLinkedBlockingDeque(); @SuppressWarnings("unchecked") ReadRowsRequest popLastRequest() throws InterruptedException { @@ -761,6 +882,37 @@ ExecuteQueryRequest popLastExecuteQueryRequest() throws InterruptedException { return executeQueryRequests.poll(1, TimeUnit.SECONDS); } + @Override + public void mutateRow( + MutateRowRequest request, StreamObserver responseObserver) { + mutateRowRequests.add(request); + + responseObserver.onNext(MutateRowResponse.getDefaultInstance()); + responseObserver.onCompleted(); + } + + @Override + public void checkAndMutateRow( + CheckAndMutateRowRequest request, + StreamObserver responseObserver) { + checkAndMutateRowRequests.add(request); + responseObserver.onNext( + CheckAndMutateRowResponse.newBuilder().setPredicateMatched(true).build()); + responseObserver.onCompleted(); + } + + @Override + public void readModifyWriteRow( + ReadModifyWriteRowRequest request, + StreamObserver responseObserver) { + rmwRequests.add(request); + responseObserver.onNext( + ReadModifyWriteRowResponse.newBuilder() + .setRow(com.google.bigtable.v2.Row.newBuilder().setKey(request.getRowKey())) + .build()); + responseObserver.onCompleted(); + } + @Override public void mutateRows( MutateRowsRequest request, StreamObserver responseObserver) { diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/MutateRowCallableTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/MutateRowCallableTest.java deleted file mode 100644 index 4792b66890..0000000000 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/MutateRowCallableTest.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2018 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.bigtable.data.v2.stub; - -import com.google.api.core.SettableApiFuture; -import com.google.api.gax.rpc.UnaryCallable; -import com.google.bigtable.v2.MutateRowRequest; -import com.google.bigtable.v2.MutateRowResponse; -import com.google.cloud.bigtable.data.v2.internal.RequestContext; -import com.google.cloud.bigtable.data.v2.models.RowMutation; -import com.google.common.primitives.Longs; -import com.google.common.truth.Truth; -import com.google.protobuf.ByteString; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; -import org.mockito.ArgumentCaptor; -import org.mockito.Mockito; - -@RunWith(JUnit4.class) -public class MutateRowCallableTest { - - private static final RequestContext REQUEST_CONTEXT = - RequestContext.create("fake-project", "fake-instance", "fake-profile"); - private UnaryCallable innerCallable; - private ArgumentCaptor innerMutation; - private SettableApiFuture innerResult; - - @SuppressWarnings("unchecked") - @Before - public void setUp() { - innerCallable = Mockito.mock(UnaryCallable.class); - innerMutation = ArgumentCaptor.forClass(MutateRowRequest.class); - innerResult = SettableApiFuture.create(); - Mockito.when(innerCallable.futureCall(innerMutation.capture(), Mockito.any())) - .thenReturn(innerResult); - } - - @Test - public void testRequestConversion() { - MutateRowCallable callable = new MutateRowCallable(innerCallable, REQUEST_CONTEXT); - RowMutation outerRequest = - RowMutation.create("fake-table", "fake-key") - .setCell("fake-family", "fake-qualifier", 1_000, "fake-value") - .addToCell("family-2", "qualifier", 1_000, 1234) - .mergeToCell( - "family-2", "qualifier2", 1_000, ByteString.copyFrom(Longs.toByteArray(1234L))); - - innerResult.set(MutateRowResponse.getDefaultInstance()); - callable.call(outerRequest); - - Truth.assertThat(innerMutation.getValue()).isEqualTo(outerRequest.toProto(REQUEST_CONTEXT)); - } -} diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/ReadModifyWriteRowCallableTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/ReadModifyWriteRowCallableTest.java deleted file mode 100644 index 4a8f857d05..0000000000 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/ReadModifyWriteRowCallableTest.java +++ /dev/null @@ -1,200 +0,0 @@ -/* - * Copyright 2018 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.bigtable.data.v2.stub; - -import static com.google.common.truth.Truth.assertThat; - -import com.google.api.core.ApiFuture; -import com.google.api.core.SettableApiFuture; -import com.google.api.gax.grpc.GrpcStatusCode; -import com.google.api.gax.rpc.ApiCallContext; -import com.google.api.gax.rpc.NotFoundException; -import com.google.api.gax.rpc.UnaryCallable; -import com.google.bigtable.v2.Cell; -import com.google.bigtable.v2.Column; -import com.google.bigtable.v2.Family; -import com.google.bigtable.v2.ReadModifyWriteRowRequest; -import com.google.bigtable.v2.ReadModifyWriteRowResponse; -import com.google.bigtable.v2.ReadModifyWriteRule; -import com.google.cloud.bigtable.data.v2.internal.NameUtil; -import com.google.cloud.bigtable.data.v2.internal.RequestContext; -import com.google.cloud.bigtable.data.v2.models.ReadModifyWriteRow; -import com.google.cloud.bigtable.data.v2.models.Row; -import com.google.cloud.bigtable.data.v2.models.RowCell; -import com.google.common.collect.ImmutableList; -import com.google.protobuf.ByteString; -import io.grpc.Status.Code; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@RunWith(JUnit4.class) -public class ReadModifyWriteRowCallableTest { - private final RequestContext requestContext = - RequestContext.create("fake-project", "fake-instance", "fake-profile"); - private FakeCallable inner; - private ReadModifyWriteRowCallable callable; - - @Before - public void setUp() { - inner = new FakeCallable(); - callable = new ReadModifyWriteRowCallable(inner, requestContext); - } - - @Test - public void requestIsCorrect() { - callable.futureCall( - ReadModifyWriteRow.create("my-table", "my-key").append("my-family", "", "suffix")); - - assertThat(inner.request) - .isEqualTo( - ReadModifyWriteRowRequest.newBuilder() - .setTableName( - NameUtil.formatTableName( - requestContext.getProjectId(), requestContext.getInstanceId(), "my-table")) - .setAppProfileId(requestContext.getAppProfileId()) - .setRowKey(ByteString.copyFromUtf8("my-key")) - .addRules( - ReadModifyWriteRule.newBuilder() - .setFamilyName("my-family") - .setColumnQualifier(ByteString.EMPTY) - .setAppendValue(ByteString.copyFromUtf8("suffix"))) - .build()); - } - - @Test - public void responseCorrectlyTransformed() throws Exception { - ApiFuture result = - callable.futureCall( - ReadModifyWriteRow.create("my-table", "my-key").append("my-family", "col", "suffix")); - - inner.response.set( - ReadModifyWriteRowResponse.newBuilder() - .setRow( - com.google.bigtable.v2.Row.newBuilder() - .setKey(ByteString.copyFromUtf8("my-key")) - .addFamilies( - Family.newBuilder() - .setName("my-family") - .addColumns( - Column.newBuilder() - .setQualifier(ByteString.copyFromUtf8("col")) - .addCells( - Cell.newBuilder() - .setTimestampMicros(1_000) - .setValue(ByteString.copyFromUtf8("suffix")))))) - .build()); - - assertThat(result.get(1, TimeUnit.SECONDS)) - .isEqualTo( - Row.create( - ByteString.copyFromUtf8("my-key"), - ImmutableList.of( - RowCell.create( - "my-family", - ByteString.copyFromUtf8("col"), - 1_000, - ImmutableList.of(), - ByteString.copyFromUtf8("suffix"))))); - } - - @Test - public void responseSortsFamilies() throws Exception { - ByteString col = ByteString.copyFromUtf8("col1"); - ByteString value1 = ByteString.copyFromUtf8("value1"); - ByteString value2 = ByteString.copyFromUtf8("value2"); - - ApiFuture result = - callable.futureCall( - ReadModifyWriteRow.create("my-table", "my-key").append("my-family", "col", "suffix")); - - inner.response.set( - ReadModifyWriteRowResponse.newBuilder() - .setRow( - com.google.bigtable.v2.Row.newBuilder() - .setKey(ByteString.copyFromUtf8("my-key")) - // family2 is out of order - .addFamilies( - Family.newBuilder() - .setName("family2") - .addColumns( - Column.newBuilder() - .setQualifier(col) - .addCells( - Cell.newBuilder() - .setTimestampMicros(1_000) - .setValue(value2)))) - .addFamilies( - Family.newBuilder() - .setName("family1") - .addColumns( - Column.newBuilder() - .setQualifier(col) - .addCells( - Cell.newBuilder() - .setTimestampMicros(1_000) - .setValue(value1))) - .build())) - .build()); - - assertThat(result.get(1, TimeUnit.SECONDS)) - .isEqualTo( - Row.create( - ByteString.copyFromUtf8("my-key"), - ImmutableList.of( - RowCell.create("family1", col, 1_000, ImmutableList.of(), value1), - RowCell.create("family2", col, 1_000, ImmutableList.of(), value2)))); - } - - @Test - public void errorIsPropagated() throws Exception { - ApiFuture result = - callable.futureCall( - ReadModifyWriteRow.create("my-table", "my-key").append("my-family", "", "suffix")); - - Throwable expectedError = - new NotFoundException("fake error", null, GrpcStatusCode.of(Code.NOT_FOUND), false); - inner.response.setException(expectedError); - - Throwable actualError = null; - try { - result.get(1, TimeUnit.SECONDS); - } catch (ExecutionException e) { - actualError = e.getCause(); - } - - assertThat(actualError).isEqualTo(expectedError); - } - - static class FakeCallable - extends UnaryCallable { - ReadModifyWriteRowRequest request; - ApiCallContext callContext; - SettableApiFuture response = SettableApiFuture.create(); - - @Override - public ApiFuture futureCall( - ReadModifyWriteRowRequest request, ApiCallContext context) { - this.request = request; - this.callContext = context; - - return response; - } - } -} diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/SampleRowKeysCallableTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/SampleRowKeysCallableTest.java deleted file mode 100644 index 40a30d3263..0000000000 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/SampleRowKeysCallableTest.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright 2018 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.bigtable.data.v2.stub; - -import static com.google.common.truth.Truth.assertThat; - -import com.google.api.core.ApiFuture; -import com.google.api.core.SettableApiFuture; -import com.google.api.gax.grpc.GrpcStatusCode; -import com.google.api.gax.rpc.ApiCallContext; -import com.google.api.gax.rpc.NotFoundException; -import com.google.api.gax.rpc.UnaryCallable; -import com.google.bigtable.v2.SampleRowKeysRequest; -import com.google.bigtable.v2.SampleRowKeysResponse; -import com.google.cloud.bigtable.data.v2.internal.NameUtil; -import com.google.cloud.bigtable.data.v2.internal.RequestContext; -import com.google.cloud.bigtable.data.v2.models.KeyOffset; -import com.google.common.collect.ImmutableList; -import com.google.protobuf.ByteString; -import io.grpc.Status.Code; -import java.util.List; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@RunWith(JUnit4.class) -public class SampleRowKeysCallableTest { - - private final RequestContext requestContext = - RequestContext.create("my-project", "my-instance", "my-profile"); - private FakeCallable inner; - private SampleRowKeysCallable callable; - - @Before - public void setUp() { - inner = new FakeCallable(); - callable = new SampleRowKeysCallable(inner, requestContext); - } - - @Test - public void requestIsCorrect() { - callable.futureCall("my-table"); - - assertThat(inner.request) - .isEqualTo( - SampleRowKeysRequest.newBuilder() - .setTableName( - NameUtil.formatTableName( - requestContext.getProjectId(), requestContext.getInstanceId(), "my-table")) - .setAppProfileId(requestContext.getAppProfileId()) - .build()); - } - - @Test - public void responseCorrectlyTransformed() throws Exception { - ApiFuture> result = callable.futureCall("my-table"); - - inner.response.set( - ImmutableList.of( - SampleRowKeysResponse.newBuilder() - .setRowKey(ByteString.copyFromUtf8("key1")) - .setOffsetBytes(100) - .build(), - SampleRowKeysResponse.newBuilder() - .setRowKey(ByteString.copyFromUtf8("")) - .setOffsetBytes(1000) - .build())); - - assertThat(result.get(1, TimeUnit.SECONDS)) - .isEqualTo( - ImmutableList.of( - KeyOffset.create(ByteString.copyFromUtf8("key1"), 100), - KeyOffset.create(ByteString.EMPTY, 1000))); - } - - @Test - public void errorIsPropagated() throws Exception { - ApiFuture> result = callable.futureCall("my-table"); - - Throwable expectedError = - new NotFoundException("fake error", null, GrpcStatusCode.of(Code.NOT_FOUND), false); - inner.response.setException(expectedError); - - Throwable actualError = null; - try { - result.get(1, TimeUnit.SECONDS); - } catch (ExecutionException e) { - actualError = e.getCause(); - } - - assertThat(actualError).isEqualTo(expectedError); - } - - static class FakeCallable - extends UnaryCallable> { - SampleRowKeysRequest request; - ApiCallContext callContext; - SettableApiFuture> response = SettableApiFuture.create(); - - @Override - public ApiFuture> futureCall( - SampleRowKeysRequest request, ApiCallContext context) { - this.request = request; - this.callContext = context; - - return response; - } - } -} diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/SkipTrailersTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/SkipTrailersTest.java new file mode 100644 index 0000000000..07ac7deee4 --- /dev/null +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/SkipTrailersTest.java @@ -0,0 +1,249 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.bigtable.data.v2.stub; + +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.api.core.ApiFuture; +import com.google.api.gax.core.NoCredentialsProvider; +import com.google.api.gax.tracing.ApiTracer; +import com.google.api.gax.tracing.ApiTracerFactory; +import com.google.auto.value.AutoValue; +import com.google.bigtable.v2.BigtableGrpc; +import com.google.bigtable.v2.CheckAndMutateRowResponse; +import com.google.bigtable.v2.MutateRowResponse; +import com.google.bigtable.v2.PingAndWarmResponse; +import com.google.bigtable.v2.ReadModifyWriteRowResponse; +import com.google.bigtable.v2.ReadRowsResponse; +import com.google.bigtable.v2.Row; +import com.google.cloud.bigtable.data.v2.BigtableDataClient; +import com.google.cloud.bigtable.data.v2.BigtableDataSettings; +import com.google.cloud.bigtable.data.v2.FakeServiceBuilder; +import com.google.cloud.bigtable.data.v2.models.ConditionalRowMutation; +import com.google.cloud.bigtable.data.v2.models.Filters; +import com.google.cloud.bigtable.data.v2.models.Mutation; +import com.google.cloud.bigtable.data.v2.models.ReadModifyWriteRow; +import com.google.cloud.bigtable.data.v2.models.RowMutation; +import com.google.cloud.bigtable.data.v2.models.TableId; +import com.google.cloud.bigtable.data.v2.models.TargetId; +import com.google.cloud.bigtable.data.v2.stub.metrics.BigtableTracer; +import com.google.common.base.Preconditions; +import com.google.common.base.Supplier; +import com.google.common.collect.ImmutableList; +import com.google.protobuf.ByteString; +import com.google.protobuf.BytesValue; +import com.google.protobuf.GeneratedMessageV3; +import com.google.protobuf.StringValue; +import io.grpc.BindableService; +import io.grpc.MethodDescriptor; +import io.grpc.Server; +import io.grpc.ServerServiceDefinition; +import io.grpc.stub.ServerCalls; +import io.grpc.stub.StreamObserver; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.exceptions.verification.WantedButNotInvoked; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +@RunWith(JUnit4.class) +public class SkipTrailersTest { + @Rule public final MockitoRule mockitoRule = MockitoJUnit.rule(); + + private static final String PROJECT_ID = "fake-project"; + private static final String INSTANCE_ID = "fake-instance"; + private static final TargetId TABLE_ID = TableId.of("fake-table"); + + private HackedBigtableService hackedService; + private Server server; + + @Mock private ApiTracerFactory tracerFactory; + @Mock private BigtableTracer tracer; + + private BigtableDataClient client; + + @Before + public void setUp() throws Exception { + hackedService = new HackedBigtableService(); + server = FakeServiceBuilder.create(hackedService).start(); + + when(tracerFactory.newTracer(Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(tracer); + when(tracer.inScope()).thenReturn(Mockito.mock(ApiTracer.Scope.class)); + + BigtableDataSettings.Builder clientBuilder = + BigtableDataSettings.newBuilderForEmulator(server.getPort()) + .setProjectId(PROJECT_ID) + .setInstanceId(INSTANCE_ID) + .setCredentialsProvider(NoCredentialsProvider.create()); + clientBuilder.stubSettings().setEnableSkipTrailers(true).setTracerFactory(tracerFactory); + + client = BigtableDataClient.create(clientBuilder.build()); + } + + @After + public void tearDown() throws Exception { + client.close(); + server.shutdown(); + } + + @Test + public void testReadRow() throws InterruptedException, ExecutionException { + ReadRowsResponse fakeResponse = + ReadRowsResponse.newBuilder() + .addChunks( + ReadRowsResponse.CellChunk.newBuilder() + .setRowKey(ByteString.copyFromUtf8("fake-key")) + .setFamilyName(StringValue.newBuilder().setValue("cf")) + .setQualifier(BytesValue.newBuilder().setValue(ByteString.copyFromUtf8("q"))) + .setTimestampMicros(0) + .setValue(ByteString.copyFromUtf8("value")) + .setCommitRow(true)) + .build(); + test(() -> client.readRowAsync(TABLE_ID, "fake-key"), fakeResponse); + } + + @Test + public void testMutateRow() throws ExecutionException, InterruptedException { + test( + () -> client.mutateRowAsync(RowMutation.create(TABLE_ID, "fake-key")), + MutateRowResponse.getDefaultInstance()); + } + + @Test + public void testCheckAndMutateRow() throws ExecutionException, InterruptedException { + ConditionalRowMutation req = + ConditionalRowMutation.create(TABLE_ID, "fake-key") + .condition(Filters.FILTERS.pass()) + .then(Mutation.create().deleteRow()); + test(() -> client.checkAndMutateRowAsync(req), CheckAndMutateRowResponse.getDefaultInstance()); + } + + @Test + public void testRMW() throws ExecutionException, InterruptedException { + ReadModifyWriteRow req = ReadModifyWriteRow.create(TABLE_ID, "fake-key").append("cf", "q", "A"); + test( + () -> client.readModifyWriteRowAsync(req), + ReadModifyWriteRowResponse.newBuilder().setRow(Row.getDefaultInstance()).build()); + } + + private void test(Supplier> invoker, T fakeResponse) + throws InterruptedException, ExecutionException { + ApiFuture future = invoker.get(); + + // Wait for the call to start on the server + @SuppressWarnings("unchecked") + ServerRpc rpc = (ServerRpc) hackedService.rpcs.poll(10, TimeUnit.SECONDS); + Preconditions.checkNotNull( + rpc, "Timed out waiting for the call to be received by the mock server"); + + // Send the only row + rpc.getResponseStream().onNext(fakeResponse); + + // Ensure that the future resolves and does not throw an error + try { + future.get(1, TimeUnit.MINUTES); + } catch (TimeoutException e) { + Assert.fail("timed out waiting for the trailer optimization future to resolve"); + } + + verify(tracer, times(1)).operationFinishEarly(); + verify(tracer, never()).operationSucceeded(); + + // clean up + rpc.getResponseStream().onCompleted(); + + // Ensure that the tracer is invoked after the internal operation is complete + // Since we dont have a way to know exactly when this happens, we poll + for (int i = 10; i > 0; i--) { + try { + verify(tracer, times(1)).operationSucceeded(); + break; + } catch (WantedButNotInvoked e) { + if (i > 1) { + Thread.sleep(100); + } else { + throw e; + } + } + } + } + + /** + * Hack the srvice definition to allow grpc server to simulate delayed trailers. This will augment + * the bigtable service definition to promote unary rpcs to server streaming + */ + class HackedBigtableService implements BindableService { + private final LinkedBlockingDeque> rpcs = new LinkedBlockingDeque<>(); + + @Override + public ServerServiceDefinition bindService() { + ServerServiceDefinition.Builder builder = + ServerServiceDefinition.builder(BigtableGrpc.SERVICE_NAME) + .addMethod( + BigtableGrpc.getPingAndWarmMethod(), + ServerCalls.asyncUnaryCall( + (ignored, observer) -> { + observer.onNext(PingAndWarmResponse.getDefaultInstance()); + observer.onCompleted(); + })) + .addMethod( + BigtableGrpc.getReadRowsMethod(), + ServerCalls.asyncServerStreamingCall( + (req, observer) -> rpcs.add(ServerRpc.create(req, observer)))); + ImmutableList> + unaryDescriptors = + ImmutableList.of( + BigtableGrpc.getMutateRowMethod(), + BigtableGrpc.getCheckAndMutateRowMethod(), + BigtableGrpc.getReadModifyWriteRowMethod()); + + for (MethodDescriptor desc : + unaryDescriptors) { + builder.addMethod( + desc.toBuilder().setType(MethodDescriptor.MethodType.SERVER_STREAMING).build(), + ServerCalls.asyncServerStreamingCall( + (req, observer) -> rpcs.add(ServerRpc.create(req, observer)))); + } + return builder.build(); + } + } + + @AutoValue + abstract static class ServerRpc { + abstract ReqT getRequest(); + + abstract StreamObserver getResponseStream(); + + static ServerRpc create(ReqT req, StreamObserver resp) { + // return new AutoValue__(req, resp); + return new AutoValue_SkipTrailersTest_ServerRpc<>(req, resp); + } + } +} diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/TransformingServerStreamingCallableTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/TransformingServerStreamingCallableTest.java new file mode 100644 index 0000000000..856d732f5c --- /dev/null +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/TransformingServerStreamingCallableTest.java @@ -0,0 +1,74 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.bigtable.data.v2.stub; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.cloud.bigtable.gaxx.testing.MockStreamingApi.MockResponseObserver; +import com.google.cloud.bigtable.gaxx.testing.MockStreamingApi.MockServerStreamingCall; +import com.google.cloud.bigtable.gaxx.testing.MockStreamingApi.MockServerStreamingCallable; +import com.google.common.base.Functions; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class TransformingServerStreamingCallableTest { + @Test + public void testReqTransform() { + MockServerStreamingCallable inner = new MockServerStreamingCallable<>(); + TransformingServerStreamingCallable xform = + new TransformingServerStreamingCallable<>(inner, Object::toString, Functions.identity()); + + MockResponseObserver responseObserver = new MockResponseObserver<>(true); + xform.call(37, responseObserver); + + MockServerStreamingCall call = inner.popLastCall(); + assertThat(call.getRequest()).isEqualTo("37"); + } + + @Test + public void testRespTransform() { + MockServerStreamingCallable inner = new MockServerStreamingCallable<>(); + TransformingServerStreamingCallable xform = + new TransformingServerStreamingCallable<>(inner, Functions.identity(), Integer::parseInt); + + MockResponseObserver outerObserver = new MockResponseObserver<>(true); + xform.call("req", outerObserver); + + MockServerStreamingCall call = inner.popLastCall(); + call.getController().getObserver().onResponse("37"); + + assertThat(outerObserver.popNextResponse()).isEqualTo(37); + } + + @Test + public void testError() { + MockServerStreamingCallable inner = new MockServerStreamingCallable<>(); + TransformingServerStreamingCallable xform = + new TransformingServerStreamingCallable<>( + inner, Functions.identity(), Functions.identity()); + + MockResponseObserver outerObserver = new MockResponseObserver<>(true); + xform.call("req", outerObserver); + + MockServerStreamingCall call = inner.popLastCall(); + RuntimeException e = new RuntimeException("fake error"); + call.getController().getObserver().onError(e); + + assertThat(outerObserver.getFinalError()).isEqualTo(e); + } +} diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracerTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracerTest.java index e5e6cee867..ada734d460 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracerTest.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracerTest.java @@ -24,6 +24,7 @@ import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.FIRST_RESPONSE_LATENCIES_NAME; import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.METHOD_KEY; import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.OPERATION_LATENCIES_NAME; +import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.REMAINING_DEADLINE_NAME; import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.RETRY_COUNT_NAME; import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.SERVER_LATENCIES_NAME; import static com.google.cloud.bigtable.data.v2.stub.metrics.BuiltinMetricsConstants.STATUS_KEY; @@ -90,11 +91,13 @@ import io.opentelemetry.sdk.metrics.SdkMeterProvider; import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder; import io.opentelemetry.sdk.metrics.View; +import io.opentelemetry.sdk.metrics.data.HistogramPointData; import io.opentelemetry.sdk.metrics.data.MetricData; import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader; import java.io.IOException; import java.net.SocketAddress; import java.nio.charset.Charset; +import java.time.Duration; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; @@ -103,6 +106,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; import javax.annotation.Nullable; import org.junit.After; import org.junit.Assert; @@ -213,6 +217,17 @@ public void sendHeaders(Metadata headers) { .retrySettings() .setInitialRetryDelayDuration(java.time.Duration.ofMillis(200)); + stubSettingsBuilder + .readRowsSettings() + .retrySettings() + .setTotalTimeoutDuration(Duration.ofMillis(9000)) + .setMaxRpcTimeoutDuration(Duration.ofMillis(6000)) + .setRpcTimeoutMultiplier(1) + .setInitialRpcTimeoutDuration(Duration.ofMillis(6000)) + .setInitialRetryDelayDuration(Duration.ofMillis(10)) + .setRetryDelayMultiplier(1) + .setMaxRetryDelayDuration(Duration.ofMillis(10)); + stubSettingsBuilder .bulkMutateRowsSettings() .setBatchingSettings( @@ -750,6 +765,53 @@ public void testPermanentFailure() { verifyAttributes(opLatency, expected); } + @Test + public void testRemainingDeadline() { + stub.readRowsCallable().all().call(Query.create(TABLE)); + MetricData deadlineMetric = getMetricData(metricReader, REMAINING_DEADLINE_NAME); + + Attributes retryAttributes = + baseAttributes + .toBuilder() + .put(STATUS_KEY, "UNAVAILABLE") + .put(TABLE_ID_KEY, TABLE) + .put(METHOD_KEY, "Bigtable.ReadRows") + .put(ZONE_ID_KEY, "global") + .put(CLUSTER_ID_KEY, "unspecified") + .put(STREAMING_KEY, true) + .put(CLIENT_NAME_KEY, CLIENT_NAME) + .build(); + HistogramPointData retryHistogramPointData = + deadlineMetric.getHistogramData().getPoints().stream() + .filter(pd -> pd.getAttributes().equals(retryAttributes)) + .collect(Collectors.toList()) + .get(0); + + double retryRemainingDeadline = retryHistogramPointData.getSum(); + // The retry remaining deadline should be equivalent to the original timeout. + assertThat(retryRemainingDeadline).isEqualTo(9000); + + Attributes okAttributes = + baseAttributes + .toBuilder() + .put(STATUS_KEY, "OK") + .put(TABLE_ID_KEY, TABLE) + .put(ZONE_ID_KEY, ZONE) + .put(CLUSTER_ID_KEY, CLUSTER) + .put(METHOD_KEY, "Bigtable.ReadRows") + .put(STREAMING_KEY, true) + .put(CLIENT_NAME_KEY, CLIENT_NAME) + .build(); + HistogramPointData okHistogramPointData = + deadlineMetric.getHistogramData().getPoints().stream() + .filter(pd -> pd.getAttributes().equals(okAttributes)) + .collect(Collectors.toList()) + .get(0); + + double okRemainingDeadline = okHistogramPointData.getSum(); + assertThat(okRemainingDeadline).isWithin(200).of(8500); + } + private static class FakeService extends BigtableGrpc.BigtableImplBase { static List createFakeResponse() { diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/sql/ExecuteQueryCallableTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/sql/ExecuteQueryCallableTest.java index deedfbaba1..1ddac33720 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/sql/ExecuteQueryCallableTest.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/sql/ExecuteQueryCallableTest.java @@ -124,15 +124,16 @@ public void testExecuteQueryRequestsIgnoreOverriddenMaxAttempts() throws IOExcep .stubSettings() .executeQuerySettings() .setRetrySettings(RetrySettings.newBuilder().setMaxAttempts(10).build()); - EnhancedBigtableStub overrideStub = - EnhancedBigtableStub.create(overrideSettings.build().getStubSettings()); - SqlServerStream stream = - overrideStub.executeQueryCallable().call(Statement.of("SELECT * FROM table")); - Iterator iterator = stream.rows().iterator(); + try (EnhancedBigtableStub overrideStub = + EnhancedBigtableStub.create(overrideSettings.build().getStubSettings())) { + SqlServerStream stream = + overrideStub.executeQueryCallable().call(Statement.of("SELECT * FROM table")); + Iterator iterator = stream.rows().iterator(); - assertThrows(UnavailableException.class, iterator::next).getCause(); - assertThat(fakeService.attempts).isEqualTo(1); + assertThrows(UnavailableException.class, iterator::next).getCause(); + assertThat(fakeService.attempts).isEqualTo(1); + } } @Test @@ -160,13 +161,15 @@ public void testExecuteQueryRequestsRespectDeadline() throws IOException { .setInitialRpcTimeout(Duration.ofMillis(10)) .setMaxRpcTimeout(Duration.ofMillis(10)) .build()); - EnhancedBigtableStub overrideDeadline = - EnhancedBigtableStub.create(overrideSettings.build().getStubSettings()); - SqlServerStream streamOverride = - overrideDeadline.executeQueryCallable().call(Statement.of("SELECT * FROM table")); - Iterator overrideIterator = streamOverride.rows().iterator(); - // We don't care about this but are reusing the fake service that tests retries - assertThrows(DeadlineExceededException.class, overrideIterator::next).getCause(); + + try (EnhancedBigtableStub overrideDeadline = + EnhancedBigtableStub.create(overrideSettings.build().getStubSettings())) { + SqlServerStream streamOverride = + overrideDeadline.executeQueryCallable().call(Statement.of("SELECT * FROM table")); + Iterator overrideIterator = streamOverride.rows().iterator(); + // We don't care about this but are reusing the fake service that tests retries + assertThrows(DeadlineExceededException.class, overrideIterator::next).getCause(); + } } private static class FakeService extends BigtableGrpc.BigtableImplBase { diff --git a/google-cloud-bigtable/src/test/resources/logging.properties b/google-cloud-bigtable/src/test/resources/logging.properties index 70319867bf..e181b45a01 100644 --- a/google-cloud-bigtable/src/test/resources/logging.properties +++ b/google-cloud-bigtable/src/test/resources/logging.properties @@ -7,6 +7,6 @@ java.util.logging.ConsoleHandler.level = INFO java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter # time [level] loggerName: message -java.util.logging.SimpleFormatter.format=%1$tT [%4$-7s] %2$s: %5$s%n +java.util.logging.SimpleFormatter.format=%1$tT [%4$-7s] %2$s: %5$s %6$s%n diff --git a/grpc-google-cloud-bigtable-admin-v2/pom.xml b/grpc-google-cloud-bigtable-admin-v2/pom.xml index f88f750858..dd5d1168d4 100644 --- a/grpc-google-cloud-bigtable-admin-v2/pom.xml +++ b/grpc-google-cloud-bigtable-admin-v2/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc grpc-google-cloud-bigtable-admin-v2 - 2.45.1 + 2.46.1-SNAPSHOT grpc-google-cloud-bigtable-admin-v2 GRPC library for grpc-google-cloud-bigtable-admin-v2 com.google.cloud google-cloud-bigtable-parent - 2.45.1 + 2.46.1-SNAPSHOT @@ -18,14 +18,14 @@ com.google.cloud google-cloud-bigtable-deps-bom - 2.45.1 + 2.46.1-SNAPSHOT pom import com.google.cloud google-cloud-bigtable-bom - 2.45.1 + 2.46.1-SNAPSHOT pom import diff --git a/grpc-google-cloud-bigtable-v2/pom.xml b/grpc-google-cloud-bigtable-v2/pom.xml index c93286716f..c415de9996 100644 --- a/grpc-google-cloud-bigtable-v2/pom.xml +++ b/grpc-google-cloud-bigtable-v2/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc grpc-google-cloud-bigtable-v2 - 2.45.1 + 2.46.1-SNAPSHOT grpc-google-cloud-bigtable-v2 GRPC library for grpc-google-cloud-bigtable-v2 com.google.cloud google-cloud-bigtable-parent - 2.45.1 + 2.46.1-SNAPSHOT @@ -18,14 +18,14 @@ com.google.cloud google-cloud-bigtable-deps-bom - 2.45.1 + 2.46.1-SNAPSHOT pom import com.google.cloud google-cloud-bigtable-bom - 2.45.1 + 2.46.1-SNAPSHOT pom import diff --git a/pom.xml b/pom.xml index 867e1b770c..15a243b8ca 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ google-cloud-bigtable-parent pom - 2.45.1 + 2.46.1-SNAPSHOT Google Cloud Bigtable Parent https://github.com/googleapis/java-bigtable @@ -14,7 +14,7 @@ com.google.cloud sdk-platform-java-config - 3.37.0 + 3.39.0 @@ -153,27 +153,27 @@ com.google.api.grpc proto-google-cloud-bigtable-v2 - 2.45.1 + 2.46.1-SNAPSHOT com.google.api.grpc proto-google-cloud-bigtable-admin-v2 - 2.45.1 + 2.46.1-SNAPSHOT com.google.api.grpc grpc-google-cloud-bigtable-v2 - 2.45.1 + 2.46.1-SNAPSHOT com.google.api.grpc grpc-google-cloud-bigtable-admin-v2 - 2.45.1 + 2.46.1-SNAPSHOT com.google.cloud google-cloud-bigtable - 2.45.1 + 2.46.1-SNAPSHOT diff --git a/proto-google-cloud-bigtable-admin-v2/pom.xml b/proto-google-cloud-bigtable-admin-v2/pom.xml index d9949d197b..367c6fee22 100644 --- a/proto-google-cloud-bigtable-admin-v2/pom.xml +++ b/proto-google-cloud-bigtable-admin-v2/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc proto-google-cloud-bigtable-admin-v2 - 2.45.1 + 2.46.1-SNAPSHOT proto-google-cloud-bigtable-admin-v2 PROTO library for proto-google-cloud-bigtable-admin-v2 com.google.cloud google-cloud-bigtable-parent - 2.45.1 + 2.46.1-SNAPSHOT @@ -18,14 +18,14 @@ com.google.cloud google-cloud-bigtable-deps-bom - 2.45.1 + 2.46.1-SNAPSHOT pom import com.google.cloud google-cloud-bigtable-bom - 2.45.1 + 2.46.1-SNAPSHOT pom import diff --git a/proto-google-cloud-bigtable-v2/pom.xml b/proto-google-cloud-bigtable-v2/pom.xml index fdd0a27b57..f53212594d 100644 --- a/proto-google-cloud-bigtable-v2/pom.xml +++ b/proto-google-cloud-bigtable-v2/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc proto-google-cloud-bigtable-v2 - 2.45.1 + 2.46.1-SNAPSHOT proto-google-cloud-bigtable-v2 PROTO library for proto-google-cloud-bigtable-v2 com.google.cloud google-cloud-bigtable-parent - 2.45.1 + 2.46.1-SNAPSHOT @@ -18,14 +18,14 @@ com.google.cloud google-cloud-bigtable-deps-bom - 2.45.1 + 2.46.1-SNAPSHOT pom import com.google.cloud google-cloud-bigtable-bom - 2.45.1 + 2.46.1-SNAPSHOT pom import diff --git a/proto-google-cloud-bigtable-v2/src/main/java/com/google/bigtable/v2/FeatureFlags.java b/proto-google-cloud-bigtable-v2/src/main/java/com/google/bigtable/v2/FeatureFlags.java index 4d18dd4c1d..f8dc326085 100644 --- a/proto-google-cloud-bigtable-v2/src/main/java/com/google/bigtable/v2/FeatureFlags.java +++ b/proto-google-cloud-bigtable-v2/src/main/java/com/google/bigtable/v2/FeatureFlags.java @@ -202,6 +202,42 @@ public boolean getClientSideMetricsEnabled() { return clientSideMetricsEnabled_; } + public static final int TRAFFIC_DIRECTOR_ENABLED_FIELD_NUMBER = 9; + private boolean trafficDirectorEnabled_ = false; + /** + * + * + *
+   * Notify the server that the client using Traffic Director endpoint.
+   * 
+ * + * bool traffic_director_enabled = 9; + * + * @return The trafficDirectorEnabled. + */ + @java.lang.Override + public boolean getTrafficDirectorEnabled() { + return trafficDirectorEnabled_; + } + + public static final int DIRECT_ACCESS_REQUESTED_FIELD_NUMBER = 10; + private boolean directAccessRequested_ = false; + /** + * + * + *
+   * Notify the server that the client explicitly opted in for Direct Access.
+   * 
+ * + * bool direct_access_requested = 10; + * + * @return The directAccessRequested. + */ + @java.lang.Override + public boolean getDirectAccessRequested() { + return directAccessRequested_; + } + private byte memoizedIsInitialized = -1; @java.lang.Override @@ -237,6 +273,12 @@ public void writeTo(com.google.protobuf.CodedOutputStream output) throws java.io if (clientSideMetricsEnabled_ != false) { output.writeBool(8, clientSideMetricsEnabled_); } + if (trafficDirectorEnabled_ != false) { + output.writeBool(9, trafficDirectorEnabled_); + } + if (directAccessRequested_ != false) { + output.writeBool(10, directAccessRequested_); + } getUnknownFields().writeTo(output); } @@ -267,6 +309,12 @@ public int getSerializedSize() { if (clientSideMetricsEnabled_ != false) { size += com.google.protobuf.CodedOutputStream.computeBoolSize(8, clientSideMetricsEnabled_); } + if (trafficDirectorEnabled_ != false) { + size += com.google.protobuf.CodedOutputStream.computeBoolSize(9, trafficDirectorEnabled_); + } + if (directAccessRequested_ != false) { + size += com.google.protobuf.CodedOutputStream.computeBoolSize(10, directAccessRequested_); + } size += getUnknownFields().getSerializedSize(); memoizedSize = size; return size; @@ -289,6 +337,8 @@ public boolean equals(final java.lang.Object obj) { if (getRoutingCookie() != other.getRoutingCookie()) return false; if (getRetryInfo() != other.getRetryInfo()) return false; if (getClientSideMetricsEnabled() != other.getClientSideMetricsEnabled()) return false; + if (getTrafficDirectorEnabled() != other.getTrafficDirectorEnabled()) return false; + if (getDirectAccessRequested() != other.getDirectAccessRequested()) return false; if (!getUnknownFields().equals(other.getUnknownFields())) return false; return true; } @@ -314,6 +364,10 @@ public int hashCode() { hash = (53 * hash) + com.google.protobuf.Internal.hashBoolean(getRetryInfo()); hash = (37 * hash) + CLIENT_SIDE_METRICS_ENABLED_FIELD_NUMBER; hash = (53 * hash) + com.google.protobuf.Internal.hashBoolean(getClientSideMetricsEnabled()); + hash = (37 * hash) + TRAFFIC_DIRECTOR_ENABLED_FIELD_NUMBER; + hash = (53 * hash) + com.google.protobuf.Internal.hashBoolean(getTrafficDirectorEnabled()); + hash = (37 * hash) + DIRECT_ACCESS_REQUESTED_FIELD_NUMBER; + hash = (53 * hash) + com.google.protobuf.Internal.hashBoolean(getDirectAccessRequested()); hash = (29 * hash) + getUnknownFields().hashCode(); memoizedHashCode = hash; return hash; @@ -466,6 +520,8 @@ public Builder clear() { routingCookie_ = false; retryInfo_ = false; clientSideMetricsEnabled_ = false; + trafficDirectorEnabled_ = false; + directAccessRequested_ = false; return this; } @@ -522,6 +578,12 @@ private void buildPartial0(com.google.bigtable.v2.FeatureFlags result) { if (((from_bitField0_ & 0x00000040) != 0)) { result.clientSideMetricsEnabled_ = clientSideMetricsEnabled_; } + if (((from_bitField0_ & 0x00000080) != 0)) { + result.trafficDirectorEnabled_ = trafficDirectorEnabled_; + } + if (((from_bitField0_ & 0x00000100) != 0)) { + result.directAccessRequested_ = directAccessRequested_; + } } @java.lang.Override @@ -590,6 +652,12 @@ public Builder mergeFrom(com.google.bigtable.v2.FeatureFlags other) { if (other.getClientSideMetricsEnabled() != false) { setClientSideMetricsEnabled(other.getClientSideMetricsEnabled()); } + if (other.getTrafficDirectorEnabled() != false) { + setTrafficDirectorEnabled(other.getTrafficDirectorEnabled()); + } + if (other.getDirectAccessRequested() != false) { + setDirectAccessRequested(other.getDirectAccessRequested()); + } this.mergeUnknownFields(other.getUnknownFields()); onChanged(); return this; @@ -658,6 +726,18 @@ public Builder mergeFrom( bitField0_ |= 0x00000040; break; } // case 64 + case 72: + { + trafficDirectorEnabled_ = input.readBool(); + bitField0_ |= 0x00000080; + break; + } // case 72 + case 80: + { + directAccessRequested_ = input.readBool(); + bitField0_ |= 0x00000100; + break; + } // case 80 default: { if (!super.parseUnknownField(input, extensionRegistry, tag)) { @@ -1072,6 +1152,112 @@ public Builder clearClientSideMetricsEnabled() { return this; } + private boolean trafficDirectorEnabled_; + /** + * + * + *
+     * Notify the server that the client using Traffic Director endpoint.
+     * 
+ * + * bool traffic_director_enabled = 9; + * + * @return The trafficDirectorEnabled. + */ + @java.lang.Override + public boolean getTrafficDirectorEnabled() { + return trafficDirectorEnabled_; + } + /** + * + * + *
+     * Notify the server that the client using Traffic Director endpoint.
+     * 
+ * + * bool traffic_director_enabled = 9; + * + * @param value The trafficDirectorEnabled to set. + * @return This builder for chaining. + */ + public Builder setTrafficDirectorEnabled(boolean value) { + + trafficDirectorEnabled_ = value; + bitField0_ |= 0x00000080; + onChanged(); + return this; + } + /** + * + * + *
+     * Notify the server that the client using Traffic Director endpoint.
+     * 
+ * + * bool traffic_director_enabled = 9; + * + * @return This builder for chaining. + */ + public Builder clearTrafficDirectorEnabled() { + bitField0_ = (bitField0_ & ~0x00000080); + trafficDirectorEnabled_ = false; + onChanged(); + return this; + } + + private boolean directAccessRequested_; + /** + * + * + *
+     * Notify the server that the client explicitly opted in for Direct Access.
+     * 
+ * + * bool direct_access_requested = 10; + * + * @return The directAccessRequested. + */ + @java.lang.Override + public boolean getDirectAccessRequested() { + return directAccessRequested_; + } + /** + * + * + *
+     * Notify the server that the client explicitly opted in for Direct Access.
+     * 
+ * + * bool direct_access_requested = 10; + * + * @param value The directAccessRequested to set. + * @return This builder for chaining. + */ + public Builder setDirectAccessRequested(boolean value) { + + directAccessRequested_ = value; + bitField0_ |= 0x00000100; + onChanged(); + return this; + } + /** + * + * + *
+     * Notify the server that the client explicitly opted in for Direct Access.
+     * 
+ * + * bool direct_access_requested = 10; + * + * @return This builder for chaining. + */ + public Builder clearDirectAccessRequested() { + bitField0_ = (bitField0_ & ~0x00000100); + directAccessRequested_ = false; + onChanged(); + return this; + } + @java.lang.Override public final Builder setUnknownFields(final com.google.protobuf.UnknownFieldSet unknownFields) { return super.setUnknownFields(unknownFields); diff --git a/proto-google-cloud-bigtable-v2/src/main/java/com/google/bigtable/v2/FeatureFlagsOrBuilder.java b/proto-google-cloud-bigtable-v2/src/main/java/com/google/bigtable/v2/FeatureFlagsOrBuilder.java index 2a61edabdb..a4f62d10eb 100644 --- a/proto-google-cloud-bigtable-v2/src/main/java/com/google/bigtable/v2/FeatureFlagsOrBuilder.java +++ b/proto-google-cloud-bigtable-v2/src/main/java/com/google/bigtable/v2/FeatureFlagsOrBuilder.java @@ -122,4 +122,30 @@ public interface FeatureFlagsOrBuilder * @return The clientSideMetricsEnabled. */ boolean getClientSideMetricsEnabled(); + + /** + * + * + *
+   * Notify the server that the client using Traffic Director endpoint.
+   * 
+ * + * bool traffic_director_enabled = 9; + * + * @return The trafficDirectorEnabled. + */ + boolean getTrafficDirectorEnabled(); + + /** + * + * + *
+   * Notify the server that the client explicitly opted in for Direct Access.
+   * 
+ * + * bool direct_access_requested = 10; + * + * @return The directAccessRequested. + */ + boolean getDirectAccessRequested(); } diff --git a/proto-google-cloud-bigtable-v2/src/main/java/com/google/bigtable/v2/FeatureFlagsProto.java b/proto-google-cloud-bigtable-v2/src/main/java/com/google/bigtable/v2/FeatureFlagsProto.java index e5875d27ee..78a36f7647 100644 --- a/proto-google-cloud-bigtable-v2/src/main/java/com/google/bigtable/v2/FeatureFlagsProto.java +++ b/proto-google-cloud-bigtable-v2/src/main/java/com/google/bigtable/v2/FeatureFlagsProto.java @@ -42,17 +42,19 @@ public static com.google.protobuf.Descriptors.FileDescriptor getDescriptor() { static { java.lang.String[] descriptorData = { "\n&google/bigtable/v2/feature_flags.proto" - + "\022\022google.bigtable.v2\"\333\001\n\014FeatureFlags\022\025\n" + + "\022\022google.bigtable.v2\"\236\002\n\014FeatureFlags\022\025\n" + "\rreverse_scans\030\001 \001(\010\022\036\n\026mutate_rows_rate" + "_limit\030\003 \001(\010\022\037\n\027mutate_rows_rate_limit2\030" + "\005 \001(\010\022\"\n\032last_scanned_row_responses\030\004 \001(" + "\010\022\026\n\016routing_cookie\030\006 \001(\010\022\022\n\nretry_info\030" + "\007 \001(\010\022#\n\033client_side_metrics_enabled\030\010 \001" - + "(\010B\273\001\n\026com.google.bigtable.v2B\021FeatureFl" - + "agsProtoP\001Z8cloud.google.com/go/bigtable" - + "/apiv2/bigtablepb;bigtablepb\252\002\030Google.Cl" - + "oud.Bigtable.V2\312\002\030Google\\Cloud\\Bigtable\\" - + "V2\352\002\033Google::Cloud::Bigtable::V2b\006proto3" + + "(\010\022 \n\030traffic_director_enabled\030\t \001(\010\022\037\n\027" + + "direct_access_requested\030\n \001(\010B\273\001\n\026com.go" + + "ogle.bigtable.v2B\021FeatureFlagsProtoP\001Z8c" + + "loud.google.com/go/bigtable/apiv2/bigtab" + + "lepb;bigtablepb\252\002\030Google.Cloud.Bigtable." + + "V2\312\002\030Google\\Cloud\\Bigtable\\V2\352\002\033Google::" + + "Cloud::Bigtable::V2b\006proto3" }; descriptor = com.google.protobuf.Descriptors.FileDescriptor.internalBuildGeneratedFileFrom( @@ -70,6 +72,8 @@ public static com.google.protobuf.Descriptors.FileDescriptor getDescriptor() { "RoutingCookie", "RetryInfo", "ClientSideMetricsEnabled", + "TrafficDirectorEnabled", + "DirectAccessRequested", }); } diff --git a/proto-google-cloud-bigtable-v2/src/main/proto/google/bigtable/v2/feature_flags.proto b/proto-google-cloud-bigtable-v2/src/main/proto/google/bigtable/v2/feature_flags.proto index e97f23e15a..d4c3bdbd71 100644 --- a/proto-google-cloud-bigtable-v2/src/main/proto/google/bigtable/v2/feature_flags.proto +++ b/proto-google-cloud-bigtable-v2/src/main/proto/google/bigtable/v2/feature_flags.proto @@ -61,4 +61,10 @@ message FeatureFlags { // Notify the server that the client has client side metrics enabled. bool client_side_metrics_enabled = 8; + + // Notify the server that the client using Traffic Director endpoint. + bool traffic_director_enabled = 9; + + // Notify the server that the client explicitly opted in for Direct Access. + bool direct_access_requested = 10; } diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml index 71414af391..4187b8d32e 100644 --- a/samples/snapshot/pom.xml +++ b/samples/snapshot/pom.xml @@ -28,7 +28,7 @@ com.google.cloud google-cloud-bigtable - 2.45.1 + 2.46.1-SNAPSHOT diff --git a/test-proxy/pom.xml b/test-proxy/pom.xml index f351f56a97..03f319f5c6 100644 --- a/test-proxy/pom.xml +++ b/test-proxy/pom.xml @@ -12,11 +12,11 @@ google-cloud-bigtable-parent com.google.cloud - 2.45.1 + 2.46.1-SNAPSHOT - 2.45.1 + 2.46.1-SNAPSHOT diff --git a/test-proxy/src/main/java/com/google/cloud/bigtable/testproxy/CbtTestProxy.java b/test-proxy/src/main/java/com/google/cloud/bigtable/testproxy/CbtTestProxy.java index 1c72704b62..16b5c8257c 100644 --- a/test-proxy/src/main/java/com/google/cloud/bigtable/testproxy/CbtTestProxy.java +++ b/test-proxy/src/main/java/com/google/cloud/bigtable/testproxy/CbtTestProxy.java @@ -26,7 +26,8 @@ import com.google.api.gax.retrying.RetrySettings; import com.google.api.gax.rpc.ApiException; import com.google.api.gax.rpc.ServerStream; -import com.google.auth.oauth2.GoogleCredentials; +import com.google.auth.oauth2.AccessToken; +import com.google.auth.oauth2.OAuth2Credentials; import com.google.auto.value.AutoValue; import com.google.bigtable.v2.Column; import com.google.bigtable.v2.Family; @@ -59,9 +60,6 @@ import java.io.ByteArrayInputStream; import java.io.Closeable; import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; @@ -72,7 +70,6 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; -import javax.annotation.Nullable; import org.threeten.bp.Duration; /** Java implementation of the CBT test proxy. Used to test the Java CBT client. */ @@ -95,50 +92,13 @@ static CbtClient create(BigtableDataSettings settings, BigtableDataClient dataCl private static final Logger logger = Logger.getLogger(CbtTestProxy.class.getName()); - private CbtTestProxy( - boolean encrypted, - @Nullable String rootCerts, - @Nullable String sslTarget, - @Nullable String credential) { - this.encrypted = encrypted; - this.rootCerts = rootCerts; - this.sslTarget = sslTarget; - this.credential = credential; + private CbtTestProxy() { this.idClientMap = new ConcurrentHashMap<>(); } - /** - * Factory method to return a proxy instance that interacts with server unencrypted and - * unauthenticated. - */ - public static CbtTestProxy createUnencrypted() { - return new CbtTestProxy(false, null, null, null); - } - - /** - * Factory method to return a proxy instance that interacts with server encrypted. Default - * authority and public certificates are used if null values are passed in. - * - * @param rootCertsPemPath The path to a root certificate PEM file - * @param sslTarget The override of SSL target name - * @param credentialJsonPath The path to a credential JSON file - */ - public static CbtTestProxy createEncrypted( - @Nullable String rootCertsPemPath, - @Nullable String sslTarget, - @Nullable String credentialJsonPath) - throws IOException { - String tmpRootCerts = null, tmpCredential = null; - if (rootCertsPemPath != null) { - Path file = Paths.get(rootCertsPemPath); - tmpRootCerts = new String(Files.readAllBytes(file), UTF_8); - } - if (credentialJsonPath != null) { - Path file = Paths.get(credentialJsonPath); - tmpCredential = new String(Files.readAllBytes(file), UTF_8); - } - - return new CbtTestProxy(true, tmpRootCerts, sslTarget, tmpCredential); + /** Factory method to return a proxy instance. */ + public static CbtTestProxy create() { + return new CbtTestProxy(); } /** @@ -196,8 +156,12 @@ public synchronized void createClient( Preconditions.checkArgument(!request.getProjectId().isEmpty(), "project id must be provided"); Preconditions.checkArgument(!request.getInstanceId().isEmpty(), "instance id must be provided"); Preconditions.checkArgument(!request.getDataTarget().isEmpty(), "data target must be provided"); + Preconditions.checkArgument( + !request.getSecurityOptions().getUseSsl() + || !request.getSecurityOptions().getSslRootCertsPemBytes().isEmpty(), + "security_options.ssl_root_certs_pem must be provided if security_options.use_ssl is true"); - if (idClientMap.contains(request.getClientId())) { + if (idClientMap.containsKey(request.getClientId())) { responseObserver.onError( Status.ALREADY_EXISTS .withDescription("Client " + request.getClientId() + " already exists.") @@ -205,6 +169,8 @@ public synchronized void createClient( return; } + // setRefreshingChannel is needed for now. + @SuppressWarnings("deprecation") BigtableDataSettings.Builder settingsBuilder = BigtableDataSettings.newBuilder() // Disable channel refreshing when not using the real server @@ -213,9 +179,6 @@ public synchronized void createClient( .setInstanceId(request.getInstanceId()) .setAppProfileId(request.getAppProfileId()); - settingsBuilder.stubSettings().setEnableRoutingCookie(false); - settingsBuilder.stubSettings().setEnableRetryInfo(false); - if (request.hasPerOperationTimeout()) { Duration newTimeout = Duration.ofMillis(Durations.toMillis(request.getPerOperationTimeout())); settingsBuilder = overrideTimeoutSetting(newTimeout, settingsBuilder); @@ -249,8 +212,13 @@ public synchronized void createClient( settingsBuilder .stubSettings() .setEndpoint(request.getDataTarget()) - .setTransportChannelProvider(getTransportChannel()) - .setCredentialsProvider(getCredentialsProvider()); + .setTransportChannelProvider( + getTransportChannel( + request.getSecurityOptions().getUseSsl(), + request.getSecurityOptions().getSslRootCertsPem(), + request.getSecurityOptions().getSslEndpointOverride())) + .setCredentialsProvider( + getCredentialsProvider(request.getSecurityOptions().getAccessToken())); } BigtableDataSettings settings = settingsBuilder.build(); BigtableDataClient client = BigtableDataClient.create(settings); @@ -780,52 +748,60 @@ private static String extractTableIdFromTableName(String fullTableName) return matcher.group(3); } - private InstantiatingGrpcChannelProvider getTransportChannel() throws IOException { + @SuppressWarnings("rawtypes") + private InstantiatingGrpcChannelProvider getTransportChannel( + boolean encrypted, String rootCertsPem, String sslTarget) { if (!encrypted) { return EnhancedBigtableStubSettings.defaultGrpcTransportProviderBuilder() .setChannelConfigurator(ManagedChannelBuilder::usePlaintext) .build(); } - if (rootCerts == null) { - return EnhancedBigtableStubSettings.defaultGrpcTransportProviderBuilder().build(); + final SslContext sslContext; + if (rootCertsPem.isEmpty()) { + sslContext = null; + } else { + try { + sslContext = + GrpcSslContexts.forClient() + .trustManager(new ByteArrayInputStream(rootCertsPem.getBytes(UTF_8))) + .build(); + } catch (IOException e) { + throw new IllegalArgumentException(e); + } } - final SslContext secureContext = - GrpcSslContexts.forClient() - .trustManager(new ByteArrayInputStream(rootCerts.getBytes(UTF_8))) - .build(); return EnhancedBigtableStubSettings.defaultGrpcTransportProviderBuilder() .setChannelConfigurator( new ApiFunction() { @Override public ManagedChannelBuilder apply(ManagedChannelBuilder input) { NettyChannelBuilder channelBuilder = (NettyChannelBuilder) input; - channelBuilder.sslContext(secureContext).overrideAuthority(sslTarget); + + if (sslContext != null) { + channelBuilder.sslContext(sslContext); + } + + if (!sslTarget.isEmpty()) { + channelBuilder.overrideAuthority(sslTarget); + } + return channelBuilder; } }) .build(); } - private CredentialsProvider getCredentialsProvider() throws IOException { - if (credential == null) { + private CredentialsProvider getCredentialsProvider(String accessToken) { + if (accessToken.isEmpty()) { return NoCredentialsProvider.create(); } - final GoogleCredentials creds = - GoogleCredentials.fromStream(new ByteArrayInputStream(credential.getBytes(UTF_8))); - - return FixedCredentialsProvider.create(creds); + return FixedCredentialsProvider.create( + OAuth2Credentials.create(new AccessToken(accessToken, null))); } private final ConcurrentHashMap idClientMap; - private final boolean encrypted; - - // Parameters that may be needed when "encrypted" is true. - private final String rootCerts; - private final String sslTarget; - private final String credential; private static final Pattern tablePattern = Pattern.compile("projects/([^/]+)/instances/([^/]+)/tables/([^/]+)"); diff --git a/test-proxy/src/main/java/com/google/cloud/bigtable/testproxy/CbtTestProxyMain.java b/test-proxy/src/main/java/com/google/cloud/bigtable/testproxy/CbtTestProxyMain.java index 8750909f1a..f817197d14 100644 --- a/test-proxy/src/main/java/com/google/cloud/bigtable/testproxy/CbtTestProxyMain.java +++ b/test-proxy/src/main/java/com/google/cloud/bigtable/testproxy/CbtTestProxyMain.java @@ -32,19 +32,7 @@ public static void main(String[] args) throws InterruptedException, IOException throw new IllegalArgumentException(String.format("Port %d is not > 0.", port)); } - CbtTestProxy cbtTestProxy; - - // If encryption is specified - boolean encrypted = Boolean.getBoolean("encrypted"); - if (encrypted) { - String rootCertsPemPath = System.getProperty("root.certs.pem.path"); - String sslTarget = System.getProperty("ssl.target"); - String credentialJsonPath = System.getProperty("credential.json.path"); - cbtTestProxy = CbtTestProxy.createEncrypted(rootCertsPemPath, sslTarget, credentialJsonPath); - } else { - cbtTestProxy = CbtTestProxy.createUnencrypted(); - } - + CbtTestProxy cbtTestProxy = CbtTestProxy.create(); logger.info(String.format("Test proxy starting on %d", port)); ServerBuilder.forPort(port).addService(cbtTestProxy).build().start().awaitTermination(); } diff --git a/test-proxy/src/main/proto/test_proxy.proto b/test-proxy/src/main/proto/test_proxy.proto index 753ca82cc0..b82354b08e 100644 --- a/test-proxy/src/main/proto/test_proxy.proto +++ b/test-proxy/src/main/proto/test_proxy.proto @@ -38,6 +38,27 @@ enum OptionalFeatureConfig { // Request to test proxy service to create a client object. message CreateClientRequest { + message SecurityOptions { + // Access token to use for client credentials. If empty, the client will not + // use any call credentials. Certain implementations may require `use_ssl` + // to be set when using this. + string access_token = 1; + + // Whether to use SSL channel credentials when connecting to the data + // endpoint. + bool use_ssl = 2; + + // If using SSL channel credentials, override the SSL endpoint to match the + // host that is specified in the backend's certificate. Also sets the + // client's authority header value. + string ssl_endpoint_override = 3; + + // PEM encoding of the server root certificates. If not set, the default + // root certs will be used instead. The default can be overridden via the + // GRPC_DEFAULT_SSL_ROOTS_FILE_PATH env var. + string ssl_root_certs_pem = 4; + } + // A unique ID associated with the client object to be created. string client_id = 1; @@ -66,6 +87,17 @@ message CreateClientRequest { // Optional config that dictates how the optional features should be enabled // during the client creation. Please check the enum type's docstring above. OptionalFeatureConfig optional_feature_config = 7; + + // Options to allow connecting to backends with channel and/or call + // credentials. This is needed internally by Cloud Bigtable's own testing + // frameworks.It is not necessary to support these fields for client + // conformance testing. + // + // WARNING: this allows the proxy to connect to a real production + // CBT backend with the right options, however, the proxy itself is insecure + // so it is not recommended to use it with real credentials or outside testing + // contexts. + SecurityOptions security_options = 8; } // Response from test proxy service for CreateClientRequest. diff --git a/versions.txt b/versions.txt index c652f2e783..e896e3222e 100644 --- a/versions.txt +++ b/versions.txt @@ -1,10 +1,10 @@ # Format: # module:released-version:current-version -google-cloud-bigtable:2.45.1:2.45.1 -grpc-google-cloud-bigtable-admin-v2:2.45.1:2.45.1 -grpc-google-cloud-bigtable-v2:2.45.1:2.45.1 -proto-google-cloud-bigtable-admin-v2:2.45.1:2.45.1 -proto-google-cloud-bigtable-v2:2.45.1:2.45.1 -google-cloud-bigtable-emulator:0.182.1:0.182.1 -google-cloud-bigtable-emulator-core:0.182.1:0.182.1 +google-cloud-bigtable:2.46.0:2.46.1-SNAPSHOT +grpc-google-cloud-bigtable-admin-v2:2.46.0:2.46.1-SNAPSHOT +grpc-google-cloud-bigtable-v2:2.46.0:2.46.1-SNAPSHOT +proto-google-cloud-bigtable-admin-v2:2.46.0:2.46.1-SNAPSHOT +proto-google-cloud-bigtable-v2:2.46.0:2.46.1-SNAPSHOT +google-cloud-bigtable-emulator:0.183.0:0.183.1-SNAPSHOT +google-cloud-bigtable-emulator-core:0.183.0:0.183.1-SNAPSHOT