From 2c543a153f9d3e8ca043439dfc06b8cb04c66a51 Mon Sep 17 00:00:00 2001 From: Nicole Lee Date: Tue, 28 Jul 2026 18:18:50 +0000 Subject: [PATCH 1/3] test(showcase): add integration tests for exponential backoff retries using SequenceService --- .../google/showcase/v1beta1/it/ITRetries.java | 193 ++++++++++++++++++ .../it/util/TestClientInitializer.java | 73 +++++++ 2 files changed, 266 insertions(+) create mode 100644 java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITRetries.java diff --git a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITRetries.java b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITRetries.java new file mode 100644 index 000000000000..01d883b76f81 --- /dev/null +++ b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITRetries.java @@ -0,0 +1,193 @@ +/* + * 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.showcase.v1beta1.it; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.api.gax.retrying.RetrySettings; +import com.google.api.gax.rpc.StatusCode; +import com.google.common.collect.ImmutableSet; +import com.google.rpc.Status; +import com.google.showcase.v1beta1.AttemptSequenceRequest; +import com.google.showcase.v1beta1.CreateSequenceRequest; +import com.google.showcase.v1beta1.GetSequenceReportRequest; +import com.google.showcase.v1beta1.Sequence; +import com.google.showcase.v1beta1.SequenceReport; +import com.google.showcase.v1beta1.SequenceServiceClient; +import com.google.showcase.v1beta1.it.util.TestClientInitializer; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class ITRetries { + + private static final Sequence STANDARD_SEQUENCE = + Sequence.newBuilder() + .addResponses( + Sequence.Response.newBuilder() + .setStatus( + Status.newBuilder() + .setCode(com.google.rpc.Code.UNAVAILABLE.getNumber()) + .build()) + .build()) + .addResponses( + Sequence.Response.newBuilder() + .setStatus( + Status.newBuilder() + .setCode(com.google.rpc.Code.UNAVAILABLE.getNumber()) + .build()) + .build()) + .addResponses( + Sequence.Response.newBuilder() + .setStatus( + Status.newBuilder() + .setCode(com.google.rpc.Code.UNAVAILABLE.getNumber()) + .build()) + .build()) + .addResponses( + Sequence.Response.newBuilder() + .setStatus( + Status.newBuilder().setCode(com.google.rpc.Code.OK.getNumber()).build()) + .build()) + .build(); + + @SuppressWarnings("deprecation") + private static final RetrySettings STANDARD_RETRY_SETTINGS = + RetrySettings.newBuilder() + .setInitialRetryDelayDuration(java.time.Duration.ofMillis(100L)) + .setRetryDelayMultiplier(2.0) + .setMaxRetryDelayDuration(java.time.Duration.ofMillis(1000L)) + .setInitialRpcTimeoutDuration(java.time.Duration.ofMillis(1000L)) + .setRpcTimeoutMultiplier(1.0) + .setMaxRpcTimeoutDuration(java.time.Duration.ofMillis(1000L)) + .setTotalTimeoutDuration(java.time.Duration.ofMillis(5000L)) + .setMaxAttempts(4) + .setJittered(false) + .build(); + + private static SequenceServiceClient grpcClient; + private static SequenceServiceClient httpjsonClient; + + @BeforeAll + static void createClients() throws Exception { + grpcClient = TestClientInitializer.createGrpcSequenceClient(); + httpjsonClient = TestClientInitializer.createHttpJsonSequenceClient(); + } + + @AfterAll + static void destroyClients() throws InterruptedException { + grpcClient.close(); + httpjsonClient.close(); + + grpcClient.awaitTermination(TestClientInitializer.AWAIT_TERMINATION_SECONDS, TimeUnit.SECONDS); + httpjsonClient.awaitTermination( + TestClientInitializer.AWAIT_TERMINATION_SECONDS, TimeUnit.SECONDS); + } + + @Test + @SuppressWarnings("deprecation") + void testGrpc_retryExponentialBackoff() throws Exception { + // Create a custom client with these retry settings on attemptSequence + try (SequenceServiceClient retryClient = + TestClientInitializer.createGrpcSequenceClientWithRetrySettings( + STANDARD_RETRY_SETTINGS, ImmutableSet.of(StatusCode.Code.UNAVAILABLE))) { + + Sequence createdSequence = + grpcClient.createSequence( + CreateSequenceRequest.newBuilder().setSequence(STANDARD_SEQUENCE).build()); + + // 3. Trigger the sequence attempts using the retrying client + retryClient.attemptSequence( + AttemptSequenceRequest.newBuilder().setName(createdSequence.getName()).build()); + + // 4. Retrieve the sequence report to inspect delay measurements + SequenceReport report = + grpcClient.getSequenceReport( + GetSequenceReportRequest.newBuilder() + .setName(createdSequence.getName() + "/sequenceReport") + .build()); + + // 5. Assert attempts count and delays + verifySequenceReport(report); + } + } + + @Test + @SuppressWarnings("deprecation") + void testHttpJson_retryExponentialBackoff() throws Exception { + // Create a custom client with these retry settings on attemptSequence + try (SequenceServiceClient retryClient = + TestClientInitializer.createHttpJsonSequenceClientWithRetrySettings( + STANDARD_RETRY_SETTINGS, ImmutableSet.of(StatusCode.Code.UNAVAILABLE))) { + + Sequence createdSequence = + httpjsonClient.createSequence( + CreateSequenceRequest.newBuilder().setSequence(STANDARD_SEQUENCE).build()); + + // 3. Trigger the sequence attempts using the retrying client + retryClient.attemptSequence( + AttemptSequenceRequest.newBuilder().setName(createdSequence.getName()).build()); + + // 4. Retrieve the sequence report to inspect delay measurements + SequenceReport report = + httpjsonClient.getSequenceReport( + GetSequenceReportRequest.newBuilder() + .setName(createdSequence.getName() + "/sequenceReport") + .build()); + + // 5. Assert attempts count and delays + verifySequenceReport(report); + } + } + + private void verifySequenceReport(SequenceReport report) { + List attempts = report.getAttemptsList(); + assertThat(attempts).hasSize(4); + + // Verify the status of each attempt + assertThat(attempts.get(0).getStatus().getCode()) + .isEqualTo(com.google.rpc.Code.UNAVAILABLE.getNumber()); + assertThat(attempts.get(1).getStatus().getCode()) + .isEqualTo(com.google.rpc.Code.UNAVAILABLE.getNumber()); + assertThat(attempts.get(2).getStatus().getCode()) + .isEqualTo(com.google.rpc.Code.UNAVAILABLE.getNumber()); + assertThat(attempts.get(3).getStatus().getCode()).isEqualTo(com.google.rpc.Code.OK.getNumber()); + + // Verify delay intervals are within expected tolerances: + // Attempt 1 -> 2: ~100ms delay. Range [80ms, 250ms] + long delay1 = getDelayMs(attempts.get(1)); + assertThat(delay1).isAtLeast(80L); + assertThat(delay1).isLessThan(250L); + + // Attempt 2 -> 3: ~200ms delay. Range [180ms, 400ms] + long delay2 = getDelayMs(attempts.get(2)); + assertThat(delay2).isAtLeast(180L); + assertThat(delay2).isLessThan(400L); + + // Attempt 3 -> 4: ~400ms delay. Range [380ms, 700ms] + long delay3 = getDelayMs(attempts.get(3)); + assertThat(delay3).isAtLeast(380L); + assertThat(delay3).isLessThan(700L); + } + + private long getDelayMs(SequenceReport.Attempt attempt) { + com.google.protobuf.Duration attemptDelay = attempt.getAttemptDelay(); + return attemptDelay.getSeconds() * 1000 + attemptDelay.getNanos() / 1_000_000; + } +} diff --git a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/util/TestClientInitializer.java b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/util/TestClientInitializer.java index d9f65674bd77..1c1012f1e84a 100644 --- a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/util/TestClientInitializer.java +++ b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/util/TestClientInitializer.java @@ -33,9 +33,12 @@ import com.google.showcase.v1beta1.EchoSettings; import com.google.showcase.v1beta1.IdentityClient; import com.google.showcase.v1beta1.IdentitySettings; +import com.google.showcase.v1beta1.SequenceServiceClient; +import com.google.showcase.v1beta1.SequenceServiceSettings; import com.google.showcase.v1beta1.WaitRequest; import com.google.showcase.v1beta1.stub.EchoStub; import com.google.showcase.v1beta1.stub.EchoStubSettings; +import com.google.showcase.v1beta1.stub.SequenceServiceStubSettings; import io.grpc.ClientInterceptor; import io.grpc.ManagedChannelBuilder; import java.io.IOException; @@ -476,4 +479,74 @@ public String getServiceName() { return "showcase"; } } + + public static SequenceServiceClient createGrpcSequenceClient() throws Exception { + SequenceServiceSettings settings = + SequenceServiceSettings.newBuilder() + .setCredentialsProvider(NoCredentialsProvider.create()) + .setTransportChannelProvider( + SequenceServiceSettings.defaultGrpcTransportProviderBuilder() + .setChannelConfigurator(ManagedChannelBuilder::usePlaintext) + .build()) + .setEndpoint(DEFAULT_GRPC_ENDPOINT) + .build(); + return SequenceServiceClient.create(settings); + } + + public static SequenceServiceClient createHttpJsonSequenceClient() throws Exception { + SequenceServiceSettings settings = + SequenceServiceSettings.newHttpJsonBuilder() + .setCredentialsProvider(NoCredentialsProvider.create()) + .setTransportChannelProvider( + SequenceServiceSettings.defaultHttpJsonTransportProviderBuilder() + .setHttpTransport( + new NetHttpTransport.Builder().doNotValidateCertificate().build()) + .setEndpoint(DEFAULT_HTTPJSON_ENDPOINT) + .build()) + .build(); + return SequenceServiceClient.create(settings); + } + + public static SequenceServiceClient createGrpcSequenceClientWithRetrySettings( + RetrySettings retrySettings, Set retryableCodes) throws Exception { + SequenceServiceStubSettings.Builder stubSettingsBuilder = + SequenceServiceStubSettings.newBuilder(); + stubSettingsBuilder + .attemptSequenceSettings() + .setRetrySettings(retrySettings) + .setRetryableCodes(retryableCodes); + SequenceServiceSettings settings = SequenceServiceSettings.create(stubSettingsBuilder.build()); + settings = + settings.toBuilder() + .setCredentialsProvider(NoCredentialsProvider.create()) + .setTransportChannelProvider( + SequenceServiceSettings.defaultGrpcTransportProviderBuilder() + .setChannelConfigurator(ManagedChannelBuilder::usePlaintext) + .build()) + .setEndpoint(DEFAULT_GRPC_ENDPOINT) + .build(); + return SequenceServiceClient.create(settings); + } + + public static SequenceServiceClient createHttpJsonSequenceClientWithRetrySettings( + RetrySettings retrySettings, Set retryableCodes) throws Exception { + SequenceServiceStubSettings.Builder stubSettingsBuilder = + SequenceServiceStubSettings.newHttpJsonBuilder(); + stubSettingsBuilder + .attemptSequenceSettings() + .setRetrySettings(retrySettings) + .setRetryableCodes(retryableCodes); + SequenceServiceSettings settings = SequenceServiceSettings.create(stubSettingsBuilder.build()); + settings = + settings.toBuilder() + .setCredentialsProvider(NoCredentialsProvider.create()) + .setTransportChannelProvider( + SequenceServiceSettings.defaultHttpJsonTransportProviderBuilder() + .setHttpTransport( + new NetHttpTransport.Builder().doNotValidateCertificate().build()) + .setEndpoint(DEFAULT_HTTPJSON_ENDPOINT) + .build()) + .build(); + return SequenceServiceClient.create(settings); + } } From 490cb3686f395593df9dedbb96e77b25a4d48f8f Mon Sep 17 00:00:00 2001 From: Nicole Lee Date: Tue, 28 Jul 2026 18:41:47 +0000 Subject: [PATCH 2/3] test(showcase): simplify retry client configuration to use SequenceServiceSettings.Builder directly --- .../it/util/TestClientInitializer.java | 50 ++++++++----------- 1 file changed, 20 insertions(+), 30 deletions(-) diff --git a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/util/TestClientInitializer.java b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/util/TestClientInitializer.java index 1c1012f1e84a..283b2a4b8950 100644 --- a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/util/TestClientInitializer.java +++ b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/util/TestClientInitializer.java @@ -38,7 +38,6 @@ import com.google.showcase.v1beta1.WaitRequest; import com.google.showcase.v1beta1.stub.EchoStub; import com.google.showcase.v1beta1.stub.EchoStubSettings; -import com.google.showcase.v1beta1.stub.SequenceServiceStubSettings; import io.grpc.ClientInterceptor; import io.grpc.ManagedChannelBuilder; import java.io.IOException; @@ -509,44 +508,35 @@ public static SequenceServiceClient createHttpJsonSequenceClient() throws Except public static SequenceServiceClient createGrpcSequenceClientWithRetrySettings( RetrySettings retrySettings, Set retryableCodes) throws Exception { - SequenceServiceStubSettings.Builder stubSettingsBuilder = - SequenceServiceStubSettings.newBuilder(); - stubSettingsBuilder + SequenceServiceSettings.Builder settingsBuilder = SequenceServiceSettings.newBuilder(); + settingsBuilder .attemptSequenceSettings() .setRetrySettings(retrySettings) .setRetryableCodes(retryableCodes); - SequenceServiceSettings settings = SequenceServiceSettings.create(stubSettingsBuilder.build()); - settings = - settings.toBuilder() - .setCredentialsProvider(NoCredentialsProvider.create()) - .setTransportChannelProvider( - SequenceServiceSettings.defaultGrpcTransportProviderBuilder() - .setChannelConfigurator(ManagedChannelBuilder::usePlaintext) - .build()) - .setEndpoint(DEFAULT_GRPC_ENDPOINT) - .build(); - return SequenceServiceClient.create(settings); + settingsBuilder + .setCredentialsProvider(NoCredentialsProvider.create()) + .setTransportChannelProvider( + SequenceServiceSettings.defaultGrpcTransportProviderBuilder() + .setChannelConfigurator(ManagedChannelBuilder::usePlaintext) + .build()) + .setEndpoint(DEFAULT_GRPC_ENDPOINT); + return SequenceServiceClient.create(settingsBuilder.build()); } public static SequenceServiceClient createHttpJsonSequenceClientWithRetrySettings( RetrySettings retrySettings, Set retryableCodes) throws Exception { - SequenceServiceStubSettings.Builder stubSettingsBuilder = - SequenceServiceStubSettings.newHttpJsonBuilder(); - stubSettingsBuilder + SequenceServiceSettings.Builder settingsBuilder = SequenceServiceSettings.newHttpJsonBuilder(); + settingsBuilder .attemptSequenceSettings() .setRetrySettings(retrySettings) .setRetryableCodes(retryableCodes); - SequenceServiceSettings settings = SequenceServiceSettings.create(stubSettingsBuilder.build()); - settings = - settings.toBuilder() - .setCredentialsProvider(NoCredentialsProvider.create()) - .setTransportChannelProvider( - SequenceServiceSettings.defaultHttpJsonTransportProviderBuilder() - .setHttpTransport( - new NetHttpTransport.Builder().doNotValidateCertificate().build()) - .setEndpoint(DEFAULT_HTTPJSON_ENDPOINT) - .build()) - .build(); - return SequenceServiceClient.create(settings); + settingsBuilder + .setCredentialsProvider(NoCredentialsProvider.create()) + .setTransportChannelProvider( + SequenceServiceSettings.defaultHttpJsonTransportProviderBuilder() + .setHttpTransport(new NetHttpTransport.Builder().doNotValidateCertificate().build()) + .setEndpoint(DEFAULT_HTTPJSON_ENDPOINT) + .build()); + return SequenceServiceClient.create(settingsBuilder.build()); } } From 51960b2f4e2e05ecf117b13ba2a4ddaf11979f2c Mon Sep 17 00:00:00 2001 From: Nicole Lee Date: Wed, 29 Jul 2026 15:52:15 +0000 Subject: [PATCH 3/3] test(showcase): use lower bounds only for retry delays to prevent flakiness --- .../com/google/showcase/v1beta1/it/ITRetries.java | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITRetries.java b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITRetries.java index 01d883b76f81..ff6c6ad13a48 100644 --- a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITRetries.java +++ b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITRetries.java @@ -169,21 +169,18 @@ private void verifySequenceReport(SequenceReport report) { .isEqualTo(com.google.rpc.Code.UNAVAILABLE.getNumber()); assertThat(attempts.get(3).getStatus().getCode()).isEqualTo(com.google.rpc.Code.OK.getNumber()); - // Verify delay intervals are within expected tolerances: - // Attempt 1 -> 2: ~100ms delay. Range [80ms, 250ms] + // Verify delay intervals are at least the expected exponential backoff thresholds: + // Attempt 1 -> 2: scheduled 100ms. Must be at least 80ms (no upper bound) long delay1 = getDelayMs(attempts.get(1)); assertThat(delay1).isAtLeast(80L); - assertThat(delay1).isLessThan(250L); - // Attempt 2 -> 3: ~200ms delay. Range [180ms, 400ms] + // Attempt 2 -> 3: scheduled 200ms. Must be at least 180ms (no upper bound) long delay2 = getDelayMs(attempts.get(2)); assertThat(delay2).isAtLeast(180L); - assertThat(delay2).isLessThan(400L); - // Attempt 3 -> 4: ~400ms delay. Range [380ms, 700ms] + // Attempt 3 -> 4: scheduled 400ms. Must be at least 380ms (no upper bound) long delay3 = getDelayMs(attempts.get(3)); assertThat(delay3).isAtLeast(380L); - assertThat(delay3).isLessThan(700L); } private long getDelayMs(SequenceReport.Attempt attempt) {