diff --git a/.kokoro/presubmit/graalvm-native-a.cfg b/.kokoro/presubmit/graalvm-native-a.cfg index af9f68ad4f..d1c7f7580d 100644 --- a/.kokoro/presubmit/graalvm-native-a.cfg +++ b/.kokoro/presubmit/graalvm-native-a.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.61.0" # {x-version-update:google-cloud-shared-dependencies:current} + value: "gcr.io/cloud-devrel-public-resources/graalvm_sdk_platform_a:3.62.0-SNAPSHOT" # {x-version-update:google-cloud-shared-dependencies:current} } env_vars: { diff --git a/.kokoro/presubmit/graalvm-native-b.cfg b/.kokoro/presubmit/graalvm-native-b.cfg index 576031a719..1041cf93b8 100644 --- a/.kokoro/presubmit/graalvm-native-b.cfg +++ b/.kokoro/presubmit/graalvm-native-b.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.61.0" # {x-version-update:google-cloud-shared-dependencies:current} + value: "gcr.io/cloud-devrel-public-resources/graalvm_sdk_platform_b:3.62.0-SNAPSHOT" # {x-version-update:google-cloud-shared-dependencies:current} } env_vars: { diff --git a/.kokoro/presubmit/graalvm-native-c.cfg b/.kokoro/presubmit/graalvm-native-c.cfg index 1d86c06d22..731eee483b 100644 --- a/.kokoro/presubmit/graalvm-native-c.cfg +++ b/.kokoro/presubmit/graalvm-native-c.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_c:3.61.0" # {x-version-update:google-cloud-shared-dependencies:current} + value: "gcr.io/cloud-devrel-public-resources/graalvm_sdk_platform_c:3.62.0-SNAPSHOT" # {x-version-update:google-cloud-shared-dependencies:current} } env_vars: { diff --git a/google-cloud-bigtable/pom.xml b/google-cloud-bigtable/pom.xml index ca56187dc4..a3604588e9 100644 --- a/google-cloud-bigtable/pom.xml +++ b/google-cloud-bigtable/pom.xml @@ -281,7 +281,6 @@ com.google.api.grpc grpc-google-cloud-bigtable-admin-v2 - test com.google.cloud diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/BaseBigtableInstanceAdminClient.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/BaseBigtableInstanceAdminClient.java index b23c08d265..af1a9b3206 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/BaseBigtableInstanceAdminClient.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/BaseBigtableInstanceAdminClient.java @@ -5249,7 +5249,7 @@ public final void deleteMaterializedView(DeleteMaterializedViewRequest request) } @Override - public final void close() { + public void close() { stub.close(); } diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/BaseBigtableTableAdminClient.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/BaseBigtableTableAdminClient.java index 9a3758b373..4952ee4c15 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/BaseBigtableTableAdminClient.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/BaseBigtableTableAdminClient.java @@ -6414,7 +6414,7 @@ public final UnaryCallable deleteSchemaBundleC } @Override - public final void close() { + public void close() { stub.close(); } diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/BigtableInstanceAdminClientV2.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/BigtableInstanceAdminClientV2.java new file mode 100644 index 0000000000..d9bcdf118e --- /dev/null +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/BigtableInstanceAdminClientV2.java @@ -0,0 +1,48 @@ +/* + * Copyright 2026 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.admin.v2; + +import com.google.cloud.bigtable.admin.v2.stub.BigtableInstanceAdminStub; +import java.io.IOException; + +/** + * Modern Cloud Bigtable Instance Admin Client. + * + *

