diff --git a/.github/workflows/hermetic_library_generation.yaml b/.github/workflows/hermetic_library_generation.yaml index 6b856abdef..336ac2c8ca 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.56.2 + - uses: googleapis/sdk-platform-java/.github/scripts@v2.58.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 6ffb42e224..f979b148c9 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.47.0 + uses: googleapis/sdk-platform-java/java-shared-dependencies/unmanaged-dependency-check@google-cloud-shared-dependencies/v3.48.0 with: bom-path: google-cloud-bigtable-bom/pom.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index 62ecd2ad07..2a559b6009 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## [2.59.0](https://github.com/googleapis/java-bigtable/compare/v2.58.2...v2.59.0) (2025-05-16) + + +### Features + +* **bigtable:** Add DeletionProtection support for Logical Views ([#2539](https://github.com/googleapis/java-bigtable/issues/2539)) ([d9ba32b](https://github.com/googleapis/java-bigtable/commit/d9ba32b8e5792ceed054f67c58f5622e153e87d6)) + + +### Dependencies + +* Update googleapis/sdk-platform-java action to v2.58.0 ([#2581](https://github.com/googleapis/java-bigtable/issues/2581)) ([c9b0289](https://github.com/googleapis/java-bigtable/commit/c9b028902dc8aae9552181d65c9743be09d45ecf)) +* Update shared dependencies ([#2584](https://github.com/googleapis/java-bigtable/issues/2584)) ([ba82675](https://github.com/googleapis/java-bigtable/commit/ba82675c25dbe12443ac5ef48464dcb3f8c8894c)) + ## [2.58.2](https://github.com/googleapis/java-bigtable/compare/v2.58.1...v2.58.2) (2025-05-08) diff --git a/README.md b/README.md index fede60076c..cbce212f01 100644 --- a/README.md +++ b/README.md @@ -56,13 +56,13 @@ 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.58.2' +implementation 'com.google.cloud:google-cloud-bigtable:2.59.0' ``` If you are using SBT, add this to your dependencies: ```Scala -libraryDependencies += "com.google.cloud" % "google-cloud-bigtable" % "2.58.2" +libraryDependencies += "com.google.cloud" % "google-cloud-bigtable" % "2.59.0" ``` ## Authentication @@ -470,7 +470,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.58.2 +[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-bigtable/2.59.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/google-cloud-bigtable-bom/pom.xml b/google-cloud-bigtable-bom/pom.xml index 42557daa85..adcb3543d9 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.58.2 + 2.59.0 pom com.google.cloud sdk-platform-java-config - 3.47.0 + 3.48.0 @@ -63,37 +63,37 @@ com.google.cloud google-cloud-bigtable - 2.58.2 + 2.59.0 com.google.cloud google-cloud-bigtable-emulator - 0.195.2 + 0.196.0 com.google.cloud google-cloud-bigtable-emulator-core - 0.195.2 + 0.196.0 com.google.api.grpc grpc-google-cloud-bigtable-admin-v2 - 2.58.2 + 2.59.0 com.google.api.grpc grpc-google-cloud-bigtable-v2 - 2.58.2 + 2.59.0 com.google.api.grpc proto-google-cloud-bigtable-admin-v2 - 2.58.2 + 2.59.0 com.google.api.grpc proto-google-cloud-bigtable-v2 - 2.58.2 + 2.59.0 diff --git a/google-cloud-bigtable-deps-bom/pom.xml b/google-cloud-bigtable-deps-bom/pom.xml index 534eab7678..3428817941 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.47.0 + 3.48.0 com.google.cloud google-cloud-bigtable-deps-bom - 2.58.2 + 2.59.0 pom @@ -67,7 +67,7 @@ com.google.cloud gapic-libraries-bom - 1.56.0 + 1.57.0 pom import diff --git a/google-cloud-bigtable-emulator-core/pom.xml b/google-cloud-bigtable-emulator-core/pom.xml index df9ae290d1..4b0b0c2c38 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.58.2 + 2.59.0 google-cloud-bigtable-emulator-core - 0.195.2 + 0.196.0 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 0f78e19303..1ebf482868 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.195.2 + 0.196.0 Google Cloud Java - Bigtable Emulator https://github.com/googleapis/java-bigtable @@ -14,7 +14,7 @@ com.google.cloud google-cloud-bigtable-parent - 2.58.2 + 2.59.0 scm:git:git@github.com:googleapis/java-bigtable.git @@ -81,14 +81,14 @@ com.google.cloud google-cloud-bigtable-deps-bom - 2.58.2 + 2.59.0 pom import com.google.cloud google-cloud-bigtable-bom - 2.58.2 + 2.59.0 pom import @@ -99,7 +99,7 @@ com.google.cloud google-cloud-bigtable-emulator-core - 0.195.2 + 0.196.0 diff --git a/google-cloud-bigtable/pom.xml b/google-cloud-bigtable/pom.xml index 0ae7baa814..f86f9c0124 100644 --- a/google-cloud-bigtable/pom.xml +++ b/google-cloud-bigtable/pom.xml @@ -2,7 +2,7 @@ 4.0.0 google-cloud-bigtable - 2.58.2 + 2.59.0 jar Google Cloud Bigtable https://github.com/googleapis/java-bigtable @@ -12,11 +12,11 @@ com.google.cloud google-cloud-bigtable-parent - 2.58.2 + 2.59.0 - 2.58.2 + 2.59.0 google-cloud-bigtable @@ -54,14 +54,14 @@ com.google.cloud google-cloud-bigtable-deps-bom - 2.58.2 + 2.59.0 pom import com.google.cloud google-cloud-bigtable-bom - 2.58.2 + 2.59.0 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 5742ac50cb..0194198f91 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.58.2"; + public static String VERSION = "2.59.0"; // {x-version-update-end} } diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/CreateLogicalViewRequest.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/CreateLogicalViewRequest.java index 9db5d80f32..5a3a6e0f5f 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/CreateLogicalViewRequest.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/CreateLogicalViewRequest.java @@ -28,7 +28,7 @@ *
{@code
  * LogicalView existingLogicalView = ...;
  * CreateLogicalViewRequest logicalViewRequest = CreateLogicalViewRequest.of("my-instance", "my-new-logical-view")
- *   .setQuery("...");
+ *   .setQuery("...").setDeletionProtection(true);
  * }
