diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/BigtableDataSettings.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/BigtableDataSettings.java index bc60362b85..ff97408809 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/BigtableDataSettings.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/BigtableDataSettings.java @@ -184,6 +184,12 @@ public String getAppProfileId() { return stubSettings.getAppProfileId(); } + /** Gets if channels will gracefully refresh connections to Cloud Bigtable service */ + @BetaApi("This API depends on experimental gRPC APIs") + public boolean isRefreshingChannel() { + return stubSettings.isRefreshingChannel(); + } + /** Returns the underlying RPC settings. */ public EnhancedBigtableStubSettings getStubSettings() { return stubSettings; @@ -275,6 +281,26 @@ public CredentialsProvider getCredentialsProvider() { return stubSettings.getCredentialsProvider(); } + /** + * Configure periodic gRPC channel refreshes. + * + *

This feature will gracefully refresh connections to the Cloud Bigtable service. This is an + * experimental feature to address tail latency caused by the service dropping long lived gRPC + * connections, which causes the client to renegotiate the gRPC connection in the request path, + * which causes periodic spikes in latency + */ + @BetaApi("This API depends on experimental gRPC APIs") + public Builder setRefreshingChannel(boolean isRefreshingChannel) { + stubSettings.setRefreshingChannel(isRefreshingChannel); + return this; + } + + /** Gets if channels will gracefully refresh connections to Cloud Bigtable service */ + @BetaApi("This API depends on experimental gRPC APIs") + public boolean isRefreshingChannel() { + return stubSettings.isRefreshingChannel(); + } + /** * Returns the underlying settings for making RPC calls. The settings should be changed with * care. diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/RefreshChannel.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/RefreshChannel.java new file mode 100644 index 0000000000..e34ecd750d --- /dev/null +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/RefreshChannel.java @@ -0,0 +1,54 @@ +/* + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.cloud.bigtable.data.v2.internal; + +import com.google.api.core.BetaApi; +import com.google.api.core.InternalApi; +import com.google.api.gax.grpc.ChannelPrimer; +import io.grpc.ConnectivityState; +import io.grpc.ManagedChannel; +import java.util.concurrent.TimeUnit; + +/** + * Establish a connection to the Cloud Bigtable service on managedChannel + * + *

This class is considered an internal implementation detail and not meant to be used by + * applications. + */ +@BetaApi("This API depends on gRPC experimental API") +@InternalApi +public final class RefreshChannel implements ChannelPrimer { + + /** + * primeChannel establishes a connection to Cloud Bigtable service. This typically take less than + * 1s. In case of service failure, an upper limit of 10s prevents primeChannel from looping + * forever. + */ + @Override + public void primeChannel(ManagedChannel managedChannel) { + for (int i = 0; i < 10; i++) { + ConnectivityState connectivityState = managedChannel.getState(true); + if (connectivityState == ConnectivityState.READY) { + break; + } + try { + TimeUnit.SECONDS.sleep(1); + } catch (InterruptedException e) { + break; + } + } + } +} diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java index a23cf780c5..11d4726080 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java @@ -15,6 +15,7 @@ */ package com.google.cloud.bigtable.data.v2.stub; +import com.google.api.core.BetaApi; import com.google.api.gax.batching.BatchingSettings; import com.google.api.gax.batching.FlowControlSettings; import com.google.api.gax.batching.FlowController.LimitExceededBehavior; @@ -29,6 +30,7 @@ import com.google.api.gax.rpc.TransportChannelProvider; import com.google.api.gax.rpc.UnaryCallSettings; import com.google.api.gax.tracing.OpencensusTracerFactory; +import com.google.cloud.bigtable.data.v2.internal.RefreshChannel; import com.google.cloud.bigtable.data.v2.models.ConditionalRowMutation; import com.google.cloud.bigtable.data.v2.models.KeyOffset; import com.google.cloud.bigtable.data.v2.models.Query; @@ -149,6 +151,7 @@ public class EnhancedBigtableStubSettings extends StubSettings readRowsSettings; private final UnaryCallSettings readRowSettings; @@ -179,6 +182,7 @@ private EnhancedBigtableStubSettings(Builder builder) { projectId = builder.projectId; instanceId = builder.instanceId; appProfileId = builder.appProfileId; + isRefreshingChannel = builder.isRefreshingChannel; // Per method settings. readRowsSettings = builder.readRowsSettings.build(); @@ -210,6 +214,12 @@ public String getAppProfileId() { return appProfileId; } + /** Returns if channels will gracefully refresh connections to Cloud Bigtable service */ + @BetaApi("This API depends on experimental gRPC APIs") + public boolean isRefreshingChannel() { + return isRefreshingChannel; + } + /** Returns a builder for the default ChannelProvider for this service. */ public static InstantiatingGrpcChannelProvider.Builder defaultGrpcTransportProviderBuilder() { return BigtableStubSettings.defaultGrpcTransportProviderBuilder() @@ -413,6 +423,7 @@ public static class Builder extends StubSettings.Builder readRowsSettings; private final UnaryCallSettings.Builder readRowSettings; @@ -433,6 +444,7 @@ public static class Builder extends StubSettings.Builder readRowsSettings() { return readRowsSettings; @@ -642,6 +672,18 @@ public EnhancedBigtableStubSettings build() { Preconditions.checkState(projectId != null, "Project id must be set"); Preconditions.checkState(instanceId != null, "Instance id must be set"); + // Set ChannelPrimer on TransportChannelProvider so channels will gracefully refresh + // connections to Cloud Bigtable service + if (isRefreshingChannel) { + Preconditions.checkArgument( + getTransportChannelProvider() instanceof InstantiatingGrpcChannelProvider, + "refreshingChannel only works with InstantiatingGrpcChannelProviders"); + InstantiatingGrpcChannelProvider.Builder channelBuilder = + ((InstantiatingGrpcChannelProvider) getTransportChannelProvider()) + .toBuilder() + .setChannelPrimer(new RefreshChannel()); + setTransportChannelProvider(channelBuilder.build()); + } return new EnhancedBigtableStubSettings(this); } // diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/RefreshChannelTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/RefreshChannelTest.java new file mode 100644 index 0000000000..c41fa4d2a5 --- /dev/null +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/RefreshChannelTest.java @@ -0,0 +1,38 @@ +/* + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.cloud.bigtable.data.v2.internal; + +import io.grpc.ConnectivityState; +import io.grpc.ManagedChannel; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mockito; + +@RunWith(JUnit4.class) +public class RefreshChannelTest { + // RefreshChannel should establish connection to the server through managedChannel.getState(true) + @Test + public void testGetStateIsCalled() { + RefreshChannel refreshChannel = new RefreshChannel(); + ManagedChannel managedChannel = Mockito.mock(ManagedChannel.class); + + Mockito.doReturn(ConnectivityState.READY).when(managedChannel).getState(true); + + refreshChannel.primeChannel(managedChannel); + Mockito.verify(managedChannel, Mockito.atLeastOnce()).getState(true); + } +} diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettingsTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettingsTest.java index 6adf129f94..b9b6a69cde 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettingsTest.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettingsTest.java @@ -61,6 +61,7 @@ public void settingsAreNotLostTest() { String projectId = "my-project"; String instanceId = "my-instance"; String appProfileId = "my-app-profile-id"; + boolean isRefreshingChannel = true; String endpoint = "some.other.host:123"; CredentialsProvider credentialsProvider = Mockito.mock(CredentialsProvider.class); WatchdogProvider watchdogProvider = Mockito.mock(WatchdogProvider.class); @@ -71,6 +72,7 @@ public void settingsAreNotLostTest() { .setProjectId(projectId) .setInstanceId(instanceId) .setAppProfileId(appProfileId) + .setRefreshingChannel(isRefreshingChannel) .setEndpoint(endpoint) .setCredentialsProvider(credentialsProvider) .setStreamWatchdogProvider(watchdogProvider) @@ -81,6 +83,7 @@ public void settingsAreNotLostTest() { projectId, instanceId, appProfileId, + isRefreshingChannel, endpoint, credentialsProvider, watchdogProvider, @@ -90,6 +93,7 @@ public void settingsAreNotLostTest() { projectId, instanceId, appProfileId, + isRefreshingChannel, endpoint, credentialsProvider, watchdogProvider, @@ -99,6 +103,7 @@ public void settingsAreNotLostTest() { projectId, instanceId, appProfileId, + isRefreshingChannel, endpoint, credentialsProvider, watchdogProvider, @@ -110,6 +115,7 @@ private void verifyBuilder( String projectId, String instanceId, String appProfileId, + boolean isRefreshingChannel, String endpoint, CredentialsProvider credentialsProvider, WatchdogProvider watchdogProvider, @@ -117,6 +123,7 @@ private void verifyBuilder( assertThat(builder.getProjectId()).isEqualTo(projectId); assertThat(builder.getInstanceId()).isEqualTo(instanceId); assertThat(builder.getAppProfileId()).isEqualTo(appProfileId); + assertThat(builder.isRefreshingChannel()).isEqualTo(isRefreshingChannel); assertThat(builder.getEndpoint()).isEqualTo(endpoint); assertThat(builder.getCredentialsProvider()).isEqualTo(credentialsProvider); assertThat(builder.getStreamWatchdogProvider()).isSameInstanceAs(watchdogProvider); @@ -128,6 +135,7 @@ private void verifySettings( String projectId, String instanceId, String appProfileId, + boolean isRefreshingChannel, String endpoint, CredentialsProvider credentialsProvider, WatchdogProvider watchdogProvider, @@ -135,6 +143,7 @@ private void verifySettings( assertThat(settings.getProjectId()).isEqualTo(projectId); assertThat(settings.getInstanceId()).isEqualTo(instanceId); assertThat(settings.getAppProfileId()).isEqualTo(appProfileId); + assertThat(settings.isRefreshingChannel()).isEqualTo(isRefreshingChannel); assertThat(settings.getEndpoint()).isEqualTo(endpoint); assertThat(settings.getCredentialsProvider()).isEqualTo(credentialsProvider); assertThat(settings.getStreamWatchdogProvider()).isSameInstanceAs(watchdogProvider); @@ -521,4 +530,31 @@ private void verifyRetrySettingAreSane(Set retryCodes, RetrySettings retry assertThat(retrySettings.getRpcTimeoutMultiplier()).isAtLeast(1.0); assertThat(retrySettings.getMaxRpcTimeout()).isGreaterThan(Duration.ZERO); } + + @Test + public void isRefreshingChannelDefaultValueTest() { + String dummyProjectId = "my-project"; + String dummyInstanceId = "my-instance"; + EnhancedBigtableStubSettings.Builder builder = + EnhancedBigtableStubSettings.newBuilder() + .setProjectId(dummyProjectId) + .setInstanceId(dummyInstanceId); + assertThat(builder.isRefreshingChannel()).isFalse(); + assertThat(builder.build().isRefreshingChannel()).isFalse(); + assertThat(builder.build().toBuilder().isRefreshingChannel()).isFalse(); + } + + @Test + public void isRefreshingChannelFalseValueTest() { + String dummyProjectId = "my-project"; + String dummyInstanceId = "my-instance"; + EnhancedBigtableStubSettings.Builder builder = + EnhancedBigtableStubSettings.newBuilder() + .setProjectId(dummyProjectId) + .setInstanceId(dummyInstanceId) + .setRefreshingChannel(false); + assertThat(builder.isRefreshingChannel()).isFalse(); + assertThat(builder.build().isRefreshingChannel()).isFalse(); + assertThat(builder.build().toBuilder().isRefreshingChannel()).isFalse(); + } }