This client extends the {@link BaseBigtableInstanceAdminClient} to provide a simplified and + * enhanced API surface for managing Cloud Bigtable instances and clusters. + */ +public class BigtableInstanceAdminClientV2 extends BaseBigtableInstanceAdminClient { + + protected BigtableInstanceAdminClientV2(BaseBigtableInstanceAdminSettings settings) + throws IOException { + super(settings); + } + + protected BigtableInstanceAdminClientV2(BigtableInstanceAdminStub stub) { + super(stub); + } + + /** Constructs an instance of BigtableInstanceAdminClientV2 with the given settings. */ + public static final BigtableInstanceAdminClientV2 create( + BaseBigtableInstanceAdminSettings settings) throws IOException { + return new BigtableInstanceAdminClientV2(settings); + } + + /** Constructs an instance of BigtableInstanceAdminClientV2 with the given stub. */ + public static final BigtableInstanceAdminClientV2 create(BigtableInstanceAdminStub stub) { + return new BigtableInstanceAdminClientV2(stub); + } +} diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/BigtableTableAdminClientV2.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/BigtableTableAdminClientV2.java new file mode 100644 index 0000000000..911e2b671c --- /dev/null +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/BigtableTableAdminClientV2.java @@ -0,0 +1,384 @@ +/* + * Copyright 2026 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.admin.v2; + +import com.google.api.core.ApiFunction; +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutures; +import com.google.api.gax.grpc.GrpcCallSettings; +import com.google.api.gax.grpc.GrpcCallableFactory; +import com.google.api.gax.grpc.ProtoOperationTransformers.MetadataTransformer; +import com.google.api.gax.grpc.ProtoOperationTransformers.ResponseTransformer; +import com.google.api.gax.longrunning.OperationSnapshot; +import com.google.api.gax.longrunning.OperationTimedPollAlgorithm; +import com.google.api.gax.retrying.RetrySettings; +import com.google.api.gax.rpc.ApiExceptions; +import com.google.api.gax.rpc.OperationCallSettings; +import com.google.api.gax.rpc.OperationCallable; +import com.google.api.gax.rpc.UnaryCallSettings; +import com.google.api.gax.rpc.UnaryCallable; +import com.google.bigtable.admin.v2.OptimizeRestoredTableMetadata; +import com.google.cloud.bigtable.admin.v2.models.ConsistencyRequest; +import com.google.cloud.bigtable.admin.v2.models.OptimizeRestoredTableOperationToken; +import com.google.cloud.bigtable.admin.v2.models.RestoredTableResult; +import com.google.cloud.bigtable.admin.v2.stub.AwaitConsistencyCallableV2; +import com.google.cloud.bigtable.admin.v2.stub.BigtableTableAdminStubSettings; +import com.google.cloud.bigtable.admin.v2.stub.GrpcBigtableTableAdminStub; +import com.google.common.base.Strings; +import com.google.longrunning.Operation; +import com.google.protobuf.Empty; +import io.grpc.MethodDescriptor; +import java.io.IOException; +import java.time.Duration; +import java.util.concurrent.ExecutionException; +import javax.annotation.Nullable; + +/** + * Modern Cloud Bigtable Table Admin Client. + * + *

This client extends the {@link BaseBigtableTableAdminClient} to provide enhanced convenience + * methods for table administration. It improves the user experience by handling chained Long + * Running Operations (such as seamlessly restoring and then optimizing a table) and provides + * built-in, automated polling for consistency tokens. + */ +public class BigtableTableAdminClientV2 extends BaseBigtableTableAdminClient { + private final AwaitConsistencyCallableV2 awaitConsistencyCallable; + private final OperationCallable + optimizeRestoredTableOperationBaseCallable; + private final java.util.concurrent.ScheduledExecutorService backgroundExecutor; + + private static final RetrySettings AWAIT_CONSISTENCY_POLLING_SETTINGS_BASE = + RetrySettings.newBuilder() + .setInitialRetryDelayDuration(Duration.ofSeconds(10)) + .setRetryDelayMultiplier(1.0) + .setMaxRetryDelayDuration(Duration.ofSeconds(10)) + .setInitialRpcTimeoutDuration(Duration.ZERO) + .setMaxRpcTimeoutDuration(Duration.ZERO) + .setRpcTimeoutMultiplier(1.0) + .build(); + + private static final RetrySettings OPTIMIZE_RESTORED_TABLE_POLLING_SETTINGS = + RetrySettings.newBuilder() + .setInitialRetryDelayDuration(Duration.ofMillis(500L)) + .setRetryDelayMultiplier(1.5) + .setMaxRetryDelayDuration(Duration.ofMillis(5000L)) + .setInitialRpcTimeoutDuration(Duration.ZERO) + .setRpcTimeoutMultiplier(1.0) + .setMaxRpcTimeoutDuration(Duration.ZERO) + .setTotalTimeoutDuration(Duration.ofMillis(600000L)) + .build(); + + /** Constructs an instance of BigtableTableAdminClientV2 with the given settings. */ + public static final BigtableTableAdminClientV2 create(BaseBigtableTableAdminSettings settings) + throws IOException { + GrpcBigtableTableAdminStub stub = + (GrpcBigtableTableAdminStub) + ((BigtableTableAdminStubSettings) settings.getStubSettings()).createStub(); + java.util.concurrent.ScheduledExecutorService backgroundExecutor = + settings.getStubSettings().getBackgroundExecutorProvider().getExecutor(); + + AwaitConsistencyCallableV2 awaitConsistencyCallable = + createAwaitConsistencyCallable( + stub, + (BigtableTableAdminStubSettings) settings.getStubSettings(), + settings.getStubSettings().getClock(), + backgroundExecutor); + + OperationCallable + optimizeRestoredTableOperationBaseCallable = + createOptimizeRestoredTableOperationBaseCallable(stub, settings, backgroundExecutor); + + return new BigtableTableAdminClientV2( + stub, + backgroundExecutor, + awaitConsistencyCallable, + optimizeRestoredTableOperationBaseCallable); + } + + /** Constructs an instance of BigtableTableAdminClientV2 with the given stub. */ + public static final BigtableTableAdminClientV2 create(GrpcBigtableTableAdminStub stub) { + return new BigtableTableAdminClientV2(stub, null, null, null); + } + + protected BigtableTableAdminClientV2( + GrpcBigtableTableAdminStub stub, + @Nullable java.util.concurrent.ScheduledExecutorService backgroundExecutor, + @Nullable AwaitConsistencyCallableV2 awaitConsistencyCallable, + @Nullable + OperationCallable + optimizeRestoredTableOperationBaseCallable) { + super(stub); + this.backgroundExecutor = backgroundExecutor; + this.awaitConsistencyCallable = awaitConsistencyCallable; + this.optimizeRestoredTableOperationBaseCallable = optimizeRestoredTableOperationBaseCallable; + } + + private static AwaitConsistencyCallableV2 createAwaitConsistencyCallable( + GrpcBigtableTableAdminStub stub, + BigtableTableAdminStubSettings settings, + com.google.api.core.ApiClock clock, + java.util.concurrent.ScheduledExecutorService executor) { + // TODO(igorbernstein2): expose polling settings + RetrySettings pollingSettings = + AWAIT_CONSISTENCY_POLLING_SETTINGS_BASE.toBuilder() + .setTotalTimeout( + settings.checkConsistencySettings().getRetrySettings().getTotalTimeout()) + .build(); + + return AwaitConsistencyCallableV2.create( + stub.generateConsistencyTokenCallable(), + stub.checkConsistencyCallable(), + clock, + executor, + pollingSettings); + } + + private static OperationCallable + createOptimizeRestoredTableOperationBaseCallable( + GrpcBigtableTableAdminStub stub, + BaseBigtableTableAdminSettings settings, + java.util.concurrent.ScheduledExecutorService backgroundExecutor) + throws IOException { + + @SuppressWarnings("unchecked") + MethodDescriptor fakeDescriptor = + (MethodDescriptor) + (MethodDescriptor) + com.google.bigtable.admin.v2.BigtableTableAdminGrpc.getUpdateTableMethod(); + + GrpcCallSettings unusedInitialCallSettings = + GrpcCallSettings.create(fakeDescriptor); + + final MetadataTransformer protoMetadataTransformer = + MetadataTransformer.create(OptimizeRestoredTableMetadata.class); + + final ResponseTransformer protoResponseTransformer = + ResponseTransformer.create(com.google.protobuf.Empty.class); + + OperationCallSettings operationCallSettings = + OperationCallSettings.newBuilder() + .setInitialCallSettings( + UnaryCallSettings.newUnaryCallSettingsBuilder() + .setSimpleTimeoutNoRetriesDuration(Duration.ZERO) + .build()) + .setMetadataTransformer( + new ApiFunction() { + @Override + public OptimizeRestoredTableMetadata apply(OperationSnapshot input) { + return protoMetadataTransformer.apply(input); + } + }) + .setResponseTransformer( + new ApiFunction() { + @Override + public Empty apply(OperationSnapshot input) { + return protoResponseTransformer.apply(input); + } + }) + .setPollingAlgorithm( + OperationTimedPollAlgorithm.create(OPTIMIZE_RESTORED_TABLE_POLLING_SETTINGS)) + .build(); + + com.google.api.gax.rpc.ClientContext clientContext = + com.google.api.gax.rpc.ClientContext.newBuilder() + .setClock(settings.getStubSettings().getClock()) + .setExecutor(backgroundExecutor) + .setDefaultCallContext(com.google.api.gax.grpc.GrpcCallContext.createDefault()) + .build(); + + return GrpcCallableFactory.createOperationCallable( + unusedInitialCallSettings, operationCallSettings, clientContext, stub.getOperationsStub()); + } + + /** + * Awaits the completion of the "Optimize Restored Table" operation. + * + *

This method blocks until the restore operation is complete, extracts the optimization token, + * and returns an ApiFuture for the optimization phase. + * + * @param restoreFuture The future returned by restoreTableAsync(). + * @return An ApiFuture that tracks the optimization progress. + */ + public ApiFuture awaitOptimizeRestoredTable(ApiFuture restoreFuture) { + // 1. Block and wait for the restore operation to complete + RestoredTableResult result; + try { + result = restoreFuture.get(); + } catch (Exception e) { + throw new RuntimeException("Restore operation failed", e); + } + + // 2. Extract the operation token from the result + // (RestoredTableResult already wraps the OptimizeRestoredTableOperationToken) + OptimizeRestoredTableOperationToken token = result.getOptimizeRestoredTableOperationToken(); + + if (token == null || Strings.isNullOrEmpty(token.getOperationName())) { + // If there is no optimization operation, return immediate success. + return ApiFutures.immediateFuture(Empty.getDefaultInstance()); + } + + // 3. Return the future for the optimization operation + return getOptimizeRestoredTableCallable().resumeFutureCall(token.getOperationName()); + } + + /** + * Awaits a restored table is fully optimized. + * + *

Sample code + * + *

{@code
+   * RestoredTableResult result =
+   *     client.restoreTable(RestoreTableRequest.of(clusterId, backupId).setTableId(tableId));
+   * client.awaitOptimizeRestoredTable(result.getOptimizeRestoredTableOperationToken());
+   * }
+ */ + public void awaitOptimizeRestoredTable(OptimizeRestoredTableOperationToken token) + throws ExecutionException, InterruptedException { + awaitOptimizeRestoredTableAsync(token).get(); + } + + /** + * Awaits a restored table is fully optimized asynchronously. + * + *

Sample code + * + *

{@code
+   * RestoredTableResult result =
+   *     client.restoreTable(RestoreTableRequest.of(clusterId, backupId).setTableId(tableId));
+   * ApiFuture future = client.awaitOptimizeRestoredTableAsync(
+   *     result.getOptimizeRestoredTableOperationToken());
+   *
+   * ApiFutures.addCallback(
+   *   future,
+   *   new ApiFutureCallback() {
+   *     public void onSuccess(Void unused) {
+   *       System.out.println("The optimization of the restored table is done.");
+   *     }
+   *
+   *     public void onFailure(Throwable t) {
+   *       t.printStackTrace();
+   *     }
+   *   },
+   *   MoreExecutors.directExecutor()
+   * );
+   * }
+ */ + public ApiFuture awaitOptimizeRestoredTableAsync( + OptimizeRestoredTableOperationToken token) { + ApiFuture emptyFuture = + getOptimizeRestoredTableCallable().resumeFutureCall(token.getOperationName()); + return ApiFutures.transform( + emptyFuture, + new com.google.api.core.ApiFunction() { + @Override + public Void apply(Empty input) { + return null; + } + }, + com.google.common.util.concurrent.MoreExecutors.directExecutor()); + } + + /** + * Polls an existing consistency token until table replication is consistent across all clusters. + * Useful for checking consistency of a token generated in a separate process. Blocks until + * completion. + * + * @param tableName The fully qualified table name to check. + * @param consistencyToken The token to poll. + */ + public void waitForConsistency(String tableName, String consistencyToken) { + ApiExceptions.callAndTranslateApiException( + waitForConsistencyAsync(tableName, consistencyToken)); + } + + /** + * Asynchronously polls the consistency token. Returns a future that completes when table + * replication is consistent across all clusters. + * + * @param tableName The fully qualified table name to check. + * @param consistencyToken The token to poll. + */ + public ApiFuture waitForConsistencyAsync(String tableName, String consistencyToken) { + return getAwaitConsistencyCallable() + .futureCall(ConsistencyRequest.forReplicationFromTableName(tableName, consistencyToken)); + } + + private UnaryCallable getAwaitConsistencyCallable() { + if (awaitConsistencyCallable != null) { + return awaitConsistencyCallable; + } + throw new IllegalStateException( + "AwaitConsistencyCallable not initialized. BigtableTableAdminClientV2 must be " + + "initialized via BigtableTableAdminClientV2.create(BaseBigtableTableAdminSettings) " + + "to use this functionality."); + } + + private OperationCallable + getOptimizeRestoredTableCallable() { + if (optimizeRestoredTableOperationBaseCallable != null) { + return optimizeRestoredTableOperationBaseCallable; + } + throw new IllegalStateException( + "OptimizeRestoredTableCallable not initialized. BigtableTableAdminClientV2 must be " + + "initialized via BigtableTableAdminClientV2.create(BaseBigtableTableAdminSettings) " + + "to use this functionality."); + } + + @Override + public void close() { + if (backgroundExecutor != null) { + backgroundExecutor.shutdown(); + } + super.close(); + } + + @Override + public void shutdown() { + if (backgroundExecutor != null) { + backgroundExecutor.shutdown(); + } + super.shutdown(); + } + + @Override + public void shutdownNow() { + if (backgroundExecutor != null) { + backgroundExecutor.shutdownNow(); + } + super.shutdownNow(); + } + + @Override + public boolean isShutdown() { + return (backgroundExecutor == null || backgroundExecutor.isShutdown()) && super.isShutdown(); + } + + @Override + public boolean isTerminated() { + return (backgroundExecutor == null || backgroundExecutor.isTerminated()) + && super.isTerminated(); + } + + @Override + public boolean awaitTermination(long duration, java.util.concurrent.TimeUnit unit) + throws InterruptedException { + boolean terminated = true; + if (backgroundExecutor != null) { + terminated = backgroundExecutor.awaitTermination(duration, unit); + } + return terminated && super.awaitTermination(duration, unit); + } +} diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/stub/AwaitConsistencyCallableV2.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/stub/AwaitConsistencyCallableV2.java new file mode 100644 index 0000000000..93d4b1eb56 --- /dev/null +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/stub/AwaitConsistencyCallableV2.java @@ -0,0 +1,217 @@ +/* + * Copyright 2026 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.admin.v2.stub; + +import com.google.api.core.ApiAsyncFunction; +import com.google.api.core.ApiClock; +import com.google.api.core.ApiFunction; +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutures; +import com.google.api.core.InternalApi; +import com.google.api.gax.retrying.ExponentialPollAlgorithm; +import com.google.api.gax.retrying.NonCancellableFuture; +import com.google.api.gax.retrying.ResultRetryAlgorithmWithContext; +import com.google.api.gax.retrying.RetryAlgorithm; +import com.google.api.gax.retrying.RetrySettings; +import com.google.api.gax.retrying.RetryingContext; +import com.google.api.gax.retrying.RetryingExecutor; +import com.google.api.gax.retrying.RetryingFuture; +import com.google.api.gax.retrying.ScheduledRetryingExecutor; +import com.google.api.gax.retrying.TimedAttemptSettings; +import com.google.api.gax.rpc.ApiCallContext; +import com.google.api.gax.rpc.UnaryCallable; +import com.google.bigtable.admin.v2.CheckConsistencyRequest; +import com.google.bigtable.admin.v2.CheckConsistencyResponse; +import com.google.bigtable.admin.v2.GenerateConsistencyTokenRequest; +import com.google.bigtable.admin.v2.GenerateConsistencyTokenResponse; +import com.google.cloud.bigtable.admin.v2.models.ConsistencyRequest; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.util.concurrent.MoreExecutors; +import java.util.concurrent.Callable; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ScheduledExecutorService; + +/** + * Decoupled modern consistency polling callable for V2 client. + * + *

This callable waits until either replication or Data Boost has caught up to the point it was + * called. It wraps GenerateConsistencyToken and CheckConsistency RPCs and contains absolutely no + * reference or dependency on the data module. + */ +@InternalApi +public class AwaitConsistencyCallableV2 extends UnaryCallable { + private final UnaryCallable + generateCallable; + private final UnaryCallable checkCallable; + private final RetryingExecutor executor; + + @InternalApi + public static AwaitConsistencyCallableV2 create( + UnaryCallable + generateCallable, + UnaryCallable checkCallable, + ApiClock clock, + ScheduledExecutorService executor, + RetrySettings pollingSettings) { + + RetryAlgorithm retryAlgorithm = + new RetryAlgorithm<>( + new PollResultAlgorithm(), new ExponentialPollAlgorithm(pollingSettings, clock)); + + RetryingExecutor retryingExecutor = + new ScheduledRetryingExecutor<>(retryAlgorithm, executor); + + return new AwaitConsistencyCallableV2(generateCallable, checkCallable, retryingExecutor); + } + + @VisibleForTesting + AwaitConsistencyCallableV2( + UnaryCallable + generateCallable, + UnaryCallable checkCallable, + RetryingExecutor executor) { + this.generateCallable = generateCallable; + this.checkCallable = checkCallable; + this.executor = executor; + } + + @Override + public ApiFuture futureCall( + final ConsistencyRequest consistencyRequest, final ApiCallContext apiCallContext) { + + // If the token is already provided, skip generation and poll directly. + if (consistencyRequest.getConsistencyToken() != null) { + CheckConsistencyRequest request = + consistencyRequest.toCheckConsistencyProto(consistencyRequest.getConsistencyToken()); + return pollToken(request, apiCallContext); + } + + ApiFuture tokenFuture = + generateToken(consistencyRequest.toGenerateTokenProto(), apiCallContext); + + return ApiFutures.transformAsync( + tokenFuture, + new ApiAsyncFunction() { + @Override + public ApiFuture apply(GenerateConsistencyTokenResponse input) { + CheckConsistencyRequest request = + consistencyRequest.toCheckConsistencyProto(input.getConsistencyToken()); + return pollToken(request, apiCallContext); + } + }, + MoreExecutors.directExecutor()); + } + + private ApiFuture generateToken( + GenerateConsistencyTokenRequest generateRequest, ApiCallContext context) { + return generateCallable.futureCall(generateRequest, context); + } + + private ApiFuture pollToken(CheckConsistencyRequest request, ApiCallContext context) { + AttemptCallable attemptCallable = + new AttemptCallable<>(checkCallable, request, context); + RetryingFuture retryingFuture = + executor.createFuture(attemptCallable); + attemptCallable.setExternalFuture(retryingFuture); + attemptCallable.call(); + + return ApiFutures.transform( + retryingFuture, + new ApiFunction() { + @Override + public Void apply(CheckConsistencyResponse input) { + return null; + } + }, + MoreExecutors.directExecutor()); + } + + /** A callable representing an attempt to make an RPC call. */ + private static class AttemptCallable implements Callable { + private final UnaryCallable callable; + private final RequestT request; + + private volatile RetryingFuture externalFuture; + private final ApiCallContext callContext; + + AttemptCallable( + UnaryCallable callable, RequestT request, ApiCallContext callContext) { + this.callable = callable; + this.request = request; + this.callContext = callContext; + } + + void setExternalFuture(RetryingFuture externalFuture) { + this.externalFuture = externalFuture; + } + + @Override + public ResponseT call() { + try { + // NOTE: unlike gax's AttemptCallable, this ignores rpc timeouts + externalFuture.setAttemptFuture(new NonCancellableFuture()); + if (externalFuture.isDone()) { + return null; + } + ApiFuture internalFuture = callable.futureCall(request, callContext); + externalFuture.setAttemptFuture(internalFuture); + } catch (Throwable e) { + externalFuture.setAttemptFuture(ApiFutures.immediateFailedFuture(e)); + } + + return null; + } + } + + /** + * A polling algorithm for waiting for a consistent {@link CheckConsistencyResponse}. Please note + * that this class doesn't handle retryable errors and expects the underlying callable chain to + * handle this. + */ + private static class PollResultAlgorithm + implements ResultRetryAlgorithmWithContext { + + @Override + public TimedAttemptSettings createNextAttempt( + Throwable prevThrowable, + CheckConsistencyResponse prevResponse, + TimedAttemptSettings prevSettings) { + return null; + } + + @Override + public TimedAttemptSettings createNextAttempt( + RetryingContext context, + Throwable previousThrowable, + CheckConsistencyResponse previousResponse, + TimedAttemptSettings previousSettings) { + return null; + } + + @Override + public boolean shouldRetry( + RetryingContext context, Throwable previousThrowable, CheckConsistencyResponse prevResponse) + throws CancellationException { + return prevResponse != null && !prevResponse.getConsistent(); + } + + @Override + public boolean shouldRetry(Throwable prevThrowable, CheckConsistencyResponse prevResponse) + throws CancellationException { + return prevResponse != null && !prevResponse.getConsistent(); + } + } +} diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/BigtableInstanceAdminClientV2Test.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/BigtableInstanceAdminClientV2Test.java new file mode 100644 index 0000000000..2e19776082 --- /dev/null +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/BigtableInstanceAdminClientV2Test.java @@ -0,0 +1,48 @@ +/* + * Copyright 2026 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.admin.v2; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.cloud.bigtable.admin.v2.stub.BigtableInstanceAdminStub; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mockito; + +@RunWith(JUnit4.class) +public class BigtableInstanceAdminClientV2Test { + + @Test + public void testCreateWithStub() { + BigtableInstanceAdminStub mockStub = Mockito.mock(BigtableInstanceAdminStub.class); + BigtableInstanceAdminClientV2 client = BigtableInstanceAdminClientV2.create(mockStub); + + assertThat(client).isNotNull(); + } + + @Test + public void testCreateClientWithSettings() throws Exception { + BaseBigtableInstanceAdminSettings settings = + BaseBigtableInstanceAdminSettings.newBuilder() + .setCredentialsProvider(com.google.api.gax.core.NoCredentialsProvider.create()) + .build(); + try (BigtableInstanceAdminClientV2 settingsClient = + BigtableInstanceAdminClientV2.create(settings)) { + assertThat(settingsClient).isNotNull(); + } + } +} diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/BigtableTableAdminClientV2Test.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/BigtableTableAdminClientV2Test.java new file mode 100644 index 0000000000..1c769c999a --- /dev/null +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/BigtableTableAdminClientV2Test.java @@ -0,0 +1,185 @@ +/* + * Copyright 2026 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.admin.v2; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutures; +import com.google.api.gax.longrunning.OperationFuture; +import com.google.api.gax.rpc.OperationCallable; +import com.google.bigtable.admin.v2.OptimizeRestoredTableMetadata; +import com.google.cloud.bigtable.admin.v2.models.ConsistencyRequest; +import com.google.cloud.bigtable.admin.v2.models.OptimizeRestoredTableOperationToken; +import com.google.cloud.bigtable.admin.v2.models.RestoredTableResult; +import com.google.cloud.bigtable.admin.v2.stub.AwaitConsistencyCallableV2; +import com.google.cloud.bigtable.admin.v2.stub.EnhancedBigtableTableAdminStub; +import com.google.cloud.bigtable.admin.v2.stub.GrpcBigtableTableAdminStub; +import com.google.protobuf.Empty; +import java.util.concurrent.atomic.AtomicBoolean; +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.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.mockito.stubbing.Answer; + +@RunWith(JUnit4.class) +public class BigtableTableAdminClientV2Test { + @Rule public final MockitoRule mockitoRule = MockitoJUnit.rule(); + + private static final String TABLE_NAME = + "projects/my-project/instances/my-instance/tables/my-table"; + + @Mock private GrpcBigtableTableAdminStub mockStub; + + @Mock private AwaitConsistencyCallableV2 mockAwaitConsistencyCallable; + + @Mock + private OperationCallable + mockOptimizeRestoredTableCallable; + + private BigtableTableAdminClientV2 client; + + @Before + public void setUp() { + client = + new BigtableTableAdminClientV2( + mockStub, null, mockAwaitConsistencyCallable, mockOptimizeRestoredTableCallable); + } + + @Test + public void testWaitForConsistencyWithToken() { + // Setup + + String token = "my-token"; + ConsistencyRequest expectedRequest = + ConsistencyRequest.forReplicationFromTableName(TABLE_NAME, token); + + final AtomicBoolean wasCalled = new AtomicBoolean(false); + + Mockito.when(mockAwaitConsistencyCallable.futureCall(expectedRequest)) + .thenAnswer( + (Answer>) + invocationOnMock -> { + wasCalled.set(true); + return ApiFutures.immediateFuture(null); + }); + + // Execute + client.waitForConsistency(TABLE_NAME, token); + + // Verify + assertThat(wasCalled.get()).isTrue(); + } + + @Test + public void testAwaitOptimizeRestoredTable() throws Exception { + // Setup + + String optimizeToken = "my-optimization-token"; + + // 1. Mock the Token + OptimizeRestoredTableOperationToken mockToken = + Mockito.mock(OptimizeRestoredTableOperationToken.class); + Mockito.when(mockToken.getOperationName()).thenReturn(optimizeToken); + + // 2. Mock the Result (wrapping the token) + RestoredTableResult mockResult = Mockito.mock(RestoredTableResult.class); + Mockito.when(mockResult.getOptimizeRestoredTableOperationToken()).thenReturn(mockToken); + + // 3. Mock the Input Future (returning the result) + ApiFuture mockRestoreFuture = Mockito.mock(ApiFuture.class); + Mockito.when(mockRestoreFuture.get()).thenReturn(mockResult); + + // 4. Mock the Stub's behavior (resuming the Optimize Op) + OperationFuture mockOptimizeOp = + Mockito.mock(OperationFuture.class); + Mockito.when(mockOptimizeRestoredTableCallable.resumeFutureCall(optimizeToken)) + .thenReturn(mockOptimizeOp); + + // Execute + ApiFuture result = client.awaitOptimizeRestoredTable(mockRestoreFuture); + + // Verify + assertThat(result).isEqualTo(mockOptimizeOp); + Mockito.verify(mockOptimizeRestoredTableCallable).resumeFutureCall(optimizeToken); + } + + @Test + public void testAwaitOptimizeRestoredTable_NoOp() throws Exception { + // Setup: Result with NO optimization token (null or empty) + RestoredTableResult mockResult = Mockito.mock(RestoredTableResult.class); + Mockito.when(mockResult.getOptimizeRestoredTableOperationToken()).thenReturn(null); + + // Mock the Input Future + ApiFuture mockRestoreFuture = Mockito.mock(ApiFuture.class); + Mockito.when(mockRestoreFuture.get()).thenReturn(mockResult); + + // Execute + ApiFuture result = client.awaitOptimizeRestoredTable(mockRestoreFuture); + + // Verify: Returns immediate success (Empty) without calling the stub + assertThat(result.get()).isEqualTo(Empty.getDefaultInstance()); + } + + @Test + public void testCreateClientWithSettings() throws Exception { + BaseBigtableTableAdminSettings settings = + BaseBigtableTableAdminSettings.newBuilder() + .setCredentialsProvider(com.google.api.gax.core.NoCredentialsProvider.create()) + .build(); + try (BigtableTableAdminClientV2 settingsClient = BigtableTableAdminClientV2.create(settings)) { + // Verify that the underlying stub is NOT an Enhanced stub by default + // but the client has successfully initialized its own callables. + assertThat(settingsClient.getStub()).isNotInstanceOf(EnhancedBigtableTableAdminStub.class); + } + } + + @Test + public void testAwaitConsistency_ThrowsWhenNotInitialized() { + BigtableTableAdminClientV2 uninitializedClient = BigtableTableAdminClientV2.create(mockStub); + + try { + uninitializedClient.waitForConsistency(TABLE_NAME, "token"); + org.junit.Assert.fail("Expected IllegalStateException"); + } catch (IllegalStateException e) { + assertThat(e.getMessage()) + .contains("BigtableTableAdminClientV2.create(BaseBigtableTableAdminSettings)"); + } + } + + @Test + public void testOptimizeRestoredTable_ThrowsWhenNotInitialized() { + BigtableTableAdminClientV2 uninitializedClient = BigtableTableAdminClientV2.create(mockStub); + + OptimizeRestoredTableOperationToken mockToken = + Mockito.mock(OptimizeRestoredTableOperationToken.class); + Mockito.when(mockToken.getOperationName()).thenReturn("op-name"); + + try { + uninitializedClient.awaitOptimizeRestoredTableAsync(mockToken); + org.junit.Assert.fail("Expected IllegalStateException"); + } catch (IllegalStateException e) { + assertThat(e.getMessage()) + .contains("BigtableTableAdminClientV2.create(BaseBigtableTableAdminSettings)"); + } + } +} diff --git a/owlbot.py b/owlbot.py index 8b4ebb3d39..f6510983d5 100644 --- a/owlbot.py +++ b/owlbot.py @@ -75,6 +75,14 @@ def make_internal_only(sources): r"protected static BaseBigtable\1AdminClient create(" ) + # Remove the 'final' modifier from the close() method in the Base Admin clients + # This allows our handwritten wrappers to override close() and clean up custom executors. + s.replace( + f"{library}/**/BaseBigtable*AdminClient.java", + r"public final void close\(\) \{", + r"public void close() {" + ) + s.move(library) s.remove_staging_dirs()