* * @see LogicalView for more details @@ -56,6 +56,13 @@ public CreateLogicalViewRequest setQuery(@Nonnull String query) { return this; } + /** Configures if the logical view is deletion protected. */ + @SuppressWarnings("WeakerAccess") + public CreateLogicalViewRequest setDeletionProtection(boolean value) { + proto.getLogicalViewBuilder().setDeletionProtection(value); + return this; + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/LogicalView.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/LogicalView.java index c884d97730..48100c81de 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/LogicalView.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/LogicalView.java @@ -75,6 +75,11 @@ public String getQuery() { return proto.getQuery(); } + /** Returns whether this logical view is deletion protected. */ + public boolean isDeletionProtected() { + return proto.getDeletionProtection(); + } + /** * Creates the request protobuf. This method is considered an internal implementation detail and * not meant to be used by applications. diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/UpdateLogicalViewRequest.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/UpdateLogicalViewRequest.java index d24cfff30a..6cbc55a28d 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/UpdateLogicalViewRequest.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/UpdateLogicalViewRequest.java @@ -80,6 +80,13 @@ public UpdateLogicalViewRequest setQuery(String query) { return this; } + /** Changes the deletion protection of an existing logical view. */ + public UpdateLogicalViewRequest setDeletionProtection(boolean deletionProtection) { + requestBuilder.getLogicalViewBuilder().setDeletionProtection(deletionProtection); + updateFieldMask(com.google.bigtable.admin.v2.LogicalView.DELETION_PROTECTION_FIELD_NUMBER); + return this; + } + private void updateFieldMask(int fieldNumber) { FieldMask newMask = FieldMaskUtil.fromFieldNumbers(com.google.bigtable.admin.v2.LogicalView.class, fieldNumber); diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableChannelPool.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableChannelPool.java new file mode 100644 index 0000000000..aae154b7b5 --- /dev/null +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableChannelPool.java @@ -0,0 +1,595 @@ +/* + * Copyright 2025 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.gaxx.grpc; + +import com.google.api.core.InternalApi; +import com.google.api.gax.grpc.ChannelFactory; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ForwardingClientCall.SimpleForwardingClientCall; +import io.grpc.ForwardingClientCallListener.SimpleForwardingClientCallListener; +import io.grpc.ManagedChannel; +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; +import io.grpc.Status; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CancellationException; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nullable; + +/** + * A {@link ManagedChannel} that will send requests round-robin via a set of channels. + * + *

In addition to spreading requests over a set of child connections, the pool will also actively + * manage the lifecycle of the channels. Currently lifecycle management is limited to pre-emptively + * replacing channels every hour. In the future it will dynamically size the pool based on number of + * outstanding requests. + * + *

Package-private for internal use. + */ +class BigtableChannelPool extends ManagedChannel { + @VisibleForTesting + static final Logger LOG = Logger.getLogger(BigtableChannelPool.class.getName()); + + private static final java.time.Duration REFRESH_PERIOD = java.time.Duration.ofMinutes(50); + + private final BigtableChannelPoolSettings settings; + private final ChannelFactory channelFactory; + private final ScheduledExecutorService executor; + + private final Object entryWriteLock = new Object(); + @VisibleForTesting final AtomicReference> entries = new AtomicReference<>(); + private final AtomicInteger indexTicker = new AtomicInteger(); + private final String authority; + + static BigtableChannelPool create( + BigtableChannelPoolSettings settings, ChannelFactory channelFactory) throws IOException { + return new BigtableChannelPool( + settings, channelFactory, Executors.newSingleThreadScheduledExecutor()); + } + + /** + * Initializes the channel pool. Assumes that all channels have the same authority. + * + * @param settings options for controling the ChannelPool sizing behavior + * @param channelFactory method to create the channels + * @param executor periodically refreshes the channels + */ + @VisibleForTesting + BigtableChannelPool( + BigtableChannelPoolSettings settings, + ChannelFactory channelFactory, + ScheduledExecutorService executor) + throws IOException { + this.settings = settings; + this.channelFactory = channelFactory; + + ImmutableList.Builder initialListBuilder = ImmutableList.builder(); + + for (int i = 0; i < settings.getInitialChannelCount(); i++) { + initialListBuilder.add(new Entry(channelFactory.createSingleChannel())); + } + + entries.set(initialListBuilder.build()); + authority = entries.get().get(0).channel.authority(); + this.executor = executor; + + if (!settings.isStaticSize()) { + executor.scheduleAtFixedRate( + this::resizeSafely, + BigtableChannelPoolSettings.RESIZE_INTERVAL.getSeconds(), + BigtableChannelPoolSettings.RESIZE_INTERVAL.getSeconds(), + TimeUnit.SECONDS); + } + if (settings.isPreemptiveRefreshEnabled()) { + executor.scheduleAtFixedRate( + this::refreshSafely, + REFRESH_PERIOD.getSeconds(), + REFRESH_PERIOD.getSeconds(), + TimeUnit.SECONDS); + } + } + + /** {@inheritDoc} */ + @Override + public String authority() { + return authority; + } + + /** + * Create a {@link ClientCall} on a Channel from the pool chosen in a round-robin fashion to the + * remote operation specified by the given {@link MethodDescriptor}. The returned {@link + * ClientCall} does not trigger any remote behavior until {@link + * ClientCall#start(ClientCall.Listener, io.grpc.Metadata)} is invoked. + */ + @Override + public ClientCall newCall( + MethodDescriptor methodDescriptor, CallOptions callOptions) { + return getChannel(indexTicker.getAndIncrement()).newCall(methodDescriptor, callOptions); + } + + Channel getChannel(int affinity) { + return new AffinityChannel(affinity); + } + + /** {@inheritDoc} */ + @Override + public ManagedChannel shutdown() { + LOG.fine("Initiating graceful shutdown due to explicit request"); + + List localEntries = entries.get(); + for (Entry entry : localEntries) { + entry.channel.shutdown(); + } + if (executor != null) { + // shutdownNow will cancel scheduled tasks + executor.shutdownNow(); + } + return this; + } + + /** {@inheritDoc} */ + @Override + public boolean isShutdown() { + List localEntries = entries.get(); + for (Entry entry : localEntries) { + if (!entry.channel.isShutdown()) { + return false; + } + } + return executor == null || executor.isShutdown(); + } + + /** {@inheritDoc} */ + @Override + public boolean isTerminated() { + List localEntries = entries.get(); + for (Entry entry : localEntries) { + if (!entry.channel.isTerminated()) { + return false; + } + } + + return executor == null || executor.isTerminated(); + } + + /** {@inheritDoc} */ + @Override + public ManagedChannel shutdownNow() { + LOG.fine("Initiating immediate shutdown due to explicit request"); + + List localEntries = entries.get(); + for (Entry entry : localEntries) { + entry.channel.shutdownNow(); + } + if (executor != null) { + executor.shutdownNow(); + } + return this; + } + + /** {@inheritDoc} */ + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { + long endTimeNanos = System.nanoTime() + unit.toNanos(timeout); + List localEntries = entries.get(); + for (Entry entry : localEntries) { + long awaitTimeNanos = endTimeNanos - System.nanoTime(); + if (awaitTimeNanos <= 0) { + break; + } + entry.channel.awaitTermination(awaitTimeNanos, TimeUnit.NANOSECONDS); + } + if (executor != null) { + long awaitTimeNanos = endTimeNanos - System.nanoTime(); + executor.awaitTermination(awaitTimeNanos, TimeUnit.NANOSECONDS); + } + return isTerminated(); + } + + private void resizeSafely() { + try { + synchronized (entryWriteLock) { + resize(); + } + } catch (Exception e) { + LOG.log(Level.WARNING, "Failed to resize channel pool", e); + } + } + + /** + * Resize the number of channels based on the number of outstanding RPCs. + * + *

This method is expected to be called on a fixed interval. On every invocation it will: + * + *

    + *
  • Get the maximum number of outstanding RPCs since last invocation + *
  • Determine a valid range of number of channels to handle that many outstanding RPCs + *
  • If the current number of channel falls outside of that range, add or remove at most + * {@link BigtableChannelPoolSettings#MAX_RESIZE_DELTA} to get closer to middle of that + * range. + *
+ * + *

Not threadsafe, must be called under the entryWriteLock monitor + */ + @VisibleForTesting + void resize() { + List localEntries = entries.get(); + // Estimate the peak of RPCs in the last interval by summing the peak of RPCs per channel + int actualOutstandingRpcs = + localEntries.stream().mapToInt(Entry::getAndResetMaxOutstanding).sum(); + + // Number of channels if each channel operated at max capacity + int minChannels = + (int) Math.ceil(actualOutstandingRpcs / (double) settings.getMaxRpcsPerChannel()); + // Limit the threshold to absolute range + if (minChannels < settings.getMinChannelCount()) { + minChannels = settings.getMinChannelCount(); + } + + // Number of channels if each channel operated at minimum capacity + // Note: getMinRpcsPerChannel() can return 0, but division by 0 shouldn't cause a problem. + int maxChannels = + (int) Math.ceil(actualOutstandingRpcs / (double) settings.getMinRpcsPerChannel()); + // Limit the threshold to absolute range + if (maxChannels > settings.getMaxChannelCount()) { + maxChannels = settings.getMaxChannelCount(); + } + if (maxChannels < minChannels) { + maxChannels = minChannels; + } + + // If the pool were to be resized, try to aim for the middle of the bound, but limit rate of + // change. + int tentativeTarget = (maxChannels + minChannels) / 2; + int currentSize = localEntries.size(); + int delta = tentativeTarget - currentSize; + int dampenedTarget = tentativeTarget; + if (Math.abs(delta) > BigtableChannelPoolSettings.MAX_RESIZE_DELTA) { + dampenedTarget = + currentSize + (int) Math.copySign(BigtableChannelPoolSettings.MAX_RESIZE_DELTA, delta); + } + + // Only resize the pool when thresholds are crossed + if (localEntries.size() < minChannels) { + LOG.fine( + String.format( + "Detected throughput peak of %d, expanding channel pool size: %d -> %d.", + actualOutstandingRpcs, currentSize, dampenedTarget)); + + expand(dampenedTarget); + } else if (localEntries.size() > maxChannels) { + LOG.fine( + String.format( + "Detected throughput drop to %d, shrinking channel pool size: %d -> %d.", + actualOutstandingRpcs, currentSize, dampenedTarget)); + + shrink(dampenedTarget); + } + } + + /** Not threadsafe, must be called under the entryWriteLock monitor */ + private void shrink(int desiredSize) { + ImmutableList localEntries = entries.get(); + Preconditions.checkState( + localEntries.size() >= desiredSize, "current size is already smaller than the desired"); + + // Set the new list + entries.set(localEntries.subList(0, desiredSize)); + // clean up removed entries + List removed = localEntries.subList(desiredSize, localEntries.size()); + removed.forEach(Entry::requestShutdown); + } + + /** Not threadsafe, must be called under the entryWriteLock monitor */ + private void expand(int desiredSize) { + List localEntries = entries.get(); + Preconditions.checkState( + localEntries.size() <= desiredSize, "current size is already bigger than the desired"); + + ImmutableList.Builder newEntries = ImmutableList.builder().addAll(localEntries); + + for (int i = 0; i < desiredSize - localEntries.size(); i++) { + try { + newEntries.add(new Entry(channelFactory.createSingleChannel())); + } catch (IOException e) { + LOG.log(Level.WARNING, "Failed to add channel", e); + } + } + + entries.set(newEntries.build()); + } + + private void refreshSafely() { + try { + refresh(); + } catch (Exception e) { + LOG.log(Level.WARNING, "Failed to pre-emptively refresh channnels", e); + } + } + + /** + * Replace all of the channels in the channel pool with fresh ones. This is meant to mitigate the + * hourly GFE disconnects by giving clients the ability to prime the channel on reconnect. + * + *

This is done on a best effort basis. If the replacement channel fails to construct, the old + * channel will continue to be used. + */ + @InternalApi("Visible for testing") + void refresh() { + // Note: synchronization is necessary in case refresh is called concurrently: + // - thread1 fails to replace a single entry + // - thread2 succeeds replacing an entry + // - thread1 loses the race to replace the list + // - then thread2 will shut down channel that thread1 will put back into circulation (after it + // replaces the list) + synchronized (entryWriteLock) { + LOG.fine("Refreshing all channels"); + ArrayList newEntries = new ArrayList<>(entries.get()); + + for (int i = 0; i < newEntries.size(); i++) { + try { + newEntries.set(i, new Entry(channelFactory.createSingleChannel())); + } catch (IOException e) { + LOG.log(Level.WARNING, "Failed to refresh channel, leaving old channel", e); + } + } + + ImmutableList replacedEntries = entries.getAndSet(ImmutableList.copyOf(newEntries)); + + // Shutdown the channels that were cycled out. + for (Entry e : replacedEntries) { + if (!newEntries.contains(e)) { + e.requestShutdown(); + } + } + } + } + + /** + * Get and retain a Channel Entry. The returned Entry will have its rpc count incremented, + * preventing it from getting recycled. + */ + Entry getRetainedEntry(int affinity) { + // The maximum number of concurrent calls to this method for any given time span is at most 2, + // so the loop can actually be 2 times. But going for 5 times for a safety margin for potential + // code evolving + for (int i = 0; i < 5; i++) { + Entry entry = getEntry(affinity); + if (entry.retain()) { + return entry; + } + } + // It is unlikely to reach here unless the pool code evolves to increase the maximum possible + // concurrent calls to this method. If it does, this is a bug in the channel pool implementation + // the number of retries above should be greater than the number of contending maintenance + // tasks. + throw new IllegalStateException("Bug: failed to retain a channel"); + } + + /** + * Returns one of the channels managed by this pool. The pool continues to "own" the channel, and + * the caller should not shut it down. + * + * @param affinity Two calls to this method with the same affinity returns the same channel most + * of the time, if the channel pool was refreshed since the last call, a new channel will be + * returned. The reverse is not true: Two calls with different affinities might return the + * same channel. However, the implementation should attempt to spread load evenly. + */ + private Entry getEntry(int affinity) { + List localEntries = entries.get(); + + int index = Math.abs(affinity % localEntries.size()); + + return localEntries.get(index); + } + + /** Bundles a gRPC {@link ManagedChannel} with some usage accounting. */ + static class Entry { + private final ManagedChannel channel; + + /** + * The primary purpose of keeping a count for outstanding RPCs is to track when a channel is + * safe to close. In grpc, initialization & starting of rpcs is split between 2 methods: + * Channel#newCall() and ClientCall#start. gRPC already has a mechanism to safely close channels + * that have rpcs that have been started. However, it does not protect calls that have been + * created but not started. In the sequence: Channel#newCall() Channel#shutdown() + * ClientCall#Start(), gRpc will error out the call telling the caller that the channel is + * shutdown. + * + *

Hence, the increment of outstanding RPCs has to happen when the ClientCall is initialized, + * as part of Channel#newCall(), not after the ClientCall is started. The decrement of + * outstanding RPCs has to happen when the ClientCall is closed or the ClientCall failed to + * start. + */ + @VisibleForTesting final AtomicInteger outstandingRpcs = new AtomicInteger(0); + + private final AtomicInteger maxOutstanding = new AtomicInteger(); + + // Flag that the channel should be closed once all of the outstanding RPC complete. + private final AtomicBoolean shutdownRequested = new AtomicBoolean(); + // Flag that the channel has been closed. + private final AtomicBoolean shutdownInitiated = new AtomicBoolean(); + + private Entry(ManagedChannel channel) { + this.channel = channel; + } + + int getAndResetMaxOutstanding() { + return maxOutstanding.getAndSet(outstandingRpcs.get()); + } + + /** + * Try to increment the outstanding RPC count. The method will return false if the channel is + * closing and the caller should pick a different channel. If the method returned true, the + * channel has been successfully retained and it is the responsibility of the caller to release + * it. + */ + private boolean retain() { + // register desire to start RPC + int currentOutstanding = outstandingRpcs.incrementAndGet(); + + // Rough book keeping + int prevMax = maxOutstanding.get(); + if (currentOutstanding > prevMax) { + maxOutstanding.incrementAndGet(); + } + + // abort if the channel is closing + if (shutdownRequested.get()) { + release(); + return false; + } + return true; + } + + /** + * Notify the channel that the number of outstanding RPCs has decreased. If shutdown has been + * previously requested, this method will shutdown the channel if its the last outstanding RPC. + */ + private void release() { + int newCount = outstandingRpcs.decrementAndGet(); + if (newCount < 0) { + LOG.log(Level.WARNING, "Bug! Reference count is negative (" + newCount + ")!"); + } + + // Must check outstandingRpcs after shutdownRequested (in reverse order of retain()) to ensure + // mutual exclusion. + if (shutdownRequested.get() && outstandingRpcs.get() == 0) { + shutdown(); + } + } + + /** + * Request a shutdown. The actual shutdown will be delayed until there are no more outstanding + * RPCs. + */ + private void requestShutdown() { + shutdownRequested.set(true); + if (outstandingRpcs.get() == 0) { + shutdown(); + } + } + + /** Ensure that shutdown is only called once. */ + private void shutdown() { + if (shutdownInitiated.compareAndSet(false, true)) { + channel.shutdown(); + } + } + } + + /** Thin wrapper to ensure that new calls are properly reference counted. */ + private class AffinityChannel extends Channel { + private final int affinity; + + public AffinityChannel(int affinity) { + this.affinity = affinity; + } + + @Override + public String authority() { + return authority; + } + + @Override + public ClientCall newCall( + MethodDescriptor methodDescriptor, CallOptions callOptions) { + + Entry entry = getRetainedEntry(affinity); + + return new ReleasingClientCall<>(entry.channel.newCall(methodDescriptor, callOptions), entry); + } + } + + /** ClientCall wrapper that makes sure to decrement the outstanding RPC count on completion. */ + static class ReleasingClientCall extends SimpleForwardingClientCall { + @Nullable private CancellationException cancellationException; + final Entry entry; + private final AtomicBoolean wasClosed = new AtomicBoolean(); + private final AtomicBoolean wasReleased = new AtomicBoolean(); + + public ReleasingClientCall(ClientCall delegate, Entry entry) { + super(delegate); + this.entry = entry; + } + + @Override + public void start(Listener responseListener, Metadata headers) { + if (cancellationException != null) { + throw new IllegalStateException("Call is already cancelled", cancellationException); + } + try { + super.start( + new SimpleForwardingClientCallListener(responseListener) { + @Override + public void onClose(Status status, Metadata trailers) { + if (!wasClosed.compareAndSet(false, true)) { + LOG.log( + Level.WARNING, + "Call is being closed more than once. Please make sure that onClose() is not" + + " being manually called."); + return; + } + try { + super.onClose(status, trailers); + } finally { + if (wasReleased.compareAndSet(false, true)) { + entry.release(); + } else { + LOG.log( + Level.WARNING, + "Entry was released before the call is closed. This may be due to an" + + " exception on start of the call."); + } + } + } + }, + headers); + } catch (Exception e) { + // In case start failed, make sure to release + if (wasReleased.compareAndSet(false, true)) { + entry.release(); + } else { + LOG.log( + Level.WARNING, + "The entry is already released. This indicates that onClose() has already been called" + + " previously"); + } + throw e; + } + } + + @Override + public void cancel(@Nullable String message, @Nullable Throwable cause) { + this.cancellationException = new CancellationException(message); + super.cancel(message, cause); + } + } +} diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableChannelPoolSettings.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableChannelPoolSettings.java new file mode 100644 index 0000000000..9ea4973900 --- /dev/null +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/BigtableChannelPoolSettings.java @@ -0,0 +1,171 @@ +/* + * Copyright 2025 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.gaxx.grpc; + +import com.google.api.core.BetaApi; +import com.google.auto.value.AutoValue; +import com.google.common.base.Preconditions; +import java.time.Duration; + +/** + * Settings to control {@link BigtableChannelPool} behavior. + * + *

To facilitate low latency/high throughout applications, gax provides a {@link + * BigtableChannelPool}. The pool is meant to facilitate high throughput/low latency clients. By + * splitting load across multiple gRPC channels the client can spread load across multiple frontends + * and overcome gRPC's limit of 100 concurrent RPCs per channel. However oversizing the {@link + * BigtableChannelPool} can lead to underutilized channels which will lead to high tail latency due + * to GFEs disconnecting idle channels. + * + *

The {@link BigtableChannelPool} is designed to adapt to varying traffic patterns by tracking + * outstanding RPCs and resizing the pool size. This class configures the behavior. In general + * clients should aim to have less than 50 concurrent RPCs per channel and at least 1 outstanding + * per channel per minute. + * + *

The settings in this class will be applied every minute. + */ +@BetaApi("surface for channel pool sizing is not yet stable") +@AutoValue +public abstract class BigtableChannelPoolSettings { + /** How often to check and possibly resize the {@link BigtableChannelPool}. */ + static final Duration RESIZE_INTERVAL = Duration.ofMinutes(1); + + /** The maximum number of channels that can be added or removed at a time. */ + static final int MAX_RESIZE_DELTA = 2; + + /** + * Threshold to start scaling down the channel pool. + * + *

When the average of the maximum number of outstanding RPCs in a single minute drop below + * this threshold, channels will be removed from the pool. + */ + public abstract int getMinRpcsPerChannel(); + + /** + * Threshold to start scaling up the channel pool. + * + *

When the average of the maximum number of outstanding RPCs in a single minute surpass this + * threshold, channels will be added to the pool. For google services, gRPC channels will start + * locally queuing RPC when there are 100 concurrent RPCs. + */ + public abstract int getMaxRpcsPerChannel(); + + /** + * The absolute minimum size of the channel pool. + * + *

Regardless of the current throughput, the number of channels will not drop below this limit + */ + public abstract int getMinChannelCount(); + + /** + * The absolute maximum size of the channel pool. + * + *

Regardless of the current throughput, the number of channels will not exceed this limit + */ + public abstract int getMaxChannelCount(); + + /** + * The initial size of the channel pool. + * + *

During client construction the client open this many connections. This will be scaled up or + * down in the next period. + */ + public abstract int getInitialChannelCount(); + + /** + * If all of the channels should be replaced on an hourly basis. + * + *

The GFE will forcibly disconnect active channels after an hour. To minimize the cost of + * reconnects, this will create a new channel asynchronuously, prime it and then swap it with an + * old channel. + */ + public abstract boolean isPreemptiveRefreshEnabled(); + + /** + * Helper to check if the {@link BigtableChannelPool} implementation can skip dynamic size logic + */ + boolean isStaticSize() { + // When range is restricted to a single size + if (getMinChannelCount() == getMaxChannelCount()) { + return true; + } + // When the scaling threshold are not set + if (getMinRpcsPerChannel() == 0 && getMaxRpcsPerChannel() == Integer.MAX_VALUE) { + return true; + } + + return false; + } + + public abstract Builder toBuilder(); + + public static BigtableChannelPoolSettings staticallySized(int size) { + return builder() + .setInitialChannelCount(size) + .setMinRpcsPerChannel(0) + .setMaxRpcsPerChannel(Integer.MAX_VALUE) + .setMinChannelCount(size) + .setMaxChannelCount(size) + .build(); + } + + public static Builder builder() { + return new AutoValue_BigtableChannelPoolSettings.Builder() + .setInitialChannelCount(1) + .setMinChannelCount(1) + .setMaxChannelCount(200) + .setMinRpcsPerChannel(0) + .setMaxRpcsPerChannel(Integer.MAX_VALUE) + .setPreemptiveRefreshEnabled(false); + } + + @AutoValue.Builder + public abstract static class Builder { + public abstract Builder setMinRpcsPerChannel(int count); + + public abstract Builder setMaxRpcsPerChannel(int count); + + public abstract Builder setMinChannelCount(int count); + + public abstract Builder setMaxChannelCount(int count); + + public abstract Builder setInitialChannelCount(int count); + + public abstract Builder setPreemptiveRefreshEnabled(boolean enabled); + + abstract BigtableChannelPoolSettings autoBuild(); + + public BigtableChannelPoolSettings build() { + BigtableChannelPoolSettings s = autoBuild(); + + Preconditions.checkState( + s.getMinRpcsPerChannel() <= s.getMaxRpcsPerChannel(), "rpcsPerChannel range is invalid"); + Preconditions.checkState( + s.getMinChannelCount() > 0, "Minimum channel count must be at least 1"); + Preconditions.checkState( + s.getMinChannelCount() <= s.getMaxRpcsPerChannel(), "absolute channel range is invalid"); + Preconditions.checkState( + s.getMinChannelCount() <= s.getInitialChannelCount(), + "initial channel count be at least minChannelCount"); + Preconditions.checkState( + s.getInitialChannelCount() <= s.getMaxChannelCount(), + "initial channel count must be less than maxChannelCount"); + Preconditions.checkState( + s.getInitialChannelCount() > 0, "Initial channel count must be greater than 0"); + return s; + } + } +} diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/it/BigtableLogicalViewIT.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/it/BigtableLogicalViewIT.java index 83ca4e4c09..d73d68d48f 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/it/BigtableLogicalViewIT.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/it/BigtableLogicalViewIT.java @@ -20,6 +20,7 @@ import static com.google.common.truth.TruthJUnit.assume; import static org.junit.Assert.fail; +import com.google.api.gax.rpc.FailedPreconditionException; import com.google.api.gax.rpc.NotFoundException; import com.google.cloud.bigtable.admin.v2.BigtableInstanceAdminClient; import com.google.cloud.bigtable.admin.v2.BigtableTableAdminClient; @@ -37,14 +38,12 @@ import org.junit.Before; import org.junit.BeforeClass; import org.junit.ClassRule; -import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @RunWith(JUnit4.class) -@Ignore("Not fully working yet in production") public class BigtableLogicalViewIT { @ClassRule public static final TestEnvRule testEnvRule = new TestEnvRule(); @Rule public final PrefixGenerator prefixGenerator = new PrefixGenerator(); @@ -77,12 +76,17 @@ public void createLogicalViewAndGetLogicalViewTest() { String logicalViewId = prefixGenerator.newPrefix(); CreateLogicalViewRequest request = - CreateLogicalViewRequest.of(instanceId, logicalViewId).setQuery(getQuery()); + CreateLogicalViewRequest.of(instanceId, logicalViewId) + .setQuery(getQuery()) + .setDeletionProtection(false); try { LogicalView response = client.createLogicalView(request); assertWithMessage("Got wrong logical view Id in CreateLogicalView") .that(response.getId()) .isEqualTo(logicalViewId); + assertWithMessage("Got wrong deletion protection in CreateLogicalView") + .that(response.isDeletionProtected()) + .isFalse(); assertWithMessage("Got wrong query in CreateLogicalView") .that(response.getQuery()) .isEqualTo(getQuery()); @@ -91,6 +95,9 @@ public void createLogicalViewAndGetLogicalViewTest() { assertWithMessage("Got wrong logical view Id in getLogicalView") .that(response.getId()) .isEqualTo(logicalViewId); + assertWithMessage("Got wrong deletion protection in getLogicalView") + .that(response.isDeletionProtected()) + .isFalse(); assertWithMessage("Got wrong query in getLogicalView") .that(response.getQuery()) .isEqualTo(getQuery()); @@ -119,15 +126,33 @@ public void listLogicalViewsTest() { public void updateLogicalViewAndDeleteLogicalViewTest() throws InterruptedException { String logicalViewId = prefixGenerator.newPrefix(); - // Create a logical view. - CreateLogicalViewRequest request = createLogicalViewRequest(logicalViewId); + // Create a deletion-protected logical view. + CreateLogicalViewRequest request = + createLogicalViewRequest(logicalViewId).setDeletionProtection(true); LogicalView response = client.createLogicalView(request); + assertWithMessage("Got wrong deletion protection in CreateLogicalView") + .that(response.isDeletionProtected()) + .isTrue(); + + // We should not be able to delete the logical view. + try { + client.deleteLogicalView(instanceId, logicalViewId); + fail("A delete-protected logical view should not have been able to be deleted"); + } catch (FailedPreconditionException e) { + assertWithMessage("Incorrect exception type") + .that(e.getCause()) + .isInstanceOf(StatusRuntimeException.class); + } - // Update the query of the logical view. + // Update the deletion protection bit and query of the logical view. String query = "SELECT 1 AS value"; - UpdateLogicalViewRequest updateRequest = UpdateLogicalViewRequest.of(response).setQuery(query); + UpdateLogicalViewRequest updateRequest = + UpdateLogicalViewRequest.of(response).setQuery(query).setDeletionProtection(false); response = client.updateLogicalView(updateRequest); + assertWithMessage("Got wrong deletion protection in UpdateLogicalView") + .that(response.isDeletionProtected()) + .isFalse(); assertWithMessage("Got wrong query in UpdateLogicalView") .that(response.getQuery()) .isEqualTo(query); diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/it/BigtableMaterializedViewIT.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/it/BigtableMaterializedViewIT.java index e9670aeef7..62a2f794b0 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/it/BigtableMaterializedViewIT.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/it/BigtableMaterializedViewIT.java @@ -24,28 +24,31 @@ import com.google.api.gax.rpc.NotFoundException; import com.google.cloud.bigtable.admin.v2.BigtableInstanceAdminClient; import com.google.cloud.bigtable.admin.v2.BigtableTableAdminClient; +import com.google.cloud.bigtable.admin.v2.models.CreateInstanceRequest; import com.google.cloud.bigtable.admin.v2.models.CreateMaterializedViewRequest; import com.google.cloud.bigtable.admin.v2.models.CreateTableRequest; +import com.google.cloud.bigtable.admin.v2.models.Instance; import com.google.cloud.bigtable.admin.v2.models.MaterializedView; +import com.google.cloud.bigtable.admin.v2.models.StorageType; import com.google.cloud.bigtable.admin.v2.models.Table; import com.google.cloud.bigtable.admin.v2.models.UpdateMaterializedViewRequest; import com.google.cloud.bigtable.test_helpers.env.EmulatorEnv; import com.google.cloud.bigtable.test_helpers.env.PrefixGenerator; import com.google.cloud.bigtable.test_helpers.env.TestEnvRule; import io.grpc.StatusRuntimeException; +import java.io.IOException; import java.util.List; import java.util.logging.Logger; +import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.ClassRule; -import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @RunWith(JUnit4.class) -@Ignore("Not fully working yet in production") public class BigtableMaterializedViewIT { @ClassRule public static final TestEnvRule testEnvRule = new TestEnvRule(); @Rule public final PrefixGenerator prefixGenerator = new PrefixGenerator(); @@ -53,24 +56,44 @@ public class BigtableMaterializedViewIT { private static final int[] BACKOFF_DURATION = {2, 4, 8, 16, 32, 64, 128, 256, 512, 1024}; private static BigtableInstanceAdminClient client; + private static BigtableTableAdminClient tableAdminClient; private static Table testTable; - - private String instanceId = testEnvRule.env().getInstanceId(); + private static String instanceId = ""; // TODO: Update this test once emulator supports InstanceAdmin operation // https://github.com/googleapis/google-cloud-go/issues/1069 @BeforeClass - public static void validatePlatform() { + public static void validatePlatform() throws IOException { assume() .withMessage("BigtableInstanceAdminClient doesn't support on Emulator") .that(testEnvRule.env()) .isNotInstanceOf(EmulatorEnv.class); + + createInstance(); + } + + public static void createInstance() throws IOException { + client = testEnvRule.env().getInstanceAdminClient(); + + Instance instance = + client.createInstance( + CreateInstanceRequest.of(new PrefixGenerator().newPrefix()) + .addCluster("my-cluster", "us-east1-c", 3, StorageType.SSD)); + instanceId = instance.getId(); + tableAdminClient = + BigtableTableAdminClient.create(testEnvRule.env().getProjectId(), instanceId); + } + + @AfterClass + public static void deleteInstance() { + if (!instanceId.isEmpty()) { + client.deleteInstance(instanceId); + } } @Before public void setUp() throws InterruptedException { - client = testEnvRule.env().getInstanceAdminClient(); - testTable = createTestTable(testEnvRule.env().getTableAdminClient()); + testTable = createTestTable(tableAdminClient); } @Test diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/CreateLogicalViewRequestTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/CreateLogicalViewRequestTest.java index ec5f6af14f..eededde65b 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/CreateLogicalViewRequestTest.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/CreateLogicalViewRequestTest.java @@ -33,13 +33,18 @@ public class CreateLogicalViewRequestTest { public void testToProto() { String query = "SELECT * FROM Table"; CreateLogicalViewRequest request = - CreateLogicalViewRequest.of(INSTANCE_ID, LOGICAL_VIEW_ID).setQuery(query); + CreateLogicalViewRequest.of(INSTANCE_ID, LOGICAL_VIEW_ID) + .setQuery(query) + .setDeletionProtection(true); com.google.bigtable.admin.v2.CreateLogicalViewRequest requestProto = com.google.bigtable.admin.v2.CreateLogicalViewRequest.newBuilder() .setParent(NameUtil.formatInstanceName(PROJECT_ID, INSTANCE_ID)) .setLogicalViewId(LOGICAL_VIEW_ID) - .setLogicalView(com.google.bigtable.admin.v2.LogicalView.newBuilder().setQuery(query)) + .setLogicalView( + com.google.bigtable.admin.v2.LogicalView.newBuilder() + .setQuery(query) + .setDeletionProtection(true)) .build(); assertThat(request.toProto(PROJECT_ID)).isEqualTo(requestProto); } @@ -47,30 +52,42 @@ public void testToProto() { @Test public void testEquality() { CreateLogicalViewRequest request = - CreateLogicalViewRequest.of(INSTANCE_ID, LOGICAL_VIEW_ID).setQuery("test 1"); + CreateLogicalViewRequest.of(INSTANCE_ID, LOGICAL_VIEW_ID) + .setQuery("test 1") + .setDeletionProtection(true); assertThat(request) - .isEqualTo(CreateLogicalViewRequest.of(INSTANCE_ID, LOGICAL_VIEW_ID).setQuery("test 1")); + .isEqualTo( + CreateLogicalViewRequest.of(INSTANCE_ID, LOGICAL_VIEW_ID) + .setQuery("test 1") + .setDeletionProtection(true)); assertThat(request) - .isNotEqualTo(CreateLogicalViewRequest.of(INSTANCE_ID, LOGICAL_VIEW_ID).setQuery("test 2")); + .isNotEqualTo( + CreateLogicalViewRequest.of(INSTANCE_ID, LOGICAL_VIEW_ID) + .setQuery("test 2") + .setDeletionProtection(true)); } @Test public void testHashCode() { CreateLogicalViewRequest request = - CreateLogicalViewRequest.of(INSTANCE_ID, LOGICAL_VIEW_ID).setQuery("test 1"); + CreateLogicalViewRequest.of(INSTANCE_ID, LOGICAL_VIEW_ID) + .setQuery("test 1") + .setDeletionProtection(true); assertThat(request.hashCode()) .isEqualTo( CreateLogicalViewRequest.of(INSTANCE_ID, LOGICAL_VIEW_ID) .setQuery("test 1") + .setDeletionProtection(true) .hashCode()); assertThat(request.hashCode()) .isNotEqualTo( CreateLogicalViewRequest.of(INSTANCE_ID, LOGICAL_VIEW_ID) .setQuery("test 2") + .setDeletionProtection(true) .hashCode()); } } diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/LogicalViewTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/LogicalViewTest.java index 8b802ec8d7..7a17aaecf8 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/LogicalViewTest.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/LogicalViewTest.java @@ -37,12 +37,14 @@ public void testFromProto() { com.google.bigtable.admin.v2.LogicalView.newBuilder() .setName(logicalViewName.toString()) .setQuery("SELECT 1 from Table") + .setDeletionProtection(true) .build(); LogicalView result = LogicalView.fromProto(logicalViewProto); assertThat(result.getId()).isEqualTo(LOGICAL_VIEW_ID); assertThat(result.getQuery()).isEqualTo("SELECT 1 from Table"); + assertThat(result.isDeletionProtected()).isEqualTo(true); } @Test @@ -70,6 +72,7 @@ public void testEquality() { com.google.bigtable.admin.v2.LogicalView.newBuilder() .setName(logicalViewName.toString()) .setQuery("SELECT 1 FROM Table") + .setDeletionProtection(true) .build(); LogicalView logicalView = LogicalView.fromProto(proto); @@ -80,6 +83,7 @@ public void testEquality() { com.google.bigtable.admin.v2.LogicalView.newBuilder() .setName(logicalViewName.toString()) .setQuery("SELECT 2 FROM Table") + .setDeletionProtection(true) .build()); } @@ -90,6 +94,7 @@ public void testHashCode() { com.google.bigtable.admin.v2.LogicalView.newBuilder() .setName(logicalViewName.toString()) .setQuery("SELECT 1 FROM Table") + .setDeletionProtection(true) .build(); LogicalView logicalView = LogicalView.fromProto(proto); @@ -100,6 +105,7 @@ public void testHashCode() { com.google.bigtable.admin.v2.LogicalView.newBuilder() .setName(logicalViewName.toString()) .setQuery("SELECT 2 FROM Table") + .setDeletionProtection(true) .build() .hashCode()); } diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/UpdateLogicalViewRequestTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/UpdateLogicalViewRequestTest.java index 6421d9cf56..da54bb5ac1 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/UpdateLogicalViewRequestTest.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/UpdateLogicalViewRequestTest.java @@ -33,16 +33,20 @@ public class UpdateLogicalViewRequestTest { @Test public void testToProto() { UpdateLogicalViewRequest request = - UpdateLogicalViewRequest.of(INSTANCE_ID, LOGICAL_VIEW_ID).setQuery("query 1"); + UpdateLogicalViewRequest.of(INSTANCE_ID, LOGICAL_VIEW_ID) + .setQuery("query 1") + .setDeletionProtection(true); com.google.bigtable.admin.v2.UpdateLogicalViewRequest requestProto = com.google.bigtable.admin.v2.UpdateLogicalViewRequest.newBuilder() .setLogicalView( com.google.bigtable.admin.v2.LogicalView.newBuilder() .setQuery("query 1") + .setDeletionProtection(true) .setName( NameUtil.formatLogicalViewName(PROJECT_ID, INSTANCE_ID, LOGICAL_VIEW_ID))) - .setUpdateMask(FieldMask.newBuilder().addPaths("query").build()) + .setUpdateMask( + FieldMask.newBuilder().addPaths("deletion_protection").addPaths("query").build()) .build(); assertThat(request.toProto(PROJECT_ID)).isEqualTo(requestProto); } @@ -50,31 +54,42 @@ public void testToProto() { @Test public void testEquality() { UpdateLogicalViewRequest request = - UpdateLogicalViewRequest.of(INSTANCE_ID, LOGICAL_VIEW_ID).setQuery("query 1"); + UpdateLogicalViewRequest.of(INSTANCE_ID, LOGICAL_VIEW_ID) + .setQuery("query 1") + .setDeletionProtection(true); assertThat(request) - .isEqualTo(UpdateLogicalViewRequest.of(INSTANCE_ID, LOGICAL_VIEW_ID).setQuery("query 1")); + .isEqualTo( + UpdateLogicalViewRequest.of(INSTANCE_ID, LOGICAL_VIEW_ID) + .setQuery("query 1") + .setDeletionProtection(true)); assertThat(request) .isNotEqualTo( - UpdateLogicalViewRequest.of(INSTANCE_ID, LOGICAL_VIEW_ID).setQuery("query 2")); + UpdateLogicalViewRequest.of(INSTANCE_ID, LOGICAL_VIEW_ID) + .setQuery("query 2") + .setDeletionProtection(true)); } @Test public void testHashCode() { UpdateLogicalViewRequest request = - UpdateLogicalViewRequest.of(INSTANCE_ID, LOGICAL_VIEW_ID).setQuery("query 1"); + UpdateLogicalViewRequest.of(INSTANCE_ID, LOGICAL_VIEW_ID) + .setQuery("query 1") + .setDeletionProtection(true); assertThat(request.hashCode()) .isEqualTo( UpdateLogicalViewRequest.of(INSTANCE_ID, LOGICAL_VIEW_ID) .setQuery("query 1") + .setDeletionProtection(true) .hashCode()); assertThat(request.hashCode()) .isNotEqualTo( UpdateLogicalViewRequest.of(INSTANCE_ID, LOGICAL_VIEW_ID) .setQuery("query 2") + .setDeletionProtection(true) .hashCode()); } } diff --git a/grpc-google-cloud-bigtable-admin-v2/pom.xml b/grpc-google-cloud-bigtable-admin-v2/pom.xml index 8fb7eabb51..d402413611 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.58.2 + 2.59.0 grpc-google-cloud-bigtable-admin-v2 GRPC library for grpc-google-cloud-bigtable-admin-v2 com.google.cloud google-cloud-bigtable-parent - 2.58.2 + 2.59.0 @@ -18,14 +18,14 @@ com.google.cloud google-cloud-bigtable-deps-bom - 2.58.2 + 2.59.0 pom import com.google.cloud google-cloud-bigtable-bom - 2.58.2 + 2.59.0 pom import diff --git a/grpc-google-cloud-bigtable-v2/pom.xml b/grpc-google-cloud-bigtable-v2/pom.xml index 2facc1c979..7249678c87 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.58.2 + 2.59.0 grpc-google-cloud-bigtable-v2 GRPC library for grpc-google-cloud-bigtable-v2 com.google.cloud google-cloud-bigtable-parent - 2.58.2 + 2.59.0 @@ -18,14 +18,14 @@ com.google.cloud google-cloud-bigtable-deps-bom - 2.58.2 + 2.59.0 pom import com.google.cloud google-cloud-bigtable-bom - 2.58.2 + 2.59.0 pom import diff --git a/pom.xml b/pom.xml index 5e479101c4..a461ba3d18 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ google-cloud-bigtable-parent pom - 2.58.2 + 2.59.0 Google Cloud Bigtable Parent https://github.com/googleapis/java-bigtable @@ -14,7 +14,7 @@ com.google.cloud sdk-platform-java-config - 3.47.0 + 3.48.0 @@ -153,27 +153,27 @@ com.google.api.grpc proto-google-cloud-bigtable-v2 - 2.58.2 + 2.59.0 com.google.api.grpc proto-google-cloud-bigtable-admin-v2 - 2.58.2 + 2.59.0 com.google.api.grpc grpc-google-cloud-bigtable-v2 - 2.58.2 + 2.59.0 com.google.api.grpc grpc-google-cloud-bigtable-admin-v2 - 2.58.2 + 2.59.0 com.google.cloud google-cloud-bigtable - 2.58.2 + 2.59.0 diff --git a/proto-google-cloud-bigtable-admin-v2/pom.xml b/proto-google-cloud-bigtable-admin-v2/pom.xml index d263222608..8db755867e 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.58.2 + 2.59.0 proto-google-cloud-bigtable-admin-v2 PROTO library for proto-google-cloud-bigtable-admin-v2 com.google.cloud google-cloud-bigtable-parent - 2.58.2 + 2.59.0 @@ -18,14 +18,14 @@ com.google.cloud google-cloud-bigtable-deps-bom - 2.58.2 + 2.59.0 pom import com.google.cloud google-cloud-bigtable-bom - 2.58.2 + 2.59.0 pom import diff --git a/proto-google-cloud-bigtable-v2/pom.xml b/proto-google-cloud-bigtable-v2/pom.xml index 86cd3562a7..1d2aba67bf 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.58.2 + 2.59.0 proto-google-cloud-bigtable-v2 PROTO library for proto-google-cloud-bigtable-v2 com.google.cloud google-cloud-bigtable-parent - 2.58.2 + 2.59.0 @@ -18,14 +18,14 @@ com.google.cloud google-cloud-bigtable-deps-bom - 2.58.2 + 2.59.0 pom import com.google.cloud google-cloud-bigtable-bom - 2.58.2 + 2.59.0 pom import diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml index 74660b0ed8..3fec5a19bc 100644 --- a/samples/snapshot/pom.xml +++ b/samples/snapshot/pom.xml @@ -28,7 +28,7 @@ com.google.cloud google-cloud-bigtable - 2.58.2 + 2.59.0 diff --git a/test-proxy/pom.xml b/test-proxy/pom.xml index 728817b276..7f982a2058 100644 --- a/test-proxy/pom.xml +++ b/test-proxy/pom.xml @@ -12,11 +12,11 @@ google-cloud-bigtable-parent com.google.cloud - 2.58.2 + 2.59.0 - 2.58.2 + 2.59.0 diff --git a/versions.txt b/versions.txt index 8e5d778469..12f079846a 100644 --- a/versions.txt +++ b/versions.txt @@ -1,10 +1,10 @@ # Format: # module:released-version:current-version -google-cloud-bigtable:2.58.2:2.58.2 -grpc-google-cloud-bigtable-admin-v2:2.58.2:2.58.2 -grpc-google-cloud-bigtable-v2:2.58.2:2.58.2 -proto-google-cloud-bigtable-admin-v2:2.58.2:2.58.2 -proto-google-cloud-bigtable-v2:2.58.2:2.58.2 -google-cloud-bigtable-emulator:0.195.2:0.195.2 -google-cloud-bigtable-emulator-core:0.195.2:0.195.2 +google-cloud-bigtable:2.59.0:2.59.0 +grpc-google-cloud-bigtable-admin-v2:2.59.0:2.59.0 +grpc-google-cloud-bigtable-v2:2.59.0:2.59.0 +proto-google-cloud-bigtable-admin-v2:2.59.0:2.59.0 +proto-google-cloud-bigtable-v2:2.59.0:2.59.0 +google-cloud-bigtable-emulator:0.196.0:0.196.0 +google-cloud-bigtable-emulator-core:0.196.0:0.196.0