diff --git a/gax-grpc/src/main/java/com/google/api/gax/grpc/GrpcCallContext.java b/gax-grpc/src/main/java/com/google/api/gax/grpc/GrpcCallContext.java index 83bd6deff..b7fb63027 100644 --- a/gax-grpc/src/main/java/com/google/api/gax/grpc/GrpcCallContext.java +++ b/gax-grpc/src/main/java/com/google/api/gax/grpc/GrpcCallContext.java @@ -30,7 +30,9 @@ package com.google.api.gax.grpc; import com.google.api.core.BetaApi; +import com.google.api.gax.retrying.RetrySettings; import com.google.api.gax.rpc.ApiCallContext; +import com.google.api.gax.rpc.StatusCode; import com.google.api.gax.rpc.TransportChannel; import com.google.api.gax.rpc.internal.Headers; import com.google.api.gax.tracing.ApiTracer; @@ -38,6 +40,7 @@ import com.google.auth.Credentials; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import io.grpc.CallCredentials; import io.grpc.CallOptions; import io.grpc.CallOptions.Key; @@ -48,6 +51,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.threeten.bp.Duration; @@ -70,18 +74,36 @@ public final class GrpcCallContext implements ApiCallContext { @Nullable private final Duration streamWaitTimeout; @Nullable private final Duration streamIdleTimeout; @Nullable private final Integer channelAffinity; + @Nullable private final RetrySettings retrySettings; + @Nullable private final ImmutableSet retryableCodes; private final ImmutableMap> extraHeaders; /** Returns an empty instance with a null channel and default {@link CallOptions}. */ public static GrpcCallContext createDefault() { return new GrpcCallContext( - null, CallOptions.DEFAULT, null, null, null, null, ImmutableMap.>of()); + null, + CallOptions.DEFAULT, + null, + null, + null, + null, + ImmutableMap.>of(), + null, + null); } /** Returns an instance with the given channel and {@link CallOptions}. */ public static GrpcCallContext of(Channel channel, CallOptions callOptions) { return new GrpcCallContext( - channel, callOptions, null, null, null, null, ImmutableMap.>of()); + channel, + callOptions, + null, + null, + null, + null, + ImmutableMap.>of(), + null, + null); } private GrpcCallContext( @@ -91,7 +113,9 @@ private GrpcCallContext( @Nullable Duration streamWaitTimeout, @Nullable Duration streamIdleTimeout, @Nullable Integer channelAffinity, - ImmutableMap> extraHeaders) { + ImmutableMap> extraHeaders, + @Nullable RetrySettings retrySettings, + @Nullable Set retryableCodes) { this.channel = channel; this.callOptions = Preconditions.checkNotNull(callOptions); this.timeout = timeout; @@ -99,6 +123,8 @@ private GrpcCallContext( this.streamIdleTimeout = streamIdleTimeout; this.channelAffinity = channelAffinity; this.extraHeaders = Preconditions.checkNotNull(extraHeaders); + this.retrySettings = retrySettings; + this.retryableCodes = retryableCodes == null ? null : ImmutableSet.copyOf(retryableCodes); } /** @@ -160,7 +186,9 @@ public GrpcCallContext withTimeout(@Nullable Duration timeout) { this.streamWaitTimeout, this.streamIdleTimeout, this.channelAffinity, - this.extraHeaders); + this.extraHeaders, + this.retrySettings, + this.retryableCodes); } @Nullable @@ -177,13 +205,15 @@ public GrpcCallContext withStreamWaitTimeout(@Nullable Duration streamWaitTimeou } return new GrpcCallContext( - channel, - callOptions, - timeout, + this.channel, + this.callOptions, + this.timeout, streamWaitTimeout, - streamIdleTimeout, - channelAffinity, - extraHeaders); + this.streamIdleTimeout, + this.channelAffinity, + this.extraHeaders, + this.retrySettings, + this.retryableCodes); } @Override @@ -194,25 +224,29 @@ public GrpcCallContext withStreamIdleTimeout(@Nullable Duration streamIdleTimeou } return new GrpcCallContext( - channel, - callOptions, - timeout, - streamWaitTimeout, + this.channel, + this.callOptions, + this.timeout, + this.streamWaitTimeout, streamIdleTimeout, - channelAffinity, - extraHeaders); + this.channelAffinity, + this.extraHeaders, + this.retrySettings, + this.retryableCodes); } @BetaApi("The surface for channel affinity is not stable yet and may change in the future.") public GrpcCallContext withChannelAffinity(@Nullable Integer affinity) { return new GrpcCallContext( - channel, - callOptions, - timeout, - streamWaitTimeout, - streamIdleTimeout, + this.channel, + this.callOptions, + this.timeout, + this.streamWaitTimeout, + this.streamIdleTimeout, affinity, - extraHeaders); + this.extraHeaders, + this.retrySettings, + this.retryableCodes); } @BetaApi("The surface for extra headers is not stable yet and may change in the future.") @@ -222,13 +256,53 @@ public GrpcCallContext withExtraHeaders(Map> extraHeaders) ImmutableMap> newExtraHeaders = Headers.mergeHeaders(this.extraHeaders, extraHeaders); return new GrpcCallContext( - channel, - callOptions, - timeout, - streamWaitTimeout, - streamIdleTimeout, - channelAffinity, - newExtraHeaders); + this.channel, + this.callOptions, + this.timeout, + this.streamWaitTimeout, + this.streamIdleTimeout, + this.channelAffinity, + newExtraHeaders, + this.retrySettings, + this.retryableCodes); + } + + @Override + public RetrySettings getRetrySettings() { + return this.retrySettings; + } + + @Override + public GrpcCallContext withRetrySettings(RetrySettings retrySettings) { + return new GrpcCallContext( + this.channel, + this.callOptions, + this.timeout, + this.streamWaitTimeout, + this.streamIdleTimeout, + this.channelAffinity, + this.extraHeaders, + retrySettings, + this.retryableCodes); + } + + @Override + public Set getRetryableCodes() { + return this.retryableCodes; + } + + @Override + public GrpcCallContext withRetryableCodes(Set retryableCodes) { + return new GrpcCallContext( + this.channel, + this.callOptions, + this.timeout, + this.streamWaitTimeout, + this.streamIdleTimeout, + this.channelAffinity, + this.extraHeaders, + this.retrySettings, + retryableCodes); } @Override @@ -283,8 +357,18 @@ public ApiCallContext merge(ApiCallContext inputCallContext) { newChannelAffinity = this.channelAffinity; } + RetrySettings newRetrySettings = grpcCallContext.retrySettings; + if (newRetrySettings == null) { + newRetrySettings = this.retrySettings; + } + + Set newRetryableCodes = grpcCallContext.retryableCodes; + if (newRetryableCodes == null) { + newRetryableCodes = this.retryableCodes; + } + ImmutableMap> newExtraHeaders = - Headers.mergeHeaders(extraHeaders, grpcCallContext.extraHeaders); + Headers.mergeHeaders(this.extraHeaders, grpcCallContext.extraHeaders); CallOptions newCallOptions = grpcCallContext @@ -303,7 +387,9 @@ public ApiCallContext merge(ApiCallContext inputCallContext) { newStreamWaitTimeout, newStreamIdleTimeout, newChannelAffinity, - newExtraHeaders); + newExtraHeaders, + newRetrySettings, + newRetryableCodes); } /** The {@link Channel} set on this context. */ @@ -357,11 +443,13 @@ public GrpcCallContext withChannel(Channel newChannel) { return new GrpcCallContext( newChannel, this.callOptions, - timeout, + this.timeout, this.streamWaitTimeout, this.streamIdleTimeout, this.channelAffinity, - this.extraHeaders); + this.extraHeaders, + this.retrySettings, + this.retryableCodes); } /** Returns a new instance with the call options set to the given call options. */ @@ -369,11 +457,13 @@ public GrpcCallContext withCallOptions(CallOptions newCallOptions) { return new GrpcCallContext( this.channel, newCallOptions, - timeout, + this.timeout, this.streamWaitTimeout, this.streamIdleTimeout, this.channelAffinity, - this.extraHeaders); + this.extraHeaders, + this.retrySettings, + this.retryableCodes); } public GrpcCallContext withRequestParamsDynamicHeaderOption(String requestParams) { @@ -410,7 +500,9 @@ public int hashCode() { streamWaitTimeout, streamIdleTimeout, channelAffinity, - extraHeaders); + extraHeaders, + retrySettings, + retryableCodes); } @Override @@ -423,13 +515,15 @@ public boolean equals(Object o) { } GrpcCallContext that = (GrpcCallContext) o; - return Objects.equals(channel, that.channel) - && Objects.equals(callOptions, that.callOptions) - && Objects.equals(timeout, that.timeout) - && Objects.equals(streamWaitTimeout, that.streamWaitTimeout) - && Objects.equals(streamIdleTimeout, that.streamIdleTimeout) - && Objects.equals(channelAffinity, that.channelAffinity) - && Objects.equals(extraHeaders, that.extraHeaders); + return Objects.equals(this.channel, that.channel) + && Objects.equals(this.callOptions, that.callOptions) + && Objects.equals(this.timeout, that.timeout) + && Objects.equals(this.streamWaitTimeout, that.streamWaitTimeout) + && Objects.equals(this.streamIdleTimeout, that.streamIdleTimeout) + && Objects.equals(this.channelAffinity, that.channelAffinity) + && Objects.equals(this.extraHeaders, that.extraHeaders) + && Objects.equals(this.retrySettings, that.retrySettings) + && Objects.equals(this.retryableCodes, that.retryableCodes); } Metadata getMetadata() { diff --git a/gax-grpc/src/test/java/com/google/api/gax/grpc/GrpcCallContextTest.java b/gax-grpc/src/test/java/com/google/api/gax/grpc/GrpcCallContextTest.java index 69c938f7d..1b9b5c187 100644 --- a/gax-grpc/src/test/java/com/google/api/gax/grpc/GrpcCallContextTest.java +++ b/gax-grpc/src/test/java/com/google/api/gax/grpc/GrpcCallContextTest.java @@ -30,8 +30,12 @@ package com.google.api.gax.grpc; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import com.google.api.gax.retrying.RetrySettings; import com.google.api.gax.rpc.ApiCallContext; +import com.google.api.gax.rpc.StatusCode; import com.google.api.gax.rpc.testing.FakeCallContext; import com.google.api.gax.rpc.testing.FakeChannel; import com.google.api.gax.rpc.testing.FakeTransportChannel; @@ -43,9 +47,11 @@ import io.grpc.ManagedChannel; import io.grpc.Metadata.Key; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; @@ -72,9 +78,9 @@ public void testNullToSelfWrongType() { public void testWithCredentials() { Credentials credentials = Mockito.mock(Credentials.class); GrpcCallContext emptyContext = GrpcCallContext.createDefault(); - Truth.assertThat(emptyContext.getCallOptions().getCredentials()).isNull(); + assertNull(emptyContext.getCallOptions().getCredentials()); GrpcCallContext context = emptyContext.withCredentials(credentials); - Truth.assertThat(context.getCallOptions().getCredentials()).isNotNull(); + assertNotNull(context.getCallOptions().getCredentials()); } @Test @@ -123,21 +129,17 @@ public void testWithRequestParamsDynamicHeaderOption() { @Test public void testWithTimeout() { - Truth.assertThat(GrpcCallContext.createDefault().withTimeout(null).getTimeout()).isNull(); + assertNull(GrpcCallContext.createDefault().withTimeout(null).getTimeout()); } @Test public void testWithNegativeTimeout() { - Truth.assertThat( - GrpcCallContext.createDefault().withTimeout(Duration.ofSeconds(-1L)).getTimeout()) - .isNull(); + assertNull(GrpcCallContext.createDefault().withTimeout(Duration.ofSeconds(-1L)).getTimeout()); } @Test public void testWithZeroTimeout() { - Truth.assertThat( - GrpcCallContext.createDefault().withTimeout(Duration.ofSeconds(0L)).getTimeout()) - .isNull(); + assertNull(GrpcCallContext.createDefault().withTimeout(Duration.ofSeconds(0L)).getTimeout()); } @Test @@ -334,6 +336,24 @@ public void testMergeWithTracer() { .isSameInstanceAs(defaultTracer); } + @Test + public void testWithRetrySettings() { + RetrySettings retrySettings = Mockito.mock(RetrySettings.class); + GrpcCallContext emptyContext = GrpcCallContext.createDefault(); + assertNull(emptyContext.getRetrySettings()); + GrpcCallContext context = emptyContext.withRetrySettings(retrySettings); + assertNotNull(context.getRetrySettings()); + } + + @Test + public void testWithRetryableCodes() { + Set codes = Collections.singleton(StatusCode.Code.UNAVAILABLE); + GrpcCallContext emptyContext = GrpcCallContext.createDefault(); + assertNull(emptyContext.getRetryableCodes()); + GrpcCallContext context = emptyContext.withRetryableCodes(codes); + assertNotNull(context.getRetryableCodes()); + } + private static Map> createTestExtraHeaders(String... keyValues) { Map> extraHeaders = new HashMap<>(); for (int i = 0; i < keyValues.length; i += 2) { diff --git a/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonCallContext.java b/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonCallContext.java index ab773c9c9..298499779 100644 --- a/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonCallContext.java +++ b/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonCallContext.java @@ -30,7 +30,9 @@ package com.google.api.gax.httpjson; import com.google.api.core.BetaApi; +import com.google.api.gax.retrying.RetrySettings; import com.google.api.gax.rpc.ApiCallContext; +import com.google.api.gax.rpc.StatusCode; import com.google.api.gax.rpc.TransportChannel; import com.google.api.gax.rpc.internal.Headers; import com.google.api.gax.tracing.ApiTracer; @@ -38,9 +40,11 @@ import com.google.auth.Credentials; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.threeten.bp.Duration; @@ -62,11 +66,13 @@ public final class HttpJsonCallContext implements ApiCallContext { private final Credentials credentials; private final ImmutableMap> extraHeaders; private final ApiTracer tracer; + private final RetrySettings retrySettings; + private final ImmutableSet retryableCodes; /** Returns an empty instance. */ public static HttpJsonCallContext createDefault() { return new HttpJsonCallContext( - null, null, null, null, ImmutableMap.>of(), null); + null, null, null, null, ImmutableMap.>of(), null, null, null); } private HttpJsonCallContext( @@ -75,13 +81,18 @@ private HttpJsonCallContext( Instant deadline, Credentials credentials, ImmutableMap> extraHeaders, - ApiTracer tracer) { + ApiTracer tracer, + RetrySettings defaultRetrySettings, + Set defaultRetryableCodes) { this.channel = channel; this.timeout = timeout; this.deadline = deadline; this.credentials = credentials; this.extraHeaders = extraHeaders; this.tracer = tracer; + this.retrySettings = defaultRetrySettings; + this.retryableCodes = + defaultRetryableCodes == null ? null : ImmutableSet.copyOf(defaultRetryableCodes); } /** @@ -146,14 +157,38 @@ public HttpJsonCallContext merge(ApiCallContext inputCallContext) { newTracer = this.tracer; } + RetrySettings newRetrySettings = httpJsonCallContext.retrySettings; + if (newRetrySettings == null) { + newRetrySettings = this.retrySettings; + } + + Set newRetryableCodes = httpJsonCallContext.retryableCodes; + if (newRetryableCodes == null) { + newRetryableCodes = this.retryableCodes; + } + return new HttpJsonCallContext( - newChannel, newTimeout, newDeadline, newCredentials, newExtraHeaders, newTracer); + newChannel, + newTimeout, + newDeadline, + newCredentials, + newExtraHeaders, + newTracer, + newRetrySettings, + newRetryableCodes); } @Override public HttpJsonCallContext withCredentials(Credentials newCredentials) { return new HttpJsonCallContext( - this.channel, this.timeout, this.deadline, newCredentials, this.extraHeaders, this.tracer); + this.channel, + this.timeout, + this.deadline, + newCredentials, + this.extraHeaders, + this.tracer, + this.retrySettings, + this.retryableCodes); } @Override @@ -180,7 +215,14 @@ public HttpJsonCallContext withTimeout(Duration timeout) { } return new HttpJsonCallContext( - this.channel, timeout, this.deadline, this.credentials, this.extraHeaders, this.tracer); + this.channel, + timeout, + this.deadline, + this.credentials, + this.extraHeaders, + this.tracer, + this.retrySettings, + this.retryableCodes); } @Nullable @@ -218,13 +260,20 @@ public ApiCallContext withExtraHeaders(Map> extraHeaders) { ImmutableMap> newExtraHeaders = Headers.mergeHeaders(this.extraHeaders, extraHeaders); return new HttpJsonCallContext( - channel, timeout, deadline, credentials, newExtraHeaders, this.tracer); + this.channel, + this.timeout, + this.deadline, + this.credentials, + newExtraHeaders, + this.tracer, + this.retrySettings, + this.retryableCodes); } @BetaApi("The surface for extra headers is not stable yet and may change in the future.") @Override public Map> getExtraHeaders() { - return this.extraHeaders; + return extraHeaders; } public HttpJsonChannel getChannel() { @@ -239,14 +288,64 @@ public Credentials getCredentials() { return credentials; } + @Override + public RetrySettings getRetrySettings() { + return retrySettings; + } + + @Override + public HttpJsonCallContext withRetrySettings(RetrySettings retrySettings) { + return new HttpJsonCallContext( + this.channel, + this.timeout, + this.deadline, + this.credentials, + this.extraHeaders, + this.tracer, + retrySettings, + this.retryableCodes); + } + + @Override + public Set getRetryableCodes() { + return retryableCodes; + } + + @Override + public HttpJsonCallContext withRetryableCodes(Set retryableCodes) { + return new HttpJsonCallContext( + this.channel, + this.timeout, + this.deadline, + this.credentials, + this.extraHeaders, + this.tracer, + this.retrySettings, + retryableCodes); + } + public HttpJsonCallContext withChannel(HttpJsonChannel newChannel) { return new HttpJsonCallContext( - newChannel, timeout, deadline, credentials, extraHeaders, this.tracer); + newChannel, + this.timeout, + this.deadline, + this.credentials, + this.extraHeaders, + this.tracer, + this.retrySettings, + this.retryableCodes); } public HttpJsonCallContext withDeadline(Instant newDeadline) { return new HttpJsonCallContext( - channel, timeout, newDeadline, credentials, extraHeaders, this.tracer); + this.channel, + this.timeout, + newDeadline, + this.credentials, + this.extraHeaders, + this.tracer, + this.retrySettings, + this.retryableCodes); } @Nonnull @@ -264,7 +363,14 @@ public HttpJsonCallContext withTracer(@Nonnull ApiTracer newTracer) { Preconditions.checkNotNull(newTracer); return new HttpJsonCallContext( - channel, timeout, deadline, credentials, extraHeaders, newTracer); + this.channel, + this.timeout, + this.deadline, + this.credentials, + this.extraHeaders, + newTracer, + this.retrySettings, + this.retryableCodes); } @Override @@ -276,16 +382,26 @@ public boolean equals(Object o) { return false; } HttpJsonCallContext that = (HttpJsonCallContext) o; - return Objects.equals(channel, that.channel) - && Objects.equals(timeout, that.timeout) - && Objects.equals(deadline, that.deadline) - && Objects.equals(credentials, that.credentials) - && Objects.equals(extraHeaders, that.extraHeaders) - && Objects.equals(tracer, that.tracer); + return Objects.equals(this.channel, that.channel) + && Objects.equals(this.timeout, that.timeout) + && Objects.equals(this.deadline, that.deadline) + && Objects.equals(this.credentials, that.credentials) + && Objects.equals(this.extraHeaders, that.extraHeaders) + && Objects.equals(this.tracer, that.tracer) + && Objects.equals(this.retrySettings, that.retrySettings) + && Objects.equals(this.retryableCodes, that.retryableCodes); } @Override public int hashCode() { - return Objects.hash(channel, timeout, deadline, credentials, extraHeaders, tracer); + return Objects.hash( + channel, + timeout, + deadline, + credentials, + extraHeaders, + tracer, + retrySettings, + retryableCodes); } } diff --git a/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonCallContextTest.java b/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonCallContextTest.java index e3412d2b6..245ace65c 100644 --- a/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonCallContextTest.java +++ b/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonCallContextTest.java @@ -29,12 +29,26 @@ */ package com.google.api.gax.httpjson; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import com.google.api.gax.retrying.RetrySettings; +import com.google.api.gax.rpc.ApiCallContext; +import com.google.api.gax.rpc.StatusCode; import com.google.api.gax.rpc.testing.FakeCallContext; import com.google.api.gax.rpc.testing.FakeChannel; import com.google.api.gax.rpc.testing.FakeTransportChannel; import com.google.api.gax.tracing.ApiTracer; import com.google.auth.Credentials; +import com.google.common.collect.ImmutableMap; import com.google.common.truth.Truth; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; @@ -61,9 +75,9 @@ public void testNullToSelfWrongType() { public void testWithCredentials() { Credentials credentials = Mockito.mock(Credentials.class); HttpJsonCallContext emptyContext = HttpJsonCallContext.createDefault(); - Truth.assertThat(emptyContext.getCredentials()).isNull(); + assertNull(emptyContext.getCredentials()); HttpJsonCallContext context = emptyContext.withCredentials(credentials); - Truth.assertThat(context.getCredentials()).isNotNull(); + assertNotNull(context.getCredentials()); } @Test @@ -103,21 +117,19 @@ public void testMergeWrongType() { @Test public void testWithTimeout() { - Truth.assertThat(HttpJsonCallContext.createDefault().withTimeout(null).getTimeout()).isNull(); + assertNull(HttpJsonCallContext.createDefault().withTimeout(null).getTimeout()); } @Test public void testWithNegativeTimeout() { - Truth.assertThat( - HttpJsonCallContext.createDefault().withTimeout(Duration.ofSeconds(-1L)).getTimeout()) - .isNull(); + assertNull( + HttpJsonCallContext.createDefault().withTimeout(Duration.ofSeconds(-1L)).getTimeout()); } @Test public void testWithZeroTimeout() { - Truth.assertThat( - HttpJsonCallContext.createDefault().withTimeout(Duration.ofSeconds(0L)).getTimeout()) - .isNull(); + assertNull( + HttpJsonCallContext.createDefault().withTimeout(Duration.ofSeconds(0L)).getTimeout()); } @Test @@ -190,4 +202,31 @@ public void testMergeWithTracer() { Truth.assertThat(ctxWithDefaultTracer.merge(HttpJsonCallContext.createDefault()).getTracer()) .isSameInstanceAs(defaultTracer); } + + @Test + public void testWithRetrySettings() { + RetrySettings retrySettings = Mockito.mock(RetrySettings.class); + HttpJsonCallContext emptyContext = HttpJsonCallContext.createDefault(); + assertNull(emptyContext.getRetrySettings()); + HttpJsonCallContext context = emptyContext.withRetrySettings(retrySettings); + assertNotNull(context.getRetrySettings()); + } + + @Test + public void testWithRetryableCodes() { + Set codes = Collections.singleton(StatusCode.Code.UNAVAILABLE); + HttpJsonCallContext emptyContext = HttpJsonCallContext.createDefault(); + assertNull(emptyContext.getRetryableCodes()); + HttpJsonCallContext context = emptyContext.withRetryableCodes(codes); + assertNotNull(context.getRetryableCodes()); + } + + @Test + public void testWithExtraHeaders() { + Map> headers = ImmutableMap.of("k", Arrays.asList("v")); + ApiCallContext emptyContext = HttpJsonCallContext.createDefault(); + assertTrue(emptyContext.getExtraHeaders().isEmpty()); + ApiCallContext context = emptyContext.withExtraHeaders(headers); + assertEquals(headers, context.getExtraHeaders()); + } } diff --git a/gax/src/main/java/com/google/api/gax/retrying/BasicResultRetryAlgorithm.java b/gax/src/main/java/com/google/api/gax/retrying/BasicResultRetryAlgorithm.java index d494aba02..b33f131be 100644 --- a/gax/src/main/java/com/google/api/gax/retrying/BasicResultRetryAlgorithm.java +++ b/gax/src/main/java/com/google/api/gax/retrying/BasicResultRetryAlgorithm.java @@ -29,6 +29,8 @@ */ package com.google.api.gax.retrying; +import java.util.concurrent.CancellationException; + /** * A basic implementation of {@link ResultRetryAlgorithm}. Using this implementation would mean that * all exceptions should be retried, all responses should be accepted (including {@code null}) and @@ -36,30 +38,50 @@ * * @param attempt response type */ -public class BasicResultRetryAlgorithm implements ResultRetryAlgorithm { +public class BasicResultRetryAlgorithm + implements ResultRetryAlgorithmWithContext { /** * Always returns null, indicating that this algorithm does not provide any specific settings for * the next attempt. - * - * @param prevThrowable exception thrown by the previous attempt ({@code null}, if none) - * @param prevResponse response returned by the previous attempt - * @param prevSettings previous attempt settings */ @Override public TimedAttemptSettings createNextAttempt( - Throwable prevThrowable, ResponseT prevResponse, TimedAttemptSettings prevSettings) { + Throwable previousThrowable, + ResponseT previousResponse, + TimedAttemptSettings previousSettings) { return null; } /** - * Returns {@code true} if an exception was thrown ({@code prevThrowable != null}), {@code false} - * otherwise. - * - * @param prevThrowable exception thrown by the previous attempt ({@code null}, if none) - * @param prevResponse response returned by the previous attempt + * Always returns null, indicating that this algorithm does not provide any specific settings for + * the next attempt. + */ + @Override + public TimedAttemptSettings createNextAttempt( + RetryingContext context, + Throwable previousThrowable, + ResponseT previousResponse, + TimedAttemptSettings previousSettings) { + return createNextAttempt(previousThrowable, previousResponse, previousSettings); + } + + /** + * Returns {@code true} if an exception was thrown ({@code previousThrowable != null}), {@code + * false} otherwise. + */ + @Override + public boolean shouldRetry(Throwable previousThrowable, ResponseT previousResponse) { + return previousThrowable != null; + } + + /** + * Returns {@code true} if an exception was thrown ({@code previousThrowable != null}), {@code + * false} otherwise. */ @Override - public boolean shouldRetry(Throwable prevThrowable, ResponseT prevResponse) { - return prevThrowable != null; + public boolean shouldRetry( + RetryingContext context, Throwable previousThrowable, ResponseT previousResponse) + throws CancellationException { + return shouldRetry(previousThrowable, previousResponse); } } diff --git a/gax/src/main/java/com/google/api/gax/retrying/BasicRetryingFuture.java b/gax/src/main/java/com/google/api/gax/retrying/BasicRetryingFuture.java index b96fd2003..de7b5b5ac 100644 --- a/gax/src/main/java/com/google/api/gax/retrying/BasicRetryingFuture.java +++ b/gax/src/main/java/com/google/api/gax/retrying/BasicRetryingFuture.java @@ -77,7 +77,7 @@ class BasicRetryingFuture extends AbstractFuture this.retryAlgorithm = checkNotNull(retryAlgorithm); this.retryingContext = checkNotNull(context); - this.attemptSettings = retryAlgorithm.createFirstAttempt(); + this.attemptSettings = retryAlgorithm.createFirstAttempt(context); // A micro crime, letting "this" reference to escape from constructor before initialization is // completed (via internal non-static class CompletionListener). But it is guaranteed to be ok, @@ -126,6 +126,7 @@ public ApiFuture peekAttemptResult() { // heavy (and in most cases redundant) settable future instantiation on each attempt, plus reduces // possibility of callback chaining going into an infinite loop in case of buggy external // callbacks implementation. + @Override public ApiFuture getAttemptResult() { synchronized (lock) { if (attemptResult == null) { @@ -167,8 +168,9 @@ void handleAttempt(Throwable throwable, ResponseT response) { } TimedAttemptSettings nextAttemptSettings = - retryAlgorithm.createNextAttempt(throwable, response, attemptSettings); - boolean shouldRetry = retryAlgorithm.shouldRetry(throwable, response, nextAttemptSettings); + retryAlgorithm.createNextAttempt(retryingContext, throwable, response, attemptSettings); + boolean shouldRetry = + retryAlgorithm.shouldRetry(retryingContext, throwable, response, nextAttemptSettings); if (shouldRetry) { // Log retry info if (LOG.isLoggable(Level.FINEST)) { @@ -190,7 +192,7 @@ void handleAttempt(Throwable throwable, ResponseT response) { setAttemptResult(throwable, response, true); // a new attempt will be (must be) scheduled by an external executor } else if (throwable != null) { - if (retryAlgorithm.getResultAlgorithm().shouldRetry(throwable, response)) { + if (retryAlgorithm.shouldRetryBasedOnResult(retryingContext, throwable, response)) { tracer.attemptFailedRetriesExhausted(throwable); } else { tracer.attemptPermanentFailure(throwable); diff --git a/gax/src/main/java/com/google/api/gax/retrying/ExponentialRetryAlgorithm.java b/gax/src/main/java/com/google/api/gax/retrying/ExponentialRetryAlgorithm.java index 355e2779c..26beb8f0b 100644 --- a/gax/src/main/java/com/google/api/gax/retrying/ExponentialRetryAlgorithm.java +++ b/gax/src/main/java/com/google/api/gax/retrying/ExponentialRetryAlgorithm.java @@ -41,7 +41,7 @@ * *

This class is thread-safe. */ -public class ExponentialRetryAlgorithm implements TimedRetryAlgorithm { +public class ExponentialRetryAlgorithm implements TimedRetryAlgorithmWithContext { private final RetrySettings globalSettings; private final ApiClock clock; @@ -77,17 +77,46 @@ public TimedAttemptSettings createFirstAttempt() { .build(); } + /** + * Creates a first attempt {@link TimedAttemptSettings}. The first attempt is configured to be + * executed immediately. + * + * @param context a {@link RetryingContext} that can contain custom {@link RetrySettings} and + * retryable codes + * @return first attempt settings + */ + @Override + public TimedAttemptSettings createFirstAttempt(RetryingContext context) { + if (context.getRetrySettings() == null) { + return createFirstAttempt(); + } + + RetrySettings retrySettings = context.getRetrySettings(); + return TimedAttemptSettings.newBuilder() + // Use the given retrySettings rather than the settings this was created with. + // Attempts created using the TimedAttemptSettings built here will use these + // retrySettings, but a new call will not (unless overridden again). + .setGlobalSettings(retrySettings) + .setRpcTimeout(retrySettings.getInitialRpcTimeout()) + .setRetryDelay(Duration.ZERO) + .setRandomizedRetryDelay(Duration.ZERO) + .setAttemptCount(0) + .setOverallAttemptCount(0) + .setFirstAttemptStartTimeNanos(clock.nanoTime()) + .build(); + } + /** * Creates a next attempt {@link TimedAttemptSettings}. The implementation increments the current * attempt count and uses randomized exponential backoff factor for calculating next attempt * execution time. * - * @param prevSettings previous attempt settings + * @param previousSettings previous attempt settings * @return next attempt settings */ @Override - public TimedAttemptSettings createNextAttempt(TimedAttemptSettings prevSettings) { - RetrySettings settings = prevSettings.getGlobalSettings(); + public TimedAttemptSettings createNextAttempt(TimedAttemptSettings previousSettings) { + RetrySettings settings = previousSettings.getGlobalSettings(); // The retry delay is determined as follows: // attempt #0 - not used (initial attempt is always made immediately); @@ -95,9 +124,9 @@ public TimedAttemptSettings createNextAttempt(TimedAttemptSettings prevSettings) // attempt #2+ - use the calculated value (i.e. the following if statement is true only // if we are about to calculate the value for the upcoming 2nd+ attempt). long newRetryDelay = settings.getInitialRetryDelay().toMillis(); - if (prevSettings.getAttemptCount() > 0) { + if (previousSettings.getAttemptCount() > 0) { newRetryDelay = - (long) (settings.getRetryDelayMultiplier() * prevSettings.getRetryDelay().toMillis()); + (long) (settings.getRetryDelayMultiplier() * previousSettings.getRetryDelay().toMillis()); newRetryDelay = Math.min(newRetryDelay, settings.getMaxRetryDelay().toMillis()); } Duration randomDelay = Duration.ofMillis(nextRandomLong(newRetryDelay)); @@ -107,7 +136,7 @@ public TimedAttemptSettings createNextAttempt(TimedAttemptSettings prevSettings) // attempt #1+ - use the calculated value, or the time remaining in totalTimeout if the // calculated value would exceed the totalTimeout. long newRpcTimeout = - (long) (settings.getRpcTimeoutMultiplier() * prevSettings.getRpcTimeout().toMillis()); + (long) (settings.getRpcTimeoutMultiplier() * previousSettings.getRpcTimeout().toMillis()); newRpcTimeout = Math.min(newRpcTimeout, settings.getMaxRpcTimeout().toMillis()); // The totalTimeout could be zero if a callable is only using maxAttempts to limit retries. @@ -116,8 +145,8 @@ public TimedAttemptSettings createNextAttempt(TimedAttemptSettings prevSettings) if (!settings.getTotalTimeout().isZero()) { Duration timeElapsed = Duration.ofNanos(clock.nanoTime()) - .minus(Duration.ofNanos(prevSettings.getFirstAttemptStartTimeNanos())); - Duration timeLeft = globalSettings.getTotalTimeout().minus(timeElapsed).minus(randomDelay); + .minus(Duration.ofNanos(previousSettings.getFirstAttemptStartTimeNanos())); + Duration timeLeft = settings.getTotalTimeout().minus(timeElapsed).minus(randomDelay); // If timeLeft at this point is < 0, the shouldRetry logic will prevent // the attempt from being made as it would exceed the totalTimeout. A negative RPC timeout @@ -127,16 +156,34 @@ public TimedAttemptSettings createNextAttempt(TimedAttemptSettings prevSettings) } return TimedAttemptSettings.newBuilder() - .setGlobalSettings(prevSettings.getGlobalSettings()) + .setGlobalSettings(previousSettings.getGlobalSettings()) .setRetryDelay(Duration.ofMillis(newRetryDelay)) .setRpcTimeout(Duration.ofMillis(newRpcTimeout)) .setRandomizedRetryDelay(randomDelay) - .setAttemptCount(prevSettings.getAttemptCount() + 1) - .setOverallAttemptCount(prevSettings.getOverallAttemptCount() + 1) - .setFirstAttemptStartTimeNanos(prevSettings.getFirstAttemptStartTimeNanos()) + .setAttemptCount(previousSettings.getAttemptCount() + 1) + .setOverallAttemptCount(previousSettings.getOverallAttemptCount() + 1) + .setFirstAttemptStartTimeNanos(previousSettings.getFirstAttemptStartTimeNanos()) .build(); } + /** + * Creates a next attempt {@link TimedAttemptSettings}. The implementation increments the current + * attempt count and uses randomized exponential backoff factor for calculating next attempt + * execution time. + * + * @param context a {@link RetryingContext} that can contain custom {@link RetrySettings} and + * retryable codes + * @param previousSettings previous attempt settings + * @return next attempt settings + */ + @Override + public TimedAttemptSettings createNextAttempt( + RetryingContext context, TimedAttemptSettings previousSettings) { + // The RetrySettings from the context are not used here, as they have already been set as the + // global settings during the creation of the initial attempt. + return createNextAttempt(previousSettings); + } + /** * Returns {@code true} if another attempt should be made, or {@code false} otherwise. * @@ -185,6 +232,23 @@ public boolean shouldRetry(TimedAttemptSettings nextAttemptSettings) { return true; } + /** + * Returns {@code true} if another attempt should be made, or {@code false} otherwise. + * + * @param context a {@link RetryingContext} that can contain custom {@link RetrySettings} and + * retryable codes. Ignored by this implementation. + * @param nextAttemptSettings attempt settings, which will be used for the next attempt, if + * accepted + * @return {@code true} if {@code nextAttemptSettings} does not exceed either maxAttempts limit or + * totalTimeout limit, or {@code false} otherwise + */ + @Override + public boolean shouldRetry(RetryingContext context, TimedAttemptSettings nextAttemptSettings) { + // The RetrySettings from the context are not used here, as they have already been set as the + // global settings during the creation of the initial attempt. + return shouldRetry(nextAttemptSettings); + } + // Injecting Random is not possible here, as Random does not provide nextLong(long bound) method protected long nextRandomLong(long bound) { return bound > 0 && globalSettings.isJittered() // Jitter check needed for testing purposes. diff --git a/gax/src/main/java/com/google/api/gax/retrying/NoopRetryingContext.java b/gax/src/main/java/com/google/api/gax/retrying/NoopRetryingContext.java index c2649d995..ae7cc0438 100644 --- a/gax/src/main/java/com/google/api/gax/retrying/NoopRetryingContext.java +++ b/gax/src/main/java/com/google/api/gax/retrying/NoopRetryingContext.java @@ -32,8 +32,10 @@ // TODO(igorbernstein2): Remove this class once RetryingExecutor#createFuture(Callable) is // deprecated and removed. +import com.google.api.gax.rpc.StatusCode.Code; import com.google.api.gax.tracing.ApiTracer; import com.google.api.gax.tracing.NoopApiTracer; +import java.util.Set; import javax.annotation.Nonnull; /** @@ -51,4 +53,14 @@ public static RetryingContext create() { public ApiTracer getTracer() { return NoopApiTracer.getInstance(); } + + @Override + public RetrySettings getRetrySettings() { + return null; + } + + @Override + public Set getRetryableCodes() { + return null; + } } diff --git a/gax/src/main/java/com/google/api/gax/retrying/ResultRetryAlgorithm.java b/gax/src/main/java/com/google/api/gax/retrying/ResultRetryAlgorithm.java index c1958215e..53363f37a 100644 --- a/gax/src/main/java/com/google/api/gax/retrying/ResultRetryAlgorithm.java +++ b/gax/src/main/java/com/google/api/gax/retrying/ResultRetryAlgorithm.java @@ -32,39 +32,27 @@ import java.util.concurrent.CancellationException; /** - * A result retry algorithm is responsible for the following operations (based on the response - * returned by the previous attempt or on the thrown exception): - * - *

    - *
  1. Accepting a task for retry so another attempt will be made. - *
  2. Canceling retrying process so the related {@link java.util.concurrent.Future} will be - * canceled. - *
  3. Creating {@link TimedAttemptSettings} for each subsequent retry attempt. - *
- * - * Implementations of this interface must be thread-safe. - * - * @param response type + * Same as {@link ResultRetryAlgorithmWithContext}, but without methods that accept a {@link + * RetryingContext}. Use {@link ResultRetryAlgorithmWithContext} instead of this interface when + * possible. */ public interface ResultRetryAlgorithm { /** - * Creates a next attempt {@link TimedAttemptSettings}. + * Same as {@link ResultRetryAlgorithmWithContext#createNextAttempt(RetryingContext, Throwable, + * Object, TimedAttemptSettings)}, but without a {@link RetryingContext}. * - * @param prevThrowable exception thrown by the previous attempt ({@code null}, if none) - * @param prevResponse response returned by the previous attempt - * @param prevSettings previous attempt settings - * @return next attempt settings or {@code null}, if the implementing algorithm does not provide - * specific settings for the next attempt + *

Use {@link ResultRetryAlgorithmWithContext#createNextAttempt(RetryingContext, Throwable, + * Object, TimedAttemptSettings)} instead of this method when possible. */ TimedAttemptSettings createNextAttempt( Throwable prevThrowable, ResponseT prevResponse, TimedAttemptSettings prevSettings); /** - * Returns {@code true} if another attempt should be made, or {@code false} otherwise. + * Same as {@link ResultRetryAlgorithmWithContext#shouldRetry(Throwable, Object)}, but without a + * {@link RetryingContext}. * - * @param prevThrowable exception thrown by the previous attempt ({@code null}, if none) - * @param prevResponse response returned by the previous attempt - * @throws CancellationException if the retrying process should be canceled + *

Use {@link ResultRetryAlgorithmWithContext#shouldRetry(RetryingContext, Throwable, Object)} + * instead of this method when possible. */ boolean shouldRetry(Throwable prevThrowable, ResponseT prevResponse) throws CancellationException; } diff --git a/gax/src/main/java/com/google/api/gax/retrying/ResultRetryAlgorithmWithContext.java b/gax/src/main/java/com/google/api/gax/retrying/ResultRetryAlgorithmWithContext.java new file mode 100644 index 000000000..2a347b13a --- /dev/null +++ b/gax/src/main/java/com/google/api/gax/retrying/ResultRetryAlgorithmWithContext.java @@ -0,0 +1,81 @@ +/* + * Copyright 2021 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.google.api.gax.retrying; + +import java.util.concurrent.CancellationException; + +/** + * A result retry algorithm is responsible for the following operations (based on the response + * returned by the previous attempt or on the thrown exception): + * + *

    + *
  1. Accepting a task for retry so another attempt will be made. + *
  2. Canceling retrying process so the related {@link java.util.concurrent.Future} will be + * canceled. + *
  3. Creating {@link TimedAttemptSettings} for each subsequent retry attempt. + *
+ * + * Implementations of this interface receive a {@link RetryingContext} that can contain specific + * {@link RetrySettings} and retryable codes that should be used to determine the retry behavior. + * + *

Implementations of this interface must be thread-safe. + * + * @param response type + */ +public interface ResultRetryAlgorithmWithContext + extends ResultRetryAlgorithm { + + /** + * Creates a next attempt {@link TimedAttemptSettings}. + * + * @param context the retrying context of this invocation that can be used to determine the + * settings for the next attempt. + * @param previousThrowable exception thrown by the previous attempt ({@code null}, if none) + * @param previousResponse response returned by the previous attempt + * @param previousSettings previous attempt settings + */ + TimedAttemptSettings createNextAttempt( + RetryingContext context, + Throwable previousThrowable, + ResponseT previousResponse, + TimedAttemptSettings previousSettings); + + /** + * Returns {@code true} if another attempt should be made, or {@code false} otherwise. + * + * @param context the retrying context of this invocation that can be used to determine whether + * the call should be retried. + * @param previousThrowable exception thrown by the previous attempt ({@code null}, if none) + * @param previousResponse response returned by the previous attempt. + */ + boolean shouldRetry( + RetryingContext context, Throwable previousThrowable, ResponseT previousResponse) + throws CancellationException; +} diff --git a/gax/src/main/java/com/google/api/gax/retrying/RetryAlgorithm.java b/gax/src/main/java/com/google/api/gax/retrying/RetryAlgorithm.java index d06c46039..16d60d148 100644 --- a/gax/src/main/java/com/google/api/gax/retrying/RetryAlgorithm.java +++ b/gax/src/main/java/com/google/api/gax/retrying/RetryAlgorithm.java @@ -45,84 +45,223 @@ public class RetryAlgorithm { private final ResultRetryAlgorithm resultAlgorithm; private final TimedRetryAlgorithm timedAlgorithm; + private final ResultRetryAlgorithmWithContext resultAlgorithmWithContext; + private final TimedRetryAlgorithmWithContext timedAlgorithmWithContext; /** * Creates a new retry algorithm instance, which uses thrown exception or returned response and * timed algorithms to make a decision. The result algorithm has higher priority than the timed * algorithm. * + *

Instances that are created using this constructor will ignore the {@link RetryingContext} + * that is passed in to the retrying methods. Use {@link + * #RetryAlgorithm(ResultRetryAlgorithmWithContext, TimedRetryAlgorithmWithContext)} to create an + * instance that will respect the {@link RetryingContext}. + * * @param resultAlgorithm result algorithm to use * @param timedAlgorithm timed algorithm to use + * @deprecated use {@link RetryAlgorithm#RetryAlgorithm(ResultRetryAlgorithmWithContext, + * TimedRetryAlgorithmWithContext)} instead */ + @Deprecated public RetryAlgorithm( ResultRetryAlgorithm resultAlgorithm, TimedRetryAlgorithm timedAlgorithm) { this.resultAlgorithm = checkNotNull(resultAlgorithm); this.timedAlgorithm = checkNotNull(timedAlgorithm); + this.resultAlgorithmWithContext = null; + this.timedAlgorithmWithContext = null; + } + + /** + * Creates a new retry algorithm instance, which uses thrown exception or returned response and + * timed algorithms to make a decision. The result algorithm has higher priority than the timed + * algorithm. + * + * @param resultAlgorithm result algorithm to use + * @param timedAlgorithm timed algorithm to use + */ + public RetryAlgorithm( + ResultRetryAlgorithmWithContext resultAlgorithm, + TimedRetryAlgorithmWithContext timedAlgorithm) { + this.resultAlgorithm = null; + this.timedAlgorithm = null; + this.resultAlgorithmWithContext = checkNotNull(resultAlgorithm); + this.timedAlgorithmWithContext = checkNotNull(timedAlgorithm); } /** * Creates a first attempt {@link TimedAttemptSettings}. * * @return first attempt settings + * @deprecated use {@link #createFirstAttempt(RetryingContext)} instead */ + @Deprecated public TimedAttemptSettings createFirstAttempt() { - return timedAlgorithm.createFirstAttempt(); + return createFirstAttempt(null); + } + + /** + * Creates a first attempt {@link TimedAttemptSettings}. + * + * @param context the {@link RetryingContext} that can be used to get the initial {@link + * RetrySettings} + * @return first attempt settings + */ + public TimedAttemptSettings createFirstAttempt(RetryingContext context) { + if (timedAlgorithmWithContext != null && context != null) { + return timedAlgorithmWithContext.createFirstAttempt(context); + } + return getTimedAlgorithm().createFirstAttempt(); } /** * Creates a next attempt {@link TimedAttemptSettings}. This method will return first non-null * value, returned by either result or timed retry algorithms in that particular order. * - * @param prevThrowable exception thrown by the previous attempt or null if a result was returned - * instead - * @param prevResponse response returned by the previous attempt or null if an exception was + * @param previousThrowable exception thrown by the previous attempt or null if a result was + * returned instead + * @param previousResponse response returned by the previous attempt or null if an exception was * thrown instead - * @param prevSettings previous attempt settings + * @param previousSettings previous attempt settings * @return next attempt settings, can be {@code null}, if no there should be no new attempt + * @deprecated use {@link #createNextAttempt(RetryingContext, Throwable, Object, + * TimedAttemptSettings)} instead */ + @Deprecated public TimedAttemptSettings createNextAttempt( - Throwable prevThrowable, ResponseT prevResponse, TimedAttemptSettings prevSettings) { - // a small optimization, which allows to avoid calling relatively heavy methods + Throwable previousThrowable, + ResponseT previousResponse, + TimedAttemptSettings previousSettings) { + return createNextAttempt(null, previousThrowable, previousResponse, previousSettings); + } + + /** + * Creates a next attempt {@link TimedAttemptSettings}. This method will return first non-null + * value, returned by either result or timed retry algorithms in that particular order. + * + * @param context the {@link RetryingContext} that can be used to determine the {@link + * RetrySettings} for the next attempt + * @param previousThrowable exception thrown by the previous attempt or null if a result was + * returned instead + * @param previousResponse response returned by the previous attempt or null if an exception was + * thrown instead + * @param previousSettings previous attempt settings + * @return next attempt settings, can be {@code null}, if there should be no new attempt + */ + public TimedAttemptSettings createNextAttempt( + RetryingContext context, + Throwable previousThrowable, + ResponseT previousResponse, + TimedAttemptSettings previousSettings) { + // a small optimization that avoids calling relatively heavy methods // like timedAlgorithm.createNextAttempt(), when it is not necessary. - if (!resultAlgorithm.shouldRetry(prevThrowable, prevResponse)) { + if (!shouldRetryBasedOnResult(context, previousThrowable, previousResponse)) { return null; } TimedAttemptSettings newSettings = - resultAlgorithm.createNextAttempt(prevThrowable, prevResponse, prevSettings); + createNextAttemptBasedOnResult( + context, previousThrowable, previousResponse, previousSettings); if (newSettings == null) { - newSettings = timedAlgorithm.createNextAttempt(prevSettings); + newSettings = createNextAttemptBasedOnTiming(context, previousSettings); } return newSettings; } + private TimedAttemptSettings createNextAttemptBasedOnResult( + RetryingContext context, + Throwable previousThrowable, + ResponseT previousResponse, + TimedAttemptSettings previousSettings) { + if (resultAlgorithmWithContext != null && context != null) { + return resultAlgorithmWithContext.createNextAttempt( + context, previousThrowable, previousResponse, previousSettings); + } + return getResultAlgorithm() + .createNextAttempt(previousThrowable, previousResponse, previousSettings); + } + + private TimedAttemptSettings createNextAttemptBasedOnTiming( + RetryingContext context, TimedAttemptSettings previousSettings) { + if (timedAlgorithmWithContext != null && context != null) { + return timedAlgorithmWithContext.createNextAttempt(context, previousSettings); + } + return getTimedAlgorithm().createNextAttempt(previousSettings); + } + /** * Returns {@code true} if another attempt should be made, or {@code false} otherwise. * - * @param prevThrowable exception thrown by the previous attempt or null if a result was returned - * instead - * @param prevResponse response returned by the previous attempt or null if an exception was + * @param previousThrowable exception thrown by the previous attempt or null if a result was + * returned instead + * @param previousResponse response returned by the previous attempt or null if an exception was * thrown instead * @param nextAttemptSettings attempt settings, which will be used for the next attempt, if * accepted * @throws CancellationException if the retrying process should be canceled * @return {@code true} if another attempt should be made, or {@code false} otherwise + * @deprecated use {@link #shouldRetry(RetryingContext, Throwable, Object, TimedAttemptSettings)} + * instead + */ + @Deprecated + public boolean shouldRetry( + Throwable previousThrowable, + ResponseT previousResponse, + TimedAttemptSettings nextAttemptSettings) + throws CancellationException { + return shouldRetry(null, previousThrowable, previousResponse, nextAttemptSettings); + } + + /** + * Returns {@code true} if another attempt should be made, or {@code false} otherwise. + * + * @param context the {@link RetryingContext} that can be used to determine whether another + * attempt should be made + * @param previousThrowable exception thrown by the previous attempt or null if a result was + * returned instead + * @param previousResponse response returned by the previous attempt or null if an exception was + * thrown instead + * @param nextAttemptSettings attempt settings, which will be used for the next attempt, if + * accepted + * @throws CancellationException if the retrying process should be cancelled + * @return {@code true} if another attempt should be made, or {@code false} otherwise */ public boolean shouldRetry( - Throwable prevThrowable, ResponseT prevResponse, TimedAttemptSettings nextAttemptSettings) + RetryingContext context, + Throwable previousThrowable, + ResponseT previousResponse, + TimedAttemptSettings nextAttemptSettings) throws CancellationException { - return resultAlgorithm.shouldRetry(prevThrowable, prevResponse) - && nextAttemptSettings != null - && timedAlgorithm.shouldRetry(nextAttemptSettings); + return shouldRetryBasedOnResult(context, previousThrowable, previousResponse) + && shouldRetryBasedOnTiming(context, nextAttemptSettings); + } + + boolean shouldRetryBasedOnResult( + RetryingContext context, Throwable previousThrowable, ResponseT previousResponse) { + if (resultAlgorithmWithContext != null && context != null) { + return resultAlgorithmWithContext.shouldRetry(context, previousThrowable, previousResponse); + } + return getResultAlgorithm().shouldRetry(previousThrowable, previousResponse); + } + + private boolean shouldRetryBasedOnTiming( + RetryingContext context, TimedAttemptSettings nextAttemptSettings) { + if (nextAttemptSettings == null) { + return false; + } + if (timedAlgorithmWithContext != null && context != null) { + return timedAlgorithmWithContext.shouldRetry(context, nextAttemptSettings); + } + return getTimedAlgorithm().shouldRetry(nextAttemptSettings); } @BetaApi("Surface for inspecting the a RetryAlgorithm is not yet stable") public ResultRetryAlgorithm getResultAlgorithm() { - return resultAlgorithm; + return resultAlgorithmWithContext != null ? resultAlgorithmWithContext : resultAlgorithm; } @BetaApi("Surface for inspecting the a RetryAlgorithm is not yet stable") public TimedRetryAlgorithm getTimedAlgorithm() { - return timedAlgorithm; + return timedAlgorithmWithContext != null ? timedAlgorithmWithContext : timedAlgorithm; } } diff --git a/gax/src/main/java/com/google/api/gax/retrying/RetryingContext.java b/gax/src/main/java/com/google/api/gax/retrying/RetryingContext.java index 04f4bb929..6db53c4e2 100644 --- a/gax/src/main/java/com/google/api/gax/retrying/RetryingContext.java +++ b/gax/src/main/java/com/google/api/gax/retrying/RetryingContext.java @@ -30,8 +30,11 @@ package com.google.api.gax.retrying; import com.google.api.core.BetaApi; +import com.google.api.gax.rpc.StatusCode; import com.google.api.gax.tracing.ApiTracer; +import java.util.Set; import javax.annotation.Nonnull; +import javax.annotation.Nullable; /** * Context for a retryable operation. @@ -43,4 +46,18 @@ public interface RetryingContext { /** Returns the {@link ApiTracer} associated with the current operation. */ @Nonnull ApiTracer getTracer(); + + /** + * Returns the {@link RetrySettings} to use with this context, or null if the default + * {@link RetrySettings} should be used. + */ + @Nullable + RetrySettings getRetrySettings(); + + /** + * Returns the retryable codes to use with this context, or null if the default + * retryable codes should be used. + */ + @Nullable + Set getRetryableCodes(); } diff --git a/gax/src/main/java/com/google/api/gax/retrying/StreamingRetryAlgorithm.java b/gax/src/main/java/com/google/api/gax/retrying/StreamingRetryAlgorithm.java index 52d58f9c7..272fe3ba1 100644 --- a/gax/src/main/java/com/google/api/gax/retrying/StreamingRetryAlgorithm.java +++ b/gax/src/main/java/com/google/api/gax/retrying/StreamingRetryAlgorithm.java @@ -43,11 +43,45 @@ */ @InternalApi("For internal use only") public final class StreamingRetryAlgorithm extends RetryAlgorithm { + + /** + * Instances that are created using this constructor will ignore the {@link RetryingContext} that + * is passed in to the retrying methods. Use {@link + * #StreamingRetryAlgorithm(ResultRetryAlgorithmWithContext, TimedRetryAlgorithmWithContext)} to + * create an instance that will respect the {@link RetryingContext}. + * + * @deprecated use {@link #StreamingRetryAlgorithm(ResultRetryAlgorithmWithContext, + * TimedRetryAlgorithmWithContext)} instead + */ + @Deprecated public StreamingRetryAlgorithm( ResultRetryAlgorithm resultAlgorithm, TimedRetryAlgorithm timedAlgorithm) { super(resultAlgorithm, timedAlgorithm); } + /** + * Creates a {@link StreamingRetryAlgorithm} that will use the settings (if any) in the {@link + * RetryingContext} that is passed in to the retrying methods. + */ + public StreamingRetryAlgorithm( + ResultRetryAlgorithmWithContext resultAlgorithm, + TimedRetryAlgorithmWithContext timedAlgorithm) { + super(resultAlgorithm, timedAlgorithm); + } + + /** + * {@inheritDoc} + * + *

The attempt settings will be reset if the stream attempt produced any messages. + */ + @Override + public TimedAttemptSettings createNextAttempt( + Throwable previousThrowable, + ResponseT previousResponse, + TimedAttemptSettings previousSettings) { + return createNextAttempt(null, previousThrowable, previousResponse, previousSettings); + } + /** * {@inheritDoc} * @@ -55,25 +89,28 @@ public StreamingRetryAlgorithm( */ @Override public TimedAttemptSettings createNextAttempt( - Throwable prevThrowable, ResponseT prevResponse, TimedAttemptSettings prevSettings) { + RetryingContext context, + Throwable previousThrowable, + ResponseT previousResponse, + TimedAttemptSettings previousSettings) { - if (prevThrowable instanceof ServerStreamingAttemptException) { + if (previousThrowable instanceof ServerStreamingAttemptException) { ServerStreamingAttemptException attemptException = - (ServerStreamingAttemptException) prevThrowable; - prevThrowable = prevThrowable.getCause(); + (ServerStreamingAttemptException) previousThrowable; + previousThrowable = previousThrowable.getCause(); // If we have made progress in the last attempt, then reset the delays if (attemptException.hasSeenResponses()) { - prevSettings = - createFirstAttempt() + previousSettings = + createFirstAttempt(context) .toBuilder() - .setFirstAttemptStartTimeNanos(prevSettings.getFirstAttemptStartTimeNanos()) - .setOverallAttemptCount(prevSettings.getOverallAttemptCount()) + .setFirstAttemptStartTimeNanos(previousSettings.getFirstAttemptStartTimeNanos()) + .setOverallAttemptCount(previousSettings.getOverallAttemptCount()) .build(); } } - return super.createNextAttempt(prevThrowable, prevResponse, prevSettings); + return super.createNextAttempt(context, previousThrowable, previousResponse, previousSettings); } /** @@ -84,20 +121,38 @@ public TimedAttemptSettings createNextAttempt( */ @Override public boolean shouldRetry( - Throwable prevThrowable, ResponseT prevResponse, TimedAttemptSettings nextAttemptSettings) + Throwable previousThrowable, + ResponseT previousResponse, + TimedAttemptSettings nextAttemptSettings) + throws CancellationException { + return shouldRetry(null, previousThrowable, previousResponse, nextAttemptSettings); + } + + /** + * {@inheritDoc} + * + *

Schedules retries only if the {@link StreamResumptionStrategy} in the {@code + * ServerStreamingAttemptCallable} supports it. + */ + @Override + public boolean shouldRetry( + RetryingContext context, + Throwable previousThrowable, + ResponseT previousResponse, + TimedAttemptSettings nextAttemptSettings) throws CancellationException { // Unwrap - if (prevThrowable instanceof ServerStreamingAttemptException) { - ServerStreamingAttemptException attemptExceptino = - (ServerStreamingAttemptException) prevThrowable; - prevThrowable = prevThrowable.getCause(); + if (previousThrowable instanceof ServerStreamingAttemptException) { + ServerStreamingAttemptException attemptException = + (ServerStreamingAttemptException) previousThrowable; + previousThrowable = previousThrowable.getCause(); - if (!attemptExceptino.canResume()) { + if (!attemptException.canResume()) { return false; } } - return super.shouldRetry(prevThrowable, prevResponse, nextAttemptSettings); + return super.shouldRetry(context, previousThrowable, previousResponse, nextAttemptSettings); } } diff --git a/gax/src/main/java/com/google/api/gax/retrying/TimedRetryAlgorithm.java b/gax/src/main/java/com/google/api/gax/retrying/TimedRetryAlgorithm.java index b67128a1e..5cae22b20 100644 --- a/gax/src/main/java/com/google/api/gax/retrying/TimedRetryAlgorithm.java +++ b/gax/src/main/java/com/google/api/gax/retrying/TimedRetryAlgorithm.java @@ -33,44 +33,36 @@ import java.util.concurrent.CancellationException; /** - * A timed retry algorithm is responsible for the following operations, based on the previous - * attempt settings and current time: - * - *

    - *
  1. Creating first attempt {@link TimedAttemptSettings}. - *
  2. Accepting a task for retry so another attempt will be made. - *
  3. Canceling retrying process so the related {@link java.util.concurrent.Future} will be - * canceled. - *
  4. Creating {@link TimedAttemptSettings} for each subsequent retry attempt. - *
- * - * Implementations of this interface must be be thread-save. + * Same as {@link TimedRetryAlgorithmWithContext}, but without methods that accept a {@link + * RetryingContext}. Use {@link TimedRetryAlgorithmWithContext} instead of this interface when + * possible. */ public interface TimedRetryAlgorithm { /** - * Creates a first attempt {@link TimedAttemptSettings}. + * Same as {@link TimedRetryAlgorithmWithContext#createFirstAttempt(RetryingContext)}, but without + * a {@link RetryingContext}. * - * @return first attempt settings + *

Use {@link TimedRetryAlgorithmWithContext#createFirstAttempt(RetryingContext)} instead of + * this method when possible. */ TimedAttemptSettings createFirstAttempt(); /** - * Creates a next attempt {@link TimedAttemptSettings}, which defines properties of the next - * attempt. + * Same as {@link TimedRetryAlgorithmWithContext#createNextAttempt(RetryingContext, + * TimedAttemptSettings)}, but without a {@link RetryingContext}. * - * @param prevSettings previous attempt settings - * @return next attempt settings or {@code null} if the implementing algorithm does not provide - * specific settings for the next attempt + *

Use {@link TimedRetryAlgorithmWithContext#createNextAttempt(RetryingContext, + * TimedAttemptSettings)} instead of this method when possible. */ TimedAttemptSettings createNextAttempt(TimedAttemptSettings prevSettings); /** - * Returns {@code true} if another attempt should be made, or {@code false} otherwise. + * Same as {@link TimedRetryAlgorithmWithContext#shouldRetry(RetryingContext, + * TimedAttemptSettings)}, but without a {@link RetryingContext}. * - * @param nextAttemptSettings attempt settings, which will be used for the next attempt, if - * accepted - * @throws CancellationException if the retrying process should be canceled + *

Use {@link TimedRetryAlgorithmWithContext#shouldRetry(RetryingContext, + * TimedAttemptSettings)} instead of this method when possible. */ boolean shouldRetry(TimedAttemptSettings nextAttemptSettings) throws CancellationException; } diff --git a/gax/src/main/java/com/google/api/gax/retrying/TimedRetryAlgorithmWithContext.java b/gax/src/main/java/com/google/api/gax/retrying/TimedRetryAlgorithmWithContext.java new file mode 100644 index 000000000..72efda7f4 --- /dev/null +++ b/gax/src/main/java/com/google/api/gax/retrying/TimedRetryAlgorithmWithContext.java @@ -0,0 +1,86 @@ +/* + * Copyright 2021 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.google.api.gax.retrying; + +import java.util.concurrent.CancellationException; + +/** + * A timed retry algorithm is responsible for the following operations, based on the previous + * attempt settings and current time: + * + *

    + *
  1. Creating first attempt {@link TimedAttemptSettings}. + *
  2. Accepting a task for retry so another attempt will be made. + *
  3. Canceling retrying process so the related {@link java.util.concurrent.Future} will be + * canceled. + *
  4. Creating {@link TimedAttemptSettings} for each subsequent retry attempt. + *
+ * + * Implementations of this interface receive a {@link RetryingContext} that can contain specific + * {@link RetrySettings} and retryable codes that should be used to determine the retry behavior. + * + *

Implementations of this interface must be be thread-save. + */ +public interface TimedRetryAlgorithmWithContext extends TimedRetryAlgorithm { + + /** + * Creates a first attempt {@link TimedAttemptSettings}. + * + * @param context a {@link RetryingContext} that can contain custom {@link RetrySettings} and + * retryable codes + * @return first attempt settings + */ + TimedAttemptSettings createFirstAttempt(RetryingContext context); + + /** + * Creates a next attempt {@link TimedAttemptSettings}, which defines properties of the next + * attempt. + * + * @param context a {@link RetryingContext} that can contain custom {@link RetrySettings} and + * retryable codes + * @param previousSettings previous attempt settings + * @return next attempt settings or {@code null} if the implementing algorithm does not provide + * specific settings for the next attempt + */ + TimedAttemptSettings createNextAttempt( + RetryingContext context, TimedAttemptSettings previousSettings); + + /** + * Returns {@code true} if another attempt should be made, or {@code false} otherwise. + * + * @param context a {@link RetryingContext} that can contain custom {@link RetrySettings} and + * retryable codes. + * @param nextAttemptSettings attempt settings, which will be used for the next attempt, if + * accepted + * @throws CancellationException if the retrying process should be canceled + */ + boolean shouldRetry(RetryingContext context, TimedAttemptSettings nextAttemptSettings); +} diff --git a/gax/src/main/java/com/google/api/gax/rpc/ApiCallContext.java b/gax/src/main/java/com/google/api/gax/rpc/ApiCallContext.java index 2336be0e7..38ed1dd4e 100644 --- a/gax/src/main/java/com/google/api/gax/rpc/ApiCallContext.java +++ b/gax/src/main/java/com/google/api/gax/rpc/ApiCallContext.java @@ -31,11 +31,14 @@ import com.google.api.core.BetaApi; import com.google.api.core.InternalExtensionOnly; +import com.google.api.gax.retrying.RetrySettings; import com.google.api.gax.retrying.RetryingContext; +import com.google.api.gax.rpc.StatusCode.Code; import com.google.api.gax.tracing.ApiTracer; import com.google.auth.Credentials; import java.util.List; import java.util.Map; +import java.util.Set; import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.threeten.bp.Duration; @@ -157,6 +160,56 @@ public interface ApiCallContext extends RetryingContext { @BetaApi("The surface for tracing is not stable yet and may change in the future") ApiCallContext withTracer(@Nonnull ApiTracer tracer); + /** + * Returns a new ApiCallContext with the given {@link RetrySettings} set. + * + *

This sets the {@link RetrySettings} to use for the RPC. These settings will work in + * combination with either the default retryable codes for the RPC, or the retryable codes + * supplied through {@link #withRetryableCodes(Set)}. Calling {@link + * #withRetrySettings(RetrySettings)} on an RPC that does not include {@link + * Code#DEADLINE_EXCEEDED} as one of its retryable codes (or without calling {@link + * #withRetryableCodes(Set)} with a set that includes at least {@link Code#DEADLINE_EXCEEDED}) + * will effectively only set a single timeout that is equal to {@link + * RetrySettings#getInitialRpcTimeout()}. If this timeout is exceeded, the RPC will not be retried + * and will fail with {@link Code#DEADLINE_EXCEEDED}. + * + *

Example usage: + * + *

{@code
+   * ApiCallContext context = GrpcCallContext.createDefault()
+   *   .withRetrySettings(RetrySettings.newBuilder()
+   *     .setInitialRetryDelay(Duration.ofMillis(10L))
+   *     .setInitialRpcTimeout(Duration.ofMillis(100L))
+   *     .setMaxAttempts(10)
+   *     .setMaxRetryDelay(Duration.ofSeconds(10L))
+   *     .setMaxRpcTimeout(Duration.ofSeconds(30L))
+   *     .setRetryDelayMultiplier(1.4)
+   *     .setRpcTimeoutMultiplier(1.5)
+   *     .setTotalTimeout(Duration.ofMinutes(10L))
+   *     .build())
+   *   .withRetryableCodes(Sets.newSet(
+   *     StatusCode.Code.UNAVAILABLE,
+   *     StatusCode.Code.DEADLINE_EXCEEDED));
+   * }
+ */ + @BetaApi + ApiCallContext withRetrySettings(RetrySettings retrySettings); + + /** + * Returns a new ApiCallContext with the given retryable codes set. + * + *

This sets the retryable codes to use for the RPC. These settings will work in combination + * with either the default {@link RetrySettings} for the RPC, or the {@link RetrySettings} + * supplied through {@link #withRetrySettings(RetrySettings)}. + * + *

Setting a non-empty set of retryable codes for an RPC that is not already retryable by + * default, will not have any effect and the RPC will NOT be retried. This option can only be used + * to change which codes are considered retryable for an RPC that already has at least one + * retryable code in its default settings. + */ + @BetaApi + ApiCallContext withRetryableCodes(Set retryableCodes); + /** If inputContext is not null, returns it; if it is null, returns the present instance. */ ApiCallContext nullToSelf(ApiCallContext inputContext); diff --git a/gax/src/main/java/com/google/api/gax/rpc/ApiResultRetryAlgorithm.java b/gax/src/main/java/com/google/api/gax/rpc/ApiResultRetryAlgorithm.java index caa0c2f7c..688fc32cd 100644 --- a/gax/src/main/java/com/google/api/gax/rpc/ApiResultRetryAlgorithm.java +++ b/gax/src/main/java/com/google/api/gax/rpc/ApiResultRetryAlgorithm.java @@ -30,12 +30,35 @@ package com.google.api.gax.rpc; import com.google.api.gax.retrying.BasicResultRetryAlgorithm; +import com.google.api.gax.retrying.RetryingContext; /* Package-private for internal use. */ class ApiResultRetryAlgorithm extends BasicResultRetryAlgorithm { + /** Returns true if previousThrowable is an {@link ApiException} that is retryable. */ @Override - public boolean shouldRetry(Throwable prevThrowable, ResponseT prevResponse) { - return (prevThrowable instanceof ApiException) && ((ApiException) prevThrowable).isRetryable(); + public boolean shouldRetry(Throwable previousThrowable, ResponseT previousResponse) { + return (previousThrowable instanceof ApiException) + && ((ApiException) previousThrowable).isRetryable(); + } + + /** + * If {@link RetryingContext#getRetryableCodes()} is not null: Returns true if the status code of + * previousThrowable is in the list of retryable code of the {@link RetryingContext}. + * + *

Otherwise it returns the result of {@link #shouldRetry(Throwable, Object)}. + */ + @Override + public boolean shouldRetry( + RetryingContext context, Throwable previousThrowable, ResponseT previousResponse) { + if (context.getRetryableCodes() != null) { + // Ignore the isRetryable() value of the throwable if the RetryingContext has a specific list + // of codes that should be retried. + return (previousThrowable instanceof ApiException) + && context + .getRetryableCodes() + .contains(((ApiException) previousThrowable).getStatusCode().getCode()); + } + return shouldRetry(previousThrowable, previousResponse); } } diff --git a/gax/src/test/java/com/google/api/gax/retrying/AbstractRetryingExecutorTest.java b/gax/src/test/java/com/google/api/gax/retrying/AbstractRetryingExecutorTest.java index cb2e6bf9c..3be825747 100644 --- a/gax/src/test/java/com/google/api/gax/retrying/AbstractRetryingExecutorTest.java +++ b/gax/src/test/java/com/google/api/gax/retrying/AbstractRetryingExecutorTest.java @@ -29,6 +29,7 @@ */ package com.google.api.gax.retrying; +import static com.google.api.gax.retrying.FailingCallable.FAILING_RETRY_SETTINGS; import static com.google.api.gax.retrying.FailingCallable.FAST_RETRY_SETTINGS; import static junit.framework.TestCase.assertFalse; import static org.junit.Assert.assertEquals; @@ -47,6 +48,9 @@ import com.google.api.gax.retrying.FailingCallable.CustomException; import com.google.api.gax.rpc.testing.FakeCallContext; import com.google.api.gax.tracing.ApiTracer; +import com.google.common.base.Stopwatch; +import java.util.Arrays; +import java.util.Collection; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; @@ -56,14 +60,24 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.threeten.bp.Duration; -@RunWith(JUnit4.class) +@RunWith(Parameterized.class) public abstract class AbstractRetryingExecutorTest { + + @Parameters(name = "Custom retry settings: {0}") + public static Collection data() { + return Arrays.asList(new Object[][] {{false}, {true}}); + } + + @Parameter public boolean withCustomRetrySettings; + @Rule public final MockitoRule mockitoRule = MockitoJUnit.rule(); @Mock protected ApiTracer tracer; @@ -75,16 +89,35 @@ protected abstract RetryingExecutorWithContext getExecutor( protected abstract RetryAlgorithm getAlgorithm( RetrySettings retrySettings, int apocalypseCountDown, RuntimeException apocalypseException); + protected void busyWaitForInitialResult(RetryingFuture future, Duration timeout) + throws TimeoutException { + Stopwatch watch = Stopwatch.createStarted(); + while (future.peekAttemptResult() == null) { + if (watch.elapsed(TimeUnit.NANOSECONDS) > timeout.toNanos()) { + throw new TimeoutException(); + } + } + } + @Before public void setUp() { - retryingContext = FakeCallContext.createDefault().withTracer(tracer); + if (withCustomRetrySettings) { + retryingContext = + FakeCallContext.createDefault().withTracer(tracer).withRetrySettings(FAST_RETRY_SETTINGS); + } else { + retryingContext = FakeCallContext.createDefault().withTracer(tracer); + } + } + + private RetrySettings getDefaultRetrySettings() { + return withCustomRetrySettings ? FAILING_RETRY_SETTINGS : FAST_RETRY_SETTINGS; } @Test public void testSuccess() throws Exception { FailingCallable callable = new FailingCallable(0, "SUCCESS", tracer); RetryingExecutorWithContext executor = - getExecutor(getAlgorithm(FAST_RETRY_SETTINGS, 0, null)); + getExecutor(getAlgorithm(getDefaultRetrySettings(), 0, null)); RetryingFuture future = executor.createFuture(callable, retryingContext); future.setAttemptFuture(executor.submit(future)); @@ -100,7 +133,7 @@ public void testSuccess() throws Exception { public void testSuccessWithFailures() throws Exception { FailingCallable callable = new FailingCallable(5, "SUCCESS", tracer); RetryingExecutorWithContext executor = - getExecutor(getAlgorithm(FAST_RETRY_SETTINGS, 0, null)); + getExecutor(getAlgorithm(getDefaultRetrySettings(), 0, null)); RetryingFuture future = executor.createFuture(callable, retryingContext); future.setAttemptFuture(executor.submit(future)); @@ -117,7 +150,7 @@ public void testSuccessWithFailures() throws Exception { public void testSuccessWithFailuresPeekGetAttempt() throws Exception { FailingCallable callable = new FailingCallable(5, "SUCCESS", tracer); RetryingExecutorWithContext executor = - getExecutor(getAlgorithm(FAST_RETRY_SETTINGS, 0, null)); + getExecutor(getAlgorithm(getDefaultRetrySettings(), 0, null)); RetryingFuture future = executor.createFuture(callable, retryingContext); assertNull(future.peekAttemptResult()); @@ -143,7 +176,7 @@ public void testSuccessWithFailuresPeekGetAttempt() throws Exception { public void testMaxRetriesExceeded() throws Exception { FailingCallable callable = new FailingCallable(6, "FAILURE", tracer); RetryingExecutorWithContext executor = - getExecutor(getAlgorithm(FAST_RETRY_SETTINGS, 0, null)); + getExecutor(getAlgorithm(getDefaultRetrySettings(), 0, null)); RetryingFuture future = executor.createFuture(callable, retryingContext); future.setAttemptFuture(executor.submit(future)); @@ -164,10 +197,19 @@ public void testTotalTimeoutExceeded() throws Exception { .setInitialRetryDelay(Duration.ofMillis(Integer.MAX_VALUE)) .setMaxRetryDelay(Duration.ofMillis(Integer.MAX_VALUE)) .build(); + boolean useContextRetrySettings = retryingContext.getRetrySettings() != null; RetryingExecutorWithContext executor = - getExecutor(getAlgorithm(retrySettings, 0, null)); + getExecutor( + getAlgorithm( + useContextRetrySettings ? getDefaultRetrySettings() : retrySettings, 0, null)); FailingCallable callable = new FailingCallable(6, "FAILURE", tracer); - RetryingFuture future = executor.createFuture(callable, retryingContext); + RetryingContext context; + if (useContextRetrySettings) { + context = FakeCallContext.createDefault().withTracer(tracer).withRetrySettings(retrySettings); + } else { + context = FakeCallContext.createDefault().withTracer(tracer); + } + RetryingFuture future = executor.createFuture(callable, context); future.setAttemptFuture(executor.submit(future)); assertFutureFail(future, CustomException.class); @@ -208,7 +250,7 @@ public void testCancelOuterFutureBeforeStart() throws Exception { public void testCancelByRetryingAlgorithm() throws Exception { FailingCallable callable = new FailingCallable(6, "FAILURE", tracer); RetryingExecutorWithContext executor = - getExecutor(getAlgorithm(FAST_RETRY_SETTINGS, 5, new CancellationException())); + getExecutor(getAlgorithm(getDefaultRetrySettings(), 5, new CancellationException())); RetryingFuture future = executor.createFuture(callable, retryingContext); future.setAttemptFuture(executor.submit(future)); @@ -227,7 +269,7 @@ public void testCancelByRetryingAlgorithm() throws Exception { public void testUnexpectedExceptionFromRetryAlgorithm() throws Exception { FailingCallable callable = new FailingCallable(6, "FAILURE", tracer); RetryingExecutorWithContext executor = - getExecutor(getAlgorithm(FAST_RETRY_SETTINGS, 5, new RuntimeException())); + getExecutor(getAlgorithm(getDefaultRetrySettings(), 5, new RuntimeException())); RetryingFuture future = executor.createFuture(callable, retryingContext); future.setAttemptFuture(executor.submit(future)); @@ -250,15 +292,24 @@ public void testPollExceptionByPollAlgorithm() throws Exception { .setInitialRetryDelay(Duration.ofMillis(Integer.MAX_VALUE)) .setMaxRetryDelay(Duration.ofMillis(Integer.MAX_VALUE)) .build(); + boolean useContextRetrySettings = retryingContext.getRetrySettings() != null; RetryAlgorithm retryAlgorithm = new RetryAlgorithm<>( new TestResultRetryAlgorithm(0, null), - new ExponentialPollAlgorithm(retrySettings, NanoClock.getDefaultClock())); + new ExponentialPollAlgorithm( + useContextRetrySettings ? getDefaultRetrySettings() : retrySettings, + NanoClock.getDefaultClock())); RetryingExecutorWithContext executor = getExecutor(retryAlgorithm); FailingCallable callable = new FailingCallable(6, "FAILURE", tracer); - RetryingFuture future = executor.createFuture(callable, retryingContext); + RetryingContext context; + if (useContextRetrySettings) { + context = FakeCallContext.createDefault().withTracer(tracer).withRetrySettings(retrySettings); + } else { + context = FakeCallContext.createDefault().withTracer(tracer); + } + RetryingFuture future = executor.createFuture(callable, context); future.setAttemptFuture(executor.submit(future)); assertFutureFail(future, PollException.class); diff --git a/gax/src/test/java/com/google/api/gax/retrying/BasicRetryingFutureTest.java b/gax/src/test/java/com/google/api/gax/retrying/BasicRetryingFutureTest.java index 72c753915..c95cda7eb 100644 --- a/gax/src/test/java/com/google/api/gax/retrying/BasicRetryingFutureTest.java +++ b/gax/src/test/java/com/google/api/gax/retrying/BasicRetryingFutureTest.java @@ -71,15 +71,18 @@ public void testHandleAttemptDoesNotThrowNPEWhenLogLevelLowerThanFiner() throws Mockito.when(retryingContext.getTracer()).thenReturn(tracer); - Mockito.when(retryAlgorithm.createFirstAttempt()).thenReturn(timedAttemptSettings); + Mockito.when(retryAlgorithm.createFirstAttempt(ArgumentMatchers.any())) + .thenReturn(timedAttemptSettings); Mockito.when( retryAlgorithm.createNextAttempt( + ArgumentMatchers.any(), ArgumentMatchers.any(), ArgumentMatchers.any(), ArgumentMatchers.any())) .thenReturn(timedAttemptSettings); Mockito.when( retryAlgorithm.shouldRetry( + ArgumentMatchers.any(), ArgumentMatchers.any(), ArgumentMatchers.any(), ArgumentMatchers.any())) @@ -97,6 +100,55 @@ public void testHandleAttemptDoesNotThrowNPEWhenLogLevelLowerThanFiner() throws Mockito.verifyNoMoreInteractions(tracer); } + @Test + public void testUsesRetryingContext() throws Exception { + @SuppressWarnings("unchecked") + Callable callable = mock(Callable.class); + @SuppressWarnings("unchecked") + RetryAlgorithm retryAlgorithm = mock(RetryAlgorithm.class); + RetryingContext retryingContext = mock(RetryingContext.class); + ApiTracer tracer = mock(ApiTracer.class); + TimedAttemptSettings timedAttemptSettings = mock(TimedAttemptSettings.class); + Mockito.when(retryingContext.getTracer()).thenReturn(tracer); + + Mockito.when(retryAlgorithm.createFirstAttempt(retryingContext)) + .thenReturn(timedAttemptSettings); + Mockito.when( + retryAlgorithm.createNextAttempt( + ArgumentMatchers.eq(retryingContext), + ArgumentMatchers.any(), + ArgumentMatchers.any(), + ArgumentMatchers.any())) + .thenReturn(timedAttemptSettings); + Mockito.when( + retryAlgorithm.shouldRetry( + ArgumentMatchers.eq(retryingContext), + ArgumentMatchers.any(), + ArgumentMatchers.any(), + ArgumentMatchers.any())) + .thenReturn(true); + + BasicRetryingFuture future = + new BasicRetryingFuture<>(callable, retryAlgorithm, retryingContext); + + future.handleAttempt(null, null); + + Mockito.verify(retryAlgorithm).createFirstAttempt(retryingContext); + Mockito.verify(retryAlgorithm) + .createNextAttempt( + ArgumentMatchers.eq(retryingContext), + ArgumentMatchers.any(), + ArgumentMatchers.any(), + ArgumentMatchers.any()); + Mockito.verify(retryAlgorithm) + .shouldRetry( + ArgumentMatchers.eq(retryingContext), + ArgumentMatchers.any(), + ArgumentMatchers.any(), + ArgumentMatchers.any()); + Mockito.verifyNoMoreInteractions(retryAlgorithm); + } + private Logger getLoggerInstance() throws NoSuchFieldException, IllegalAccessException { Field logger = BasicRetryingFuture.class.getDeclaredField("LOG"); logger.setAccessible(true); diff --git a/gax/src/test/java/com/google/api/gax/retrying/ExponentialRetryAlgorithmTest.java b/gax/src/test/java/com/google/api/gax/retrying/ExponentialRetryAlgorithmTest.java index da804a1b5..b81c8c95b 100644 --- a/gax/src/test/java/com/google/api/gax/retrying/ExponentialRetryAlgorithmTest.java +++ b/gax/src/test/java/com/google/api/gax/retrying/ExponentialRetryAlgorithmTest.java @@ -35,6 +35,7 @@ import static org.junit.Assert.assertTrue; import com.google.api.gax.core.FakeApiClock; +import com.google.api.gax.rpc.testing.FakeCallContext; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -50,13 +51,25 @@ public class ExponentialRetryAlgorithmTest { .setRetryDelayMultiplier(2.0) .setMaxRetryDelay(Duration.ofMillis(8L)) .setInitialRpcTimeout(Duration.ofMillis(1L)) - .setJittered(false) .setRpcTimeoutMultiplier(2.0) .setMaxRpcTimeout(Duration.ofMillis(8L)) .setTotalTimeout(Duration.ofMillis(200L)) .build(); private final ExponentialRetryAlgorithm algorithm = new ExponentialRetryAlgorithm(retrySettings, clock); + private final RetrySettings retrySettingsOverride = + RetrySettings.newBuilder() + .setMaxAttempts(3) + .setInitialRetryDelay(Duration.ofMillis(2L)) + .setRetryDelayMultiplier(3.0) + .setMaxRetryDelay(Duration.ofMillis(18L)) + .setInitialRpcTimeout(Duration.ofMillis(2L)) + .setRpcTimeoutMultiplier(3.0) + .setMaxRpcTimeout(Duration.ofMillis(18L)) + .setTotalTimeout(Duration.ofMillis(300L)) + .build(); + private final RetryingContext retryingContext = + FakeCallContext.createDefault().withRetrySettings(retrySettingsOverride); @Test public void testCreateFirstAttempt() { @@ -71,6 +84,19 @@ public void testCreateFirstAttempt() { assertEquals(Duration.ZERO, attempt.getRetryDelay()); } + @Test + public void testCreateFirstAttemptOverride() { + TimedAttemptSettings attempt = algorithm.createFirstAttempt(retryingContext); + + // Checking only the most core values, to not make this test too implementation specific. + assertEquals(0, attempt.getAttemptCount()); + assertEquals(0, attempt.getOverallAttemptCount()); + assertEquals(Duration.ZERO, attempt.getRetryDelay()); + assertEquals(Duration.ZERO, attempt.getRandomizedRetryDelay()); + assertEquals(retrySettingsOverride.getInitialRpcTimeout(), attempt.getRpcTimeout()); + assertEquals(Duration.ZERO, attempt.getRetryDelay()); + } + @Test public void testCreateNextAttempt() { TimedAttemptSettings firstAttempt = algorithm.createFirstAttempt(); @@ -80,16 +106,31 @@ public void testCreateNextAttempt() { assertEquals(1, secondAttempt.getAttemptCount()); assertEquals(1, secondAttempt.getOverallAttemptCount()); assertEquals(Duration.ofMillis(1L), secondAttempt.getRetryDelay()); - assertEquals(Duration.ofMillis(1L), secondAttempt.getRandomizedRetryDelay()); assertEquals(Duration.ofMillis(2L), secondAttempt.getRpcTimeout()); TimedAttemptSettings thirdAttempt = algorithm.createNextAttempt(secondAttempt); assertEquals(2, thirdAttempt.getAttemptCount()); assertEquals(Duration.ofMillis(2L), thirdAttempt.getRetryDelay()); - assertEquals(Duration.ofMillis(2L), thirdAttempt.getRandomizedRetryDelay()); assertEquals(Duration.ofMillis(4L), thirdAttempt.getRpcTimeout()); } + @Test + public void testCreateNextAttemptOverride() { + TimedAttemptSettings firstAttempt = algorithm.createFirstAttempt(retryingContext); + TimedAttemptSettings secondAttempt = algorithm.createNextAttempt(firstAttempt); + + // Checking only the most core values, to not make this test too implementation specific. + assertEquals(1, secondAttempt.getAttemptCount()); + assertEquals(1, secondAttempt.getOverallAttemptCount()); + assertEquals(Duration.ofMillis(2L), secondAttempt.getRetryDelay()); + assertEquals(Duration.ofMillis(6L), secondAttempt.getRpcTimeout()); + + TimedAttemptSettings thirdAttempt = algorithm.createNextAttempt(secondAttempt); + assertEquals(2, thirdAttempt.getAttemptCount()); + assertEquals(Duration.ofMillis(6L), thirdAttempt.getRetryDelay()); + assertEquals(Duration.ofMillis(18L), thirdAttempt.getRpcTimeout()); + } + @Test public void testTruncateToTotalTimeout() { RetrySettings timeoutSettings = @@ -103,10 +144,11 @@ public void testTruncateToTotalTimeout() { TimedAttemptSettings firstAttempt = timeoutAlg.createFirstAttempt(); TimedAttemptSettings secondAttempt = timeoutAlg.createNextAttempt(firstAttempt); - assertThat(firstAttempt.getRpcTimeout()).isGreaterThan(secondAttempt.getRpcTimeout()); + assertThat(secondAttempt.getRpcTimeout()).isAtLeast(firstAttempt.getRpcTimeout()); + assertThat(secondAttempt.getRpcTimeout()).isAtMost(Duration.ofSeconds(4L)); TimedAttemptSettings thirdAttempt = timeoutAlg.createNextAttempt(secondAttempt); - assertThat(secondAttempt.getRpcTimeout()).isGreaterThan(thirdAttempt.getRpcTimeout()); + assertThat(thirdAttempt.getRpcTimeout()).isAtMost(Duration.ofSeconds(4L)); } @Test @@ -136,7 +178,7 @@ public void testShouldRetryFalseOnMaxTimeout() { for (int i = 0; i < 4; i++) { assertTrue(algorithm.shouldRetry(attempt)); attempt = algorithm.createNextAttempt(attempt); - clock.incrementNanoTime(Duration.ofMillis(50L).toNanos()); + clock.incrementNanoTime(Duration.ofMillis(60L).toNanos()); } assertFalse(algorithm.shouldRetry(attempt)); diff --git a/gax/src/test/java/com/google/api/gax/retrying/FailingCallable.java b/gax/src/test/java/com/google/api/gax/retrying/FailingCallable.java index a334d770e..07fa1c59d 100644 --- a/gax/src/test/java/com/google/api/gax/retrying/FailingCallable.java +++ b/gax/src/test/java/com/google/api/gax/retrying/FailingCallable.java @@ -31,6 +31,7 @@ import com.google.api.gax.tracing.ApiTracer; import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; import org.threeten.bp.Duration; @@ -42,16 +43,27 @@ class FailingCallable implements Callable { .setRetryDelayMultiplier(1) .setMaxRetryDelay(Duration.ofMillis(8L)) .setInitialRpcTimeout(Duration.ofMillis(8L)) - .setJittered(false) .setRpcTimeoutMultiplier(1) .setMaxRpcTimeout(Duration.ofMillis(8L)) .setTotalTimeout(Duration.ofMillis(400L)) .build(); + static final RetrySettings FAILING_RETRY_SETTINGS = + RetrySettings.newBuilder() + .setMaxAttempts(2) + .setInitialRetryDelay(Duration.ofNanos(1L)) + .setRetryDelayMultiplier(1) + .setMaxRetryDelay(Duration.ofNanos(1L)) + .setInitialRpcTimeout(Duration.ofNanos(1L)) + .setRpcTimeoutMultiplier(1) + .setMaxRpcTimeout(Duration.ofNanos(1L)) + .setTotalTimeout(Duration.ofNanos(1L)) + .build(); private AtomicInteger attemptsCount = new AtomicInteger(0); private final ApiTracer tracer; private final int expectedFailuresCount; private final String result; + private final CountDownLatch firstAttemptFinished = new CountDownLatch(1); FailingCallable(int expectedFailuresCount, String result, ApiTracer tracer) { this.tracer = tracer; @@ -59,17 +71,25 @@ class FailingCallable implements Callable { this.result = result; } + CountDownLatch getFirstAttemptFinishedLatch() { + return firstAttemptFinished; + } + @Override public String call() throws Exception { - int attemptNumber = attemptsCount.getAndIncrement(); + try { + int attemptNumber = attemptsCount.getAndIncrement(); - tracer.attemptStarted(attemptNumber); + tracer.attemptStarted(attemptNumber); - if (attemptNumber < expectedFailuresCount) { - throw new CustomException(); - } + if (attemptNumber < expectedFailuresCount) { + throw new CustomException(); + } - return result; + return result; + } finally { + firstAttemptFinished.countDown(); + } } static class CustomException extends RuntimeException { diff --git a/gax/src/test/java/com/google/api/gax/retrying/NoopRetryingContextTest.java b/gax/src/test/java/com/google/api/gax/retrying/NoopRetryingContextTest.java new file mode 100644 index 000000000..285b8f5ce --- /dev/null +++ b/gax/src/test/java/com/google/api/gax/retrying/NoopRetryingContextTest.java @@ -0,0 +1,60 @@ +/* + * Copyright 2021 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.google.api.gax.retrying; + +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; + +import com.google.api.gax.tracing.NoopApiTracer; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class NoopRetryingContextTest { + + @Test + public void testGetTracer() { + RetryingContext context = NoopRetryingContext.create(); + assertSame(NoopApiTracer.getInstance(), context.getTracer()); + } + + @Test + public void testGetRetrySettings() { + RetryingContext context = NoopRetryingContext.create(); + assertNull(context.getRetrySettings()); + } + + @Test + public void testGetRetryableCodes() { + RetryingContext context = NoopRetryingContext.create(); + assertNull(context.getRetryableCodes()); + } +} diff --git a/gax/src/test/java/com/google/api/gax/retrying/RetryAlgorithmTest.java b/gax/src/test/java/com/google/api/gax/retrying/RetryAlgorithmTest.java new file mode 100644 index 000000000..a08cdee7f --- /dev/null +++ b/gax/src/test/java/com/google/api/gax/retrying/RetryAlgorithmTest.java @@ -0,0 +1,205 @@ +/* + * Copyright 2021 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.google.api.gax.retrying; + +import static org.junit.Assert.assertFalse; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@SuppressWarnings({"unchecked", "deprecation"}) +@RunWith(JUnit4.class) +public class RetryAlgorithmTest { + + @Test + public void testCreateFirstAttempt() { + TimedRetryAlgorithm timedAlgorithm = mock(TimedRetryAlgorithm.class); + RetryAlgorithm algorithm = + new RetryAlgorithm<>(mock(ResultRetryAlgorithm.class), timedAlgorithm); + + algorithm.createFirstAttempt(); + verify(timedAlgorithm).createFirstAttempt(); + } + + @Test + public void testCreateFirstAttemptWithUnusedContext() { + TimedRetryAlgorithm timedAlgorithm = mock(TimedRetryAlgorithm.class); + RetryAlgorithm algorithm = + new RetryAlgorithm<>(mock(ResultRetryAlgorithm.class), timedAlgorithm); + + RetryingContext context = mock(RetryingContext.class); + algorithm.createFirstAttempt(context); + verify(timedAlgorithm).createFirstAttempt(); + } + + @Test + public void testCreateFirstAttemptWithContext() { + TimedRetryAlgorithmWithContext timedAlgorithm = mock(TimedRetryAlgorithmWithContext.class); + RetryAlgorithm algorithm = + new RetryAlgorithm<>(mock(ResultRetryAlgorithmWithContext.class), timedAlgorithm); + + RetryingContext context = mock(RetryingContext.class); + algorithm.createFirstAttempt(context); + verify(timedAlgorithm).createFirstAttempt(context); + } + + @Test + public void testCreateFirstAttemptWithNullContext() { + TimedRetryAlgorithmWithContext timedAlgorithm = mock(TimedRetryAlgorithmWithContext.class); + RetryAlgorithm algorithm = + new RetryAlgorithm<>(mock(ResultRetryAlgorithmWithContext.class), timedAlgorithm); + + algorithm.createFirstAttempt(null); + verify(timedAlgorithm).createFirstAttempt(); + } + + @Test + public void testNextAttempt() { + ResultRetryAlgorithm resultAlgorithm = mock(ResultRetryAlgorithm.class); + TimedRetryAlgorithm timedAlgorithm = mock(TimedRetryAlgorithm.class); + RetryAlgorithm algorithm = new RetryAlgorithm<>(resultAlgorithm, timedAlgorithm); + + Throwable previousThrowable = new Throwable(); + Object previousResult = new Object(); + TimedAttemptSettings previousSettings = mock(TimedAttemptSettings.class); + + algorithm.createNextAttempt(previousThrowable, previousResult, previousSettings); + verify(resultAlgorithm).shouldRetry(previousThrowable, previousResult); + } + + @Test + public void testNextAttemptWithContext() { + ResultRetryAlgorithmWithContext resultAlgorithm = + mock(ResultRetryAlgorithmWithContext.class); + TimedRetryAlgorithmWithContext timedAlgorithm = mock(TimedRetryAlgorithmWithContext.class); + RetryAlgorithm algorithm = new RetryAlgorithm<>(resultAlgorithm, timedAlgorithm); + + RetryingContext context = mock(RetryingContext.class); + Throwable previousThrowable = new Throwable(); + Object previousResult = new Object(); + TimedAttemptSettings previousSettings = mock(TimedAttemptSettings.class); + + algorithm.createNextAttempt(context, previousThrowable, previousResult, previousSettings); + verify(resultAlgorithm).shouldRetry(context, previousThrowable, previousResult); + } + + @Test + public void testShouldRetry() { + ResultRetryAlgorithm resultAlgorithm = mock(ResultRetryAlgorithm.class); + TimedRetryAlgorithm timedAlgorithm = mock(TimedRetryAlgorithm.class); + RetryAlgorithm algorithm = new RetryAlgorithm<>(resultAlgorithm, timedAlgorithm); + + Throwable previousThrowable = new Throwable(); + Object previousResult = new Object(); + TimedAttemptSettings previousSettings = mock(TimedAttemptSettings.class); + + algorithm.shouldRetry(previousThrowable, previousResult, previousSettings); + verify(resultAlgorithm).shouldRetry(previousThrowable, previousResult); + } + + @Test + public void testShouldRetry_usesTimedAlgorithm() { + ResultRetryAlgorithm resultAlgorithm = mock(ResultRetryAlgorithm.class); + TimedRetryAlgorithm timedAlgorithm = mock(TimedRetryAlgorithm.class); + RetryAlgorithm algorithm = new RetryAlgorithm<>(resultAlgorithm, timedAlgorithm); + + Throwable previousThrowable = new Throwable(); + Object previousResult = new Object(); + TimedAttemptSettings previousSettings = mock(TimedAttemptSettings.class); + when(resultAlgorithm.shouldRetry(previousThrowable, previousResult)).thenReturn(true); + + algorithm.shouldRetry(previousThrowable, previousResult, previousSettings); + verify(timedAlgorithm).shouldRetry(previousSettings); + } + + @Test + public void testShouldRetryWithContext() { + ResultRetryAlgorithmWithContext resultAlgorithm = + mock(ResultRetryAlgorithmWithContext.class); + TimedRetryAlgorithmWithContext timedAlgorithm = mock(TimedRetryAlgorithmWithContext.class); + RetryAlgorithm algorithm = new RetryAlgorithm<>(resultAlgorithm, timedAlgorithm); + + RetryingContext context = mock(RetryingContext.class); + Throwable previousThrowable = new Throwable(); + Object previousResult = new Object(); + TimedAttemptSettings previousSettings = mock(TimedAttemptSettings.class); + + algorithm.shouldRetry(context, previousThrowable, previousResult, previousSettings); + verify(resultAlgorithm).shouldRetry(context, previousThrowable, previousResult); + } + + @Test + public void testShouldRetryWithContext_usesTimedAlgorithm() { + ResultRetryAlgorithmWithContext resultAlgorithm = + mock(ResultRetryAlgorithmWithContext.class); + TimedRetryAlgorithmWithContext timedAlgorithm = mock(TimedRetryAlgorithmWithContext.class); + RetryAlgorithm algorithm = new RetryAlgorithm<>(resultAlgorithm, timedAlgorithm); + + RetryingContext context = mock(RetryingContext.class); + Throwable previousThrowable = new Throwable(); + Object previousResult = new Object(); + TimedAttemptSettings previousSettings = mock(TimedAttemptSettings.class); + when(resultAlgorithm.shouldRetry(context, previousThrowable, previousResult)).thenReturn(true); + + algorithm.shouldRetry(context, previousThrowable, previousResult, previousSettings); + verify(timedAlgorithm).shouldRetry(context, previousSettings); + } + + @Test + public void testShouldRetry_noPreviousSettings() { + ResultRetryAlgorithm resultAlgorithm = mock(ResultRetryAlgorithm.class); + TimedRetryAlgorithm timedAlgorithm = mock(TimedRetryAlgorithm.class); + RetryAlgorithm algorithm = new RetryAlgorithm<>(resultAlgorithm, timedAlgorithm); + + Throwable previousThrowable = new Throwable(); + Object previousResult = new Object(); + when(resultAlgorithm.shouldRetry(previousThrowable, previousResult)).thenReturn(true); + + assertFalse(algorithm.shouldRetry(previousThrowable, previousResult, null)); + } + + @Test + public void testShouldRetryWithContext_noPreviousSettings() { + ResultRetryAlgorithmWithContext resultAlgorithm = + mock(ResultRetryAlgorithmWithContext.class); + TimedRetryAlgorithmWithContext timedAlgorithm = mock(TimedRetryAlgorithmWithContext.class); + RetryAlgorithm algorithm = new RetryAlgorithm<>(resultAlgorithm, timedAlgorithm); + + RetryingContext context = mock(RetryingContext.class); + Throwable previousThrowable = new Throwable(); + Object previousResult = new Object(); + + assertFalse(algorithm.shouldRetry(context, previousThrowable, previousResult, null)); + } +} diff --git a/gax/src/test/java/com/google/api/gax/retrying/ScheduledRetryingExecutorTest.java b/gax/src/test/java/com/google/api/gax/retrying/ScheduledRetryingExecutorTest.java index cbf760f2f..23dff4a30 100644 --- a/gax/src/test/java/com/google/api/gax/retrying/ScheduledRetryingExecutorTest.java +++ b/gax/src/test/java/com/google/api/gax/retrying/ScheduledRetryingExecutorTest.java @@ -40,6 +40,7 @@ import com.google.api.core.ApiFuture; import com.google.api.core.NanoClock; import com.google.api.gax.retrying.FailingCallable.CustomException; +import com.google.api.gax.rpc.testing.FakeCallContext; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; @@ -48,12 +49,10 @@ import java.util.concurrent.ScheduledExecutorService; import org.junit.After; import org.junit.Test; -import org.junit.runner.RunWith; import org.mockito.Mockito; -import org.mockito.junit.MockitoJUnitRunner; import org.threeten.bp.Duration; -@RunWith(MockitoJUnitRunner.class) +// @RunWith(MockitoJUnitRunner.class) public class ScheduledRetryingExecutorTest extends AbstractRetryingExecutorTest { private ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); @@ -75,7 +74,6 @@ protected RetryAlgorithm getAlgorithm( private RetryingExecutorWithContext getRetryingExecutor( RetryAlgorithm retryAlgorithm, ScheduledExecutorService scheduler) { - return new ScheduledRetryingExecutor<>(retryAlgorithm, scheduler); } @@ -101,7 +99,8 @@ public void testSuccessWithFailuresPeekAttempt() throws Exception { RetryingExecutorWithContext executor = getRetryingExecutor(getAlgorithm(retrySettings, 0, null), localExecutor); - RetryingFuture future = executor.createFuture(callable, retryingContext); + RetryingFuture future = + executor.createFuture(callable, FakeCallContext.createDefault().withTracer(tracer)); assertNull(future.peekAttemptResult()); assertSame(future.peekAttemptResult(), future.peekAttemptResult()); @@ -150,7 +149,8 @@ public void testSuccessWithFailuresGetAttempt() throws Exception { RetryingExecutorWithContext executor = getRetryingExecutor(getAlgorithm(retrySettings, 0, null), localExecutor); - RetryingFuture future = executor.createFuture(callable, retryingContext); + RetryingFuture future = + executor.createFuture(callable, FakeCallContext.createDefault().withTracer(tracer)); assertNull(future.peekAttemptResult()); assertSame(future.getAttemptResult(), future.getAttemptResult()); @@ -259,7 +259,8 @@ public void testCancelOuterFutureAfterStart() throws Exception { .build(); RetryingExecutorWithContext executor = getRetryingExecutor(getAlgorithm(retrySettings, 0, null), localExecutor); - RetryingFuture future = executor.createFuture(callable, retryingContext); + RetryingFuture future = + executor.createFuture(callable, FakeCallContext.createDefault().withTracer(tracer)); future.setAttemptFuture(executor.submit(future)); Thread.sleep(30L); @@ -285,7 +286,8 @@ public void testCancelIsTraced() throws Exception { .build(); RetryingExecutorWithContext executor = getRetryingExecutor(getAlgorithm(retrySettings, 0, null), localExecutor); - RetryingFuture future = executor.createFuture(callable, retryingContext); + RetryingFuture future = + executor.createFuture(callable, FakeCallContext.createDefault().withTracer(tracer)); future.setAttemptFuture(executor.submit(future)); Thread.sleep(30L); @@ -313,7 +315,8 @@ public void testCancelProxiedFutureAfterStart() throws Exception { .build(); RetryingExecutorWithContext executor = getRetryingExecutor(getAlgorithm(retrySettings, 0, null), localExecutor); - RetryingFuture future = executor.createFuture(callable, retryingContext); + RetryingFuture future = + executor.createFuture(callable, FakeCallContext.createDefault().withTracer(tracer)); future.setAttemptFuture(executor.submit(future)); Thread.sleep(50L); diff --git a/gax/src/test/java/com/google/api/gax/rpc/ApiResultRetryAlgorithmTest.java b/gax/src/test/java/com/google/api/gax/rpc/ApiResultRetryAlgorithmTest.java new file mode 100644 index 000000000..e1ef46dbd --- /dev/null +++ b/gax/src/test/java/com/google/api/gax/rpc/ApiResultRetryAlgorithmTest.java @@ -0,0 +1,112 @@ +/* + * Copyright 2021 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.google.api.gax.rpc; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.google.api.gax.rpc.StatusCode.Code; +import com.google.api.gax.rpc.testing.FakeStatusCode; +import java.util.Collections; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.internal.util.collections.Sets; + +@RunWith(JUnit4.class) +public class ApiResultRetryAlgorithmTest { + + @Test + public void testShouldRetryNoContext() { + ApiException nonRetryable = + new ApiException(null, new FakeStatusCode(Code.INTERNAL), /* retryable = */ false); + ApiException retryable = + new ApiException(null, new FakeStatusCode(Code.UNAVAILABLE), /* retryable = */ true); + + ApiResultRetryAlgorithm algorithm = new ApiResultRetryAlgorithm<>(); + assertFalse(algorithm.shouldRetry(nonRetryable, null)); + assertTrue(algorithm.shouldRetry(retryable, null)); + } + + @Test + public void testShouldRetryWithContextWithoutRetryableCodes() { + ApiCallContext context = mock(ApiCallContext.class); + // No retryable codes in the call context, means that the retry algorithm should fall back to + // its default implementation. + when(context.getRetryableCodes()).thenReturn(null); + + ApiException nonRetryable = + new ApiException(null, new FakeStatusCode(Code.UNAVAILABLE), /* retryable = */ false); + ApiException retryable = + new ApiException(null, new FakeStatusCode(Code.UNAVAILABLE), /* retryable = */ true); + + ApiResultRetryAlgorithm algorithm = new ApiResultRetryAlgorithm<>(); + assertFalse(algorithm.shouldRetry(context, nonRetryable, null)); + assertTrue(algorithm.shouldRetry(context, retryable, null)); + } + + @Test + public void testShouldRetryWithContextWithRetryableCodes() { + ApiCallContext context = mock(ApiCallContext.class); + when(context.getRetryableCodes()) + .thenReturn(Sets.newSet(StatusCode.Code.DEADLINE_EXCEEDED, StatusCode.Code.UNAVAILABLE)); + + StatusCode unavailable = mock(StatusCode.class); + when(unavailable.getCode()).thenReturn(Code.UNAVAILABLE); + StatusCode dataLoss = mock(StatusCode.class); + when(dataLoss.getCode()).thenReturn(Code.DATA_LOSS); + + // The return value of isRetryable() will be ignored, as UNAVAILABLE has been added as a + // retryable code to the call context. + ApiException unavailableException = + new ApiException(null, new FakeStatusCode(Code.UNAVAILABLE), /* retryable = */ false); + ApiException dataLossException = + new ApiException(null, new FakeStatusCode(Code.DATA_LOSS), /* retryable = */ true); + + ApiResultRetryAlgorithm algorithm = new ApiResultRetryAlgorithm<>(); + assertTrue(algorithm.shouldRetry(context, unavailableException, null)); + assertFalse(algorithm.shouldRetry(context, dataLossException, null)); + } + + @Test + public void testShouldRetryWithContextWithEmptyRetryableCodes() { + ApiCallContext context = mock(ApiCallContext.class); + // This will effectively make the RPC non-retryable. + when(context.getRetryableCodes()).thenReturn(Collections.emptySet()); + + ApiException unavailableException = + new ApiException(null, new FakeStatusCode(Code.UNAVAILABLE), /* retryable = */ true); + + ApiResultRetryAlgorithm algorithm = new ApiResultRetryAlgorithm<>(); + assertFalse(algorithm.shouldRetry(context, unavailableException, null)); + } +} diff --git a/gax/src/test/java/com/google/api/gax/rpc/RetryingTest.java b/gax/src/test/java/com/google/api/gax/rpc/RetryingTest.java index aaff28b97..30198bcb7 100644 --- a/gax/src/test/java/com/google/api/gax/rpc/RetryingTest.java +++ b/gax/src/test/java/com/google/api/gax/rpc/RetryingTest.java @@ -29,6 +29,9 @@ */ package com.google.api.gax.rpc; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + import com.google.api.core.ApiFuture; import com.google.api.core.ApiFutures; import com.google.api.gax.core.FakeApiClock; @@ -51,6 +54,7 @@ import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.mockito.Mockito; +import org.mockito.internal.util.collections.Sets; import org.threeten.bp.Duration; @RunWith(JUnit4.class) @@ -73,6 +77,17 @@ public class RetryingTest { .setMaxRpcTimeout(Duration.ofMillis(2L)) .setTotalTimeout(Duration.ofMillis(10L)) .build(); + private static final RetrySettings FAILING_RETRY_SETTINGS = + RetrySettings.newBuilder() + .setMaxAttempts(2) + .setInitialRetryDelay(Duration.ofNanos(0L)) + .setRetryDelayMultiplier(1) + .setMaxRetryDelay(Duration.ofMillis(0L)) + .setInitialRpcTimeout(Duration.ofNanos(1L)) + .setRpcTimeoutMultiplier(1) + .setMaxRpcTimeout(Duration.ofNanos(1L)) + .setTotalTimeout(Duration.ofNanos(1L)) + .build(); @Before public void resetClock() { @@ -109,6 +124,23 @@ public void retry() { assertRetrying(FAST_RETRY_SETTINGS); } + @Test + public void retryUsingContext() { + Throwable throwable = + new UnavailableException(null, FakeStatusCode.of(StatusCode.Code.INTERNAL), false); + Mockito.when(callInt.futureCall(Mockito.any(), Mockito.any())) + .thenReturn(RetryingTest.immediateFailedFuture(throwable)) + .thenReturn(RetryingTest.immediateFailedFuture(throwable)) + .thenReturn(RetryingTest.immediateFailedFuture(throwable)) + .thenReturn(ApiFutures.immediateFuture(2)); + + assertRetryingUsingContext( + FAILING_RETRY_SETTINGS, + FakeCallContext.createDefault() + .withRetrySettings(FAST_RETRY_SETTINGS) + .withRetryableCodes(Sets.newSet(StatusCode.Code.INTERNAL))); + } + @Test(expected = ApiException.class) public void retryTotalTimeoutExceeded() { Throwable throwable = @@ -127,6 +159,33 @@ public void retryTotalTimeoutExceeded() { assertRetrying(retrySettings); } + @Test + public void retryUsingContextTotalTimeoutExceeded() { + Throwable throwable = + new UnavailableException(null, FakeStatusCode.of(StatusCode.Code.INTERNAL), false); + Mockito.when(callInt.futureCall((Integer) Mockito.any(), (ApiCallContext) Mockito.any())) + .thenReturn(RetryingTest.immediateFailedFuture(throwable)) + .thenReturn(ApiFutures.immediateFuture(2)); + + RetrySettings retrySettings = + FAST_RETRY_SETTINGS + .toBuilder() + .setInitialRetryDelay(Duration.ofMillis(Integer.MAX_VALUE)) + .setMaxRetryDelay(Duration.ofMillis(Integer.MAX_VALUE)) + .build(); + + try { + assertRetryingUsingContext( + FAILING_RETRY_SETTINGS, + FakeCallContext.createDefault() + .withRetrySettings(retrySettings) + .withRetryableCodes(Sets.newSet(StatusCode.Code.INTERNAL))); + fail("missing expected exception"); + } catch (ApiException e) { + assertEquals(Code.INTERNAL, e.getStatusCode().getCode()); + } + } + @Test(expected = ApiException.class) public void retryMaxAttemptsExceeded() { Throwable throwable = @@ -139,6 +198,27 @@ public void retryMaxAttemptsExceeded() { assertRetrying(FAST_RETRY_SETTINGS.toBuilder().setMaxAttempts(2).build()); } + @Test + public void retryUsingContextMaxAttemptsExceeded() { + Throwable throwable = + new UnavailableException(null, FakeStatusCode.of(StatusCode.Code.INTERNAL), false); + Mockito.when(callInt.futureCall((Integer) Mockito.any(), (ApiCallContext) Mockito.any())) + .thenReturn(RetryingTest.immediateFailedFuture(throwable)) + .thenReturn(RetryingTest.immediateFailedFuture(throwable)) + .thenReturn(ApiFutures.immediateFuture(2)); + + try { + assertRetryingUsingContext( + FAILING_RETRY_SETTINGS, + FakeCallContext.createDefault() + .withRetrySettings(FAST_RETRY_SETTINGS.toBuilder().setMaxAttempts(2).build()) + .withRetryableCodes(Sets.newSet(StatusCode.Code.INTERNAL))); + fail("missing expected exception"); + } catch (ApiException e) { + assertEquals(Code.INTERNAL, e.getStatusCode().getCode()); + } + } + @Test public void retryWithinMaxAttempts() { Throwable throwable = @@ -151,6 +231,22 @@ public void retryWithinMaxAttempts() { assertRetrying(FAST_RETRY_SETTINGS.toBuilder().setMaxAttempts(3).build()); } + @Test + public void retryUsingContextWithinMaxAttempts() { + Throwable throwable = + new UnavailableException(null, FakeStatusCode.of(StatusCode.Code.INTERNAL), false); + Mockito.when(callInt.futureCall((Integer) Mockito.any(), (ApiCallContext) Mockito.any())) + .thenReturn(RetryingTest.immediateFailedFuture(throwable)) + .thenReturn(RetryingTest.immediateFailedFuture(throwable)) + .thenReturn(ApiFutures.immediateFuture(2)); + + assertRetryingUsingContext( + FAILING_RETRY_SETTINGS, + FakeCallContext.createDefault() + .withRetrySettings(FAST_RETRY_SETTINGS.toBuilder().setMaxAttempts(3).build()) + .withRetryableCodes(Sets.newSet(StatusCode.Code.INTERNAL))); + } + @Test public void retryWithOnlyMaxAttempts() { Throwable throwable = @@ -167,6 +263,26 @@ public void retryWithOnlyMaxAttempts() { .futureCall(Mockito.any(), Mockito.any()); } + @Test + public void retryUsingContextWithOnlyMaxAttempts() { + Throwable throwable = + new UnavailableException(null, FakeStatusCode.of(StatusCode.Code.INTERNAL), false); + Mockito.when(callInt.futureCall(Mockito.any(), Mockito.any())) + .thenReturn(RetryingTest.immediateFailedFuture(throwable)) + .thenReturn(RetryingTest.immediateFailedFuture(throwable)) + .thenReturn(ApiFutures.immediateFuture(2)); + + RetrySettings retrySettings = RetrySettings.newBuilder().setMaxAttempts(3).build(); + + assertRetryingUsingContext( + FAILING_RETRY_SETTINGS, + FakeCallContext.createDefault() + .withRetrySettings(retrySettings) + .withRetryableCodes(Sets.newSet(StatusCode.Code.INTERNAL))); + Mockito.verify(callInt, Mockito.times(3)) + .futureCall(Mockito.any(), Mockito.any()); + } + @Test public void retryWithoutRetrySettings() { Mockito.when(callInt.futureCall(Mockito.any(), Mockito.any())) @@ -178,6 +294,18 @@ public void retryWithoutRetrySettings() { Mockito.verify(callInt).futureCall(Mockito.any(), Mockito.any()); } + @Test + public void retryUsingContextWithoutRetrySettings() { + Mockito.when(callInt.futureCall(Mockito.any(), Mockito.any())) + .thenReturn(ApiFutures.immediateFuture(2)); + + RetrySettings retrySettings = RetrySettings.newBuilder().build(); + + assertRetryingUsingContext( + FAILING_RETRY_SETTINGS, FakeCallContext.createDefault().withRetrySettings(retrySettings)); + Mockito.verify(callInt).futureCall(Mockito.any(), Mockito.any()); + } + @Test public void retryOnStatusUnknown() { Throwable throwable = @@ -221,6 +349,27 @@ public void retryNoRecover() { } } + @Test + public void retryUsingContextNoRecover() { + Throwable throwable = + new FailedPreconditionException( + "foobar", null, FakeStatusCode.of(StatusCode.Code.FAILED_PRECONDITION), false); + Mockito.when(callInt.futureCall((Integer) Mockito.any(), (ApiCallContext) Mockito.any())) + .thenReturn(RetryingTest.immediateFailedFuture(throwable)) + .thenReturn(ApiFutures.immediateFuture(2)); + try { + assertRetryingUsingContext( + FAILING_RETRY_SETTINGS, + FakeCallContext.createDefault() + .withRetrySettings(FAST_RETRY_SETTINGS) + .withRetryableCodes( + Sets.newSet(Code.UNAVAILABLE, Code.DEADLINE_EXCEEDED, Code.UNKNOWN))); + Assert.fail("Callable should have thrown an exception"); + } catch (ApiException expected) { + Truth.assertThat(expected).isSameInstanceAs(throwable); + } + } + @Test public void retryKeepFailing() { Throwable throwable = @@ -295,4 +444,11 @@ private void assertRetrying(RetrySettings retrySettings) { FakeCallableFactory.createUnaryCallable(callInt, callSettings, clientContext); Truth.assertThat(callable.call(1)).isEqualTo(2); } + + private void assertRetryingUsingContext(RetrySettings retrySettings, ApiCallContext context) { + UnaryCallSettings callSettings = createSettings(retrySettings); + UnaryCallable callable = + FakeCallableFactory.createUnaryCallable(callInt, callSettings, clientContext); + Truth.assertThat(callable.call(1, context)).isEqualTo(2); + } } diff --git a/gax/src/test/java/com/google/api/gax/rpc/StreamingCallableTest.java b/gax/src/test/java/com/google/api/gax/rpc/StreamingCallableTest.java index 46af5daf6..f946aff92 100644 --- a/gax/src/test/java/com/google/api/gax/rpc/StreamingCallableTest.java +++ b/gax/src/test/java/com/google/api/gax/rpc/StreamingCallableTest.java @@ -29,6 +29,9 @@ */ package com.google.api.gax.rpc; +import static org.junit.Assert.assertSame; + +import com.google.api.gax.retrying.RetrySettings; import com.google.api.gax.rpc.StatusCode.Code; import com.google.api.gax.rpc.testing.FakeCallContext; import com.google.api.gax.rpc.testing.FakeCallableFactory; @@ -38,9 +41,11 @@ import com.google.api.gax.rpc.testing.FakeTransportChannel; import com.google.auth.Credentials; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import com.google.common.truth.Truth; import java.util.ArrayList; import java.util.List; +import java.util.Set; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -125,8 +130,8 @@ public void testClientStreamingCall() { ClientStreamingCallable callable = stashCallable.withDefaultCallContext(defaultCallContext); callable.clientStreamingCall(observer); - Truth.assertThat(stashCallable.getActualObserver()).isSameInstanceAs(observer); - Truth.assertThat(stashCallable.getContext()).isSameInstanceAs(defaultCallContext); + assertSame(observer, stashCallable.getActualObserver()); + assertSame(defaultCallContext, stashCallable.getContext()); } @Test @@ -134,17 +139,29 @@ public void testClientStreamingCall() { public void testClientStreamingCallWithContext() { FakeChannel channel = new FakeChannel(); Credentials credentials = Mockito.mock(Credentials.class); + RetrySettings retrySettings = Mockito.mock(RetrySettings.class); + Set retryableCodes = + ImmutableSet.of( + StatusCode.Code.INTERNAL, + StatusCode.Code.UNAVAILABLE, + StatusCode.Code.DEADLINE_EXCEEDED); ApiCallContext context = - FakeCallContext.createDefault().withChannel(channel).withCredentials(credentials); + FakeCallContext.createDefault() + .withChannel(channel) + .withCredentials(credentials) + .withRetrySettings(retrySettings) + .withRetryableCodes(retryableCodes); ClientStreamingStashCallable stashCallable = new ClientStreamingStashCallable<>(); ApiStreamObserver observer = Mockito.mock(ApiStreamObserver.class); ClientStreamingCallable callable = stashCallable.withDefaultCallContext(FakeCallContext.createDefault()); callable.clientStreamingCall(observer, context); - Truth.assertThat(stashCallable.getActualObserver()).isSameInstanceAs(observer); + assertSame(observer, stashCallable.getActualObserver()); FakeCallContext actualContext = (FakeCallContext) stashCallable.getContext(); - Truth.assertThat(actualContext.getChannel()).isSameInstanceAs(channel); - Truth.assertThat(actualContext.getCredentials()).isSameInstanceAs(credentials); + assertSame(channel, actualContext.getChannel()); + assertSame(credentials, actualContext.getCredentials()); + assertSame(retrySettings, actualContext.getRetrySettings()); + assertSame(retryableCodes, actualContext.getRetryableCodes()); } } diff --git a/gax/src/test/java/com/google/api/gax/rpc/StreamingRetryAlgorithmTest.java b/gax/src/test/java/com/google/api/gax/rpc/StreamingRetryAlgorithmTest.java new file mode 100644 index 000000000..740000723 --- /dev/null +++ b/gax/src/test/java/com/google/api/gax/rpc/StreamingRetryAlgorithmTest.java @@ -0,0 +1,228 @@ +/* + * Copyright 2021 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.google.api.gax.rpc; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.google.api.core.ApiClock; +import com.google.api.gax.retrying.BasicResultRetryAlgorithm; +import com.google.api.gax.retrying.ExponentialRetryAlgorithm; +import com.google.api.gax.retrying.RetrySettings; +import com.google.api.gax.retrying.RetryingContext; +import com.google.api.gax.retrying.ServerStreamingAttemptException; +import com.google.api.gax.retrying.StreamingRetryAlgorithm; +import com.google.api.gax.retrying.TimedAttemptSettings; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mockito; +import org.threeten.bp.Duration; + +@RunWith(JUnit4.class) +public class StreamingRetryAlgorithmTest { + private static final RetrySettings DEFAULT_RETRY_SETTINGS = + RetrySettings.newBuilder() + .setInitialRetryDelay(Duration.ofMillis(10L)) + .setInitialRpcTimeout(Duration.ofMillis(100L)) + .setMaxAttempts(10) + .setMaxRetryDelay(Duration.ofSeconds(10L)) + .setMaxRpcTimeout(Duration.ofSeconds(30L)) + .setRetryDelayMultiplier(1.4) + .setRpcTimeoutMultiplier(1.5) + .setTotalTimeout(Duration.ofMinutes(10L)) + .build(); + + private static final RetrySettings CONTEXT_RETRY_SETTINGS = + RetrySettings.newBuilder() + .setInitialRetryDelay(Duration.ofMillis(20L)) + .setInitialRpcTimeout(Duration.ofMillis(200L)) + .setMaxAttempts(10) + .setMaxRetryDelay(Duration.ofSeconds(20L)) + .setMaxRpcTimeout(Duration.ofSeconds(60L)) + .setRetryDelayMultiplier(2.4) + .setRpcTimeoutMultiplier(2.5) + .setTotalTimeout(Duration.ofMinutes(20L)) + .build(); + + @Test + public void testFirstAttemptUsesDefaultSettings() { + RetryingContext context = mock(RetryingContext.class); + BasicResultRetryAlgorithm resultAlgorithm = new BasicResultRetryAlgorithm<>(); + ExponentialRetryAlgorithm timedAlgorithm = + new ExponentialRetryAlgorithm(DEFAULT_RETRY_SETTINGS, mock(ApiClock.class)); + + StreamingRetryAlgorithm algorithm = + new StreamingRetryAlgorithm<>(resultAlgorithm, timedAlgorithm); + + TimedAttemptSettings attempt = algorithm.createFirstAttempt(context); + assertThat(attempt.getGlobalSettings()).isSameInstanceAs(DEFAULT_RETRY_SETTINGS); + assertThat(attempt.getRpcTimeout()).isEqualTo(DEFAULT_RETRY_SETTINGS.getInitialRpcTimeout()); + } + + @Test + public void testFirstAttemptUsesContextSettings() { + RetryingContext context = mock(RetryingContext.class); + when(context.getRetrySettings()).thenReturn(CONTEXT_RETRY_SETTINGS); + BasicResultRetryAlgorithm resultAlgorithm = new BasicResultRetryAlgorithm<>(); + ExponentialRetryAlgorithm timedAlgorithm = + new ExponentialRetryAlgorithm(DEFAULT_RETRY_SETTINGS, mock(ApiClock.class)); + + StreamingRetryAlgorithm algorithm = + new StreamingRetryAlgorithm<>(resultAlgorithm, timedAlgorithm); + + TimedAttemptSettings attempt = algorithm.createFirstAttempt(context); + assertThat(attempt.getGlobalSettings()).isSameInstanceAs(CONTEXT_RETRY_SETTINGS); + assertThat(attempt.getRpcTimeout()).isEqualTo(CONTEXT_RETRY_SETTINGS.getInitialRpcTimeout()); + } + + @Test + public void testNextAttemptReturnsNullWhenShouldNotRetry() { + RetryingContext context = mock(RetryingContext.class); + @SuppressWarnings("unchecked") + BasicResultRetryAlgorithm resultAlgorithm = mock(BasicResultRetryAlgorithm.class); + UnavailableException exception = mock(UnavailableException.class); + when(resultAlgorithm.shouldRetry(context, exception, null)).thenReturn(false); + ExponentialRetryAlgorithm timedAlgorithm = + new ExponentialRetryAlgorithm(DEFAULT_RETRY_SETTINGS, mock(ApiClock.class)); + + StreamingRetryAlgorithm algorithm = + new StreamingRetryAlgorithm<>(resultAlgorithm, timedAlgorithm); + + TimedAttemptSettings attempt = + algorithm.createNextAttempt(context, exception, null, mock(TimedAttemptSettings.class)); + assertThat(attempt).isNull(); + + TimedAttemptSettings attemptWithoutContext = + algorithm.createNextAttempt(exception, null, mock(TimedAttemptSettings.class)); + assertThat(attemptWithoutContext).isNull(); + } + + @Test + public void testNextAttemptReturnsResultAlgorithmSettingsWhenShouldRetry() { + RetryingContext context = mock(RetryingContext.class); + @SuppressWarnings("unchecked") + BasicResultRetryAlgorithm resultAlgorithm = mock(BasicResultRetryAlgorithm.class); + UnavailableException exception = mock(UnavailableException.class); + when(resultAlgorithm.shouldRetry(context, exception, null)).thenReturn(true); + TimedAttemptSettings next = mock(TimedAttemptSettings.class); + when(resultAlgorithm.createNextAttempt( + Mockito.eq(context), + Mockito.eq(exception), + Mockito.isNull(), + any(TimedAttemptSettings.class))) + .thenReturn(next); + ExponentialRetryAlgorithm timedAlgorithm = + new ExponentialRetryAlgorithm(DEFAULT_RETRY_SETTINGS, mock(ApiClock.class)); + + StreamingRetryAlgorithm algorithm = + new StreamingRetryAlgorithm<>(resultAlgorithm, timedAlgorithm); + + TimedAttemptSettings first = algorithm.createFirstAttempt(context); + TimedAttemptSettings attempt = algorithm.createNextAttempt(context, exception, null, first); + assertThat(attempt).isSameInstanceAs(next); + } + + @Test + public void testNextAttemptResetsTimedSettings() { + RetryingContext context = mock(RetryingContext.class); + BasicResultRetryAlgorithm resultAlgorithm = new BasicResultRetryAlgorithm<>(); + + ServerStreamingAttemptException exception = mock(ServerStreamingAttemptException.class); + when(exception.canResume()).thenReturn(true); + when(exception.hasSeenResponses()).thenReturn(true); + UnavailableException cause = mock(UnavailableException.class); + when(exception.getCause()).thenReturn(cause); + + ExponentialRetryAlgorithm timedAlgorithm = + new ExponentialRetryAlgorithm(DEFAULT_RETRY_SETTINGS, mock(ApiClock.class)); + StreamingRetryAlgorithm algorithm = + new StreamingRetryAlgorithm<>(resultAlgorithm, timedAlgorithm); + + TimedAttemptSettings first = algorithm.createFirstAttempt(context); + TimedAttemptSettings second = + algorithm.createNextAttempt(context, mock(Exception.class), null, first); + TimedAttemptSettings third = algorithm.createNextAttempt(context, exception, null, second); + assertThat(third.getFirstAttemptStartTimeNanos()) + .isEqualTo(first.getFirstAttemptStartTimeNanos()); + // The timeout values are reset to the second call. + assertThat(third.getRpcTimeout()).isEqualTo(second.getRpcTimeout()); + } + + @Test + public void testShouldNotRetryIfAttemptIsNonResumable() { + RetryingContext context = mock(RetryingContext.class); + + ServerStreamingAttemptException exception = mock(ServerStreamingAttemptException.class); + when(exception.canResume()).thenReturn(false); + UnavailableException cause = mock(UnavailableException.class); + when(exception.getCause()).thenReturn(cause); + + BasicResultRetryAlgorithm resultAlgorithm = new BasicResultRetryAlgorithm<>(); + + ExponentialRetryAlgorithm timedAlgorithm = + new ExponentialRetryAlgorithm(DEFAULT_RETRY_SETTINGS, mock(ApiClock.class)); + StreamingRetryAlgorithm algorithm = + new StreamingRetryAlgorithm<>(resultAlgorithm, timedAlgorithm); + + // This should return false because the attempt exception indicates that it is non-resumable. + boolean shouldRetry = + algorithm.shouldRetry(context, exception, null, mock(TimedAttemptSettings.class)); + assertThat(shouldRetry).isFalse(); + + boolean shouldRetryWithoutContext = + algorithm.shouldRetry(exception, null, mock(TimedAttemptSettings.class)); + assertThat(shouldRetryWithoutContext).isFalse(); + } + + @Test + public void testShouldRetryIfAllSayYes() { + RetryingContext context = mock(RetryingContext.class); + + ServerStreamingAttemptException exception = mock(ServerStreamingAttemptException.class); + when(exception.canResume()).thenReturn(true); + UnavailableException cause = mock(UnavailableException.class); + when(exception.getCause()).thenReturn(cause); + + BasicResultRetryAlgorithm resultAlgorithm = new BasicResultRetryAlgorithm<>(); + + ExponentialRetryAlgorithm timedAlgorithm = mock(ExponentialRetryAlgorithm.class); + when(timedAlgorithm.shouldRetry(Mockito.eq(context), any(TimedAttemptSettings.class))) + .thenReturn(true); + StreamingRetryAlgorithm algorithm = + new StreamingRetryAlgorithm<>(resultAlgorithm, timedAlgorithm); + + boolean shouldRetry = + algorithm.shouldRetry(context, exception, null, mock(TimedAttemptSettings.class)); + assertThat(shouldRetry).isTrue(); + } +} diff --git a/gax/src/test/java/com/google/api/gax/rpc/UnaryCallableTest.java b/gax/src/test/java/com/google/api/gax/rpc/UnaryCallableTest.java index ffc5185a7..0e16545d4 100644 --- a/gax/src/test/java/com/google/api/gax/rpc/UnaryCallableTest.java +++ b/gax/src/test/java/com/google/api/gax/rpc/UnaryCallableTest.java @@ -29,11 +29,19 @@ */ package com.google.api.gax.rpc; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; + +import com.google.api.gax.retrying.RetrySettings; import com.google.api.gax.rpc.testing.FakeCallContext; import com.google.api.gax.rpc.testing.FakeChannel; import com.google.api.gax.rpc.testing.FakeSimpleApi.StashCallable; import com.google.auth.Credentials; -import com.google.common.truth.Truth; +import com.google.common.collect.ImmutableSet; +import java.util.Set; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -48,10 +56,10 @@ public void simpleCall() throws Exception { StashCallable stashCallable = new StashCallable<>(1); Integer response = stashCallable.call(2, FakeCallContext.createDefault()); - Truth.assertThat(response).isEqualTo(Integer.valueOf(1)); + assertEquals(Integer.valueOf(1), response); FakeCallContext callContext = (FakeCallContext) stashCallable.getContext(); - Truth.assertThat(callContext.getChannel()).isNull(); - Truth.assertThat(callContext.getCredentials()).isNull(); + assertNull(callContext.getChannel()); + assertNull(callContext.getCredentials()); } @Test @@ -62,25 +70,37 @@ public void call() throws Exception { stashCallable.withDefaultCallContext(defaultCallContext); Integer response = callable.call(2); - Truth.assertThat(response).isEqualTo(Integer.valueOf(1)); - Truth.assertThat(stashCallable.getContext()).isNotNull(); - Truth.assertThat(stashCallable.getContext()).isSameInstanceAs(defaultCallContext); + assertEquals(Integer.valueOf(1), response); + assertNotNull(stashCallable.getContext()); + assertSame(defaultCallContext, stashCallable.getContext()); } @Test public void callWithContext() throws Exception { FakeChannel channel = new FakeChannel(); Credentials credentials = Mockito.mock(Credentials.class); + RetrySettings retrySettings = Mockito.mock(RetrySettings.class); + Set retryableCodes = + ImmutableSet.of( + StatusCode.Code.INTERNAL, + StatusCode.Code.UNAVAILABLE, + StatusCode.Code.DEADLINE_EXCEEDED); ApiCallContext context = - FakeCallContext.createDefault().withChannel(channel).withCredentials(credentials); + FakeCallContext.createDefault() + .withChannel(channel) + .withCredentials(credentials) + .withRetrySettings(retrySettings) + .withRetryableCodes(retryableCodes); StashCallable stashCallable = new StashCallable<>(1); UnaryCallable callable = stashCallable.withDefaultCallContext(FakeCallContext.createDefault()); Integer response = callable.call(2, context); - Truth.assertThat(response).isEqualTo(Integer.valueOf(1)); + assertEquals(Integer.valueOf(1), response); FakeCallContext actualContext = (FakeCallContext) stashCallable.getContext(); - Truth.assertThat(actualContext.getChannel()).isSameInstanceAs(channel); - Truth.assertThat(actualContext.getCredentials()).isSameInstanceAs(credentials); + assertSame(channel, actualContext.getChannel()); + assertSame(credentials, actualContext.getCredentials()); + assertSame(retrySettings, actualContext.getRetrySettings()); + assertThat(actualContext.getRetryableCodes()).containsExactlyElementsIn(retryableCodes); } } diff --git a/gax/src/test/java/com/google/api/gax/rpc/testing/FakeCallContext.java b/gax/src/test/java/com/google/api/gax/rpc/testing/FakeCallContext.java index 61f668573..09696b9ff 100644 --- a/gax/src/test/java/com/google/api/gax/rpc/testing/FakeCallContext.java +++ b/gax/src/test/java/com/google/api/gax/rpc/testing/FakeCallContext.java @@ -30,8 +30,10 @@ package com.google.api.gax.rpc.testing; import com.google.api.core.InternalApi; +import com.google.api.gax.retrying.RetrySettings; import com.google.api.gax.rpc.ApiCallContext; import com.google.api.gax.rpc.ClientContext; +import com.google.api.gax.rpc.StatusCode; import com.google.api.gax.rpc.TransportChannel; import com.google.api.gax.rpc.internal.Headers; import com.google.api.gax.tracing.ApiTracer; @@ -39,8 +41,10 @@ import com.google.auth.Credentials; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import java.util.List; import java.util.Map; +import java.util.Set; import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.threeten.bp.Duration; @@ -54,6 +58,8 @@ public class FakeCallContext implements ApiCallContext { private final Duration streamIdleTimeout; private final ImmutableMap> extraHeaders; private final ApiTracer tracer; + private final RetrySettings retrySettings; + private final ImmutableSet retryableCodes; private FakeCallContext( Credentials credentials, @@ -62,7 +68,9 @@ private FakeCallContext( Duration streamWaitTimeout, Duration streamIdleTimeout, ImmutableMap> extraHeaders, - ApiTracer tracer) { + ApiTracer tracer, + RetrySettings retrySettings, + Set retryableCodes) { this.credentials = credentials; this.channel = channel; this.timeout = timeout; @@ -70,11 +78,13 @@ private FakeCallContext( this.streamIdleTimeout = streamIdleTimeout; this.extraHeaders = extraHeaders; this.tracer = tracer; + this.retrySettings = retrySettings; + this.retryableCodes = retryableCodes == null ? null : ImmutableSet.copyOf(retryableCodes); } public static FakeCallContext createDefault() { return new FakeCallContext( - null, null, null, null, null, ImmutableMap.>of(), null); + null, null, null, null, null, ImmutableMap.>of(), null, null, null); } @Override @@ -135,6 +145,16 @@ public ApiCallContext merge(ApiCallContext inputCallContext) { newTracer = this.tracer; } + RetrySettings newRetrySettings = fakeCallContext.retrySettings; + if (newRetrySettings == null) { + newRetrySettings = this.retrySettings; + } + + Set newRetryableCodes = fakeCallContext.retryableCodes; + if (newRetryableCodes == null) { + newRetryableCodes = this.retryableCodes; + } + ImmutableMap> newExtraHeaders = Headers.mergeHeaders(extraHeaders, fakeCallContext.extraHeaders); return new FakeCallContext( @@ -144,7 +164,43 @@ public ApiCallContext merge(ApiCallContext inputCallContext) { newStreamWaitTimeout, newStreamIdleTimeout, newExtraHeaders, - newTracer); + newTracer, + newRetrySettings, + newRetryableCodes); + } + + public RetrySettings getRetrySettings() { + return retrySettings; + } + + public FakeCallContext withRetrySettings(RetrySettings retrySettings) { + return new FakeCallContext( + this.credentials, + this.channel, + this.timeout, + this.streamWaitTimeout, + this.streamIdleTimeout, + this.extraHeaders, + this.tracer, + retrySettings, + this.retryableCodes); + } + + public Set getRetryableCodes() { + return retryableCodes; + } + + public FakeCallContext withRetryableCodes(Set retryableCodes) { + return new FakeCallContext( + this.credentials, + this.channel, + this.timeout, + this.streamWaitTimeout, + this.streamIdleTimeout, + this.extraHeaders, + this.tracer, + this.retrySettings, + retryableCodes); } public Credentials getCredentials() { @@ -181,7 +237,9 @@ public FakeCallContext withCredentials(Credentials credentials) { this.streamWaitTimeout, this.streamIdleTimeout, this.extraHeaders, - this.tracer); + this.tracer, + this.retrySettings, + this.retryableCodes); } @Override @@ -203,7 +261,9 @@ public FakeCallContext withChannel(FakeChannel channel) { this.streamWaitTimeout, this.streamIdleTimeout, this.extraHeaders, - this.tracer); + this.tracer, + this.retrySettings, + this.retryableCodes); } @Override @@ -225,7 +285,9 @@ public FakeCallContext withTimeout(Duration timeout) { this.streamWaitTimeout, this.streamIdleTimeout, this.extraHeaders, - this.tracer); + this.tracer, + this.retrySettings, + this.retryableCodes); } @Override @@ -237,7 +299,9 @@ public ApiCallContext withStreamWaitTimeout(@Nullable Duration streamWaitTimeout streamWaitTimeout, this.streamIdleTimeout, this.extraHeaders, - this.tracer); + this.tracer, + this.retrySettings, + this.retryableCodes); } @Override @@ -250,7 +314,9 @@ public ApiCallContext withStreamIdleTimeout(@Nullable Duration streamIdleTimeout this.streamWaitTimeout, streamIdleTimeout, this.extraHeaders, - this.tracer); + this.tracer, + this.retrySettings, + this.retryableCodes); } @Override @@ -265,7 +331,9 @@ public ApiCallContext withExtraHeaders(Map> extraHeaders) { streamWaitTimeout, streamIdleTimeout, newExtraHeaders, - this.tracer); + this.tracer, + this.retrySettings, + this.retryableCodes); } @Override @@ -295,7 +363,9 @@ public ApiCallContext withTracer(@Nonnull ApiTracer tracer) { this.streamWaitTimeout, this.streamIdleTimeout, this.extraHeaders, - tracer); + tracer, + this.retrySettings, + this.retryableCodes); } public static FakeCallContext create(ClientContext clientContext) {