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()