Skip to content

Commit f5b49a7

Browse files
authored
Add retry conditions to the strategy if none is configured (#5648)
* Add retry conditions to the strategy if none is configured * Update core/retries/src/main/java/software/amazon/awssdk/retries/internal/BaseRetryStrategy.java * Fix a typo in the name of the class
1 parent 85857b5 commit f5b49a7

File tree

5 files changed

+209
-0
lines changed

5 files changed

+209
-0
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"category": "AWS SDK for Java v2",
3+
"contributor": "",
4+
"type": "bugfix",
5+
"description": "This change adds the default retry conditions in the client builder if none are configured to behave similarly to retry policies that are configured behind the scenes without the users having to do that themselves. This will prevent customers using directly the retry strategies builders from `DefaultRetryStrategy` to end up with a no-op strategy."
6+
}

core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilder.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
import software.amazon.awssdk.core.client.config.SdkClientOption;
5252
import software.amazon.awssdk.core.interceptor.ExecutionInterceptor;
5353
import software.amazon.awssdk.core.internal.SdkInternalTestAdvancedClientOption;
54+
import software.amazon.awssdk.core.internal.retry.SdkDefaultRetryStrategy;
5455
import software.amazon.awssdk.core.retry.RetryMode;
5556
import software.amazon.awssdk.core.retry.RetryPolicy;
5657
import software.amazon.awssdk.http.SdkHttpClient;
@@ -64,6 +65,7 @@
6465
import software.amazon.awssdk.regions.ServiceMetadataAdvancedOption;
6566
import software.amazon.awssdk.regions.providers.DefaultAwsRegionProviderChain;
6667
import software.amazon.awssdk.retries.api.RetryStrategy;
68+
import software.amazon.awssdk.retries.internal.BaseRetryStrategy;
6769
import software.amazon.awssdk.utils.AttributeMap;
6870
import software.amazon.awssdk.utils.AttributeMap.LazyValueSource;
6971
import software.amazon.awssdk.utils.CollectionUtils;
@@ -422,6 +424,20 @@ private void configureRetryPolicy(SdkClientConfiguration.Builder config) {
422424
private void configureRetryStrategy(SdkClientConfiguration.Builder config) {
423425
RetryStrategy strategy = config.option(SdkClientOption.RETRY_STRATEGY);
424426
if (strategy != null) {
427+
// Fix the retry strategy if no retry predicates were configured. Users using
428+
// DefaultRetryStrategy do not have any conditions configured as that package
429+
// does not know anything about the SDK or AWS retry conditions. This mimics the
430+
// retry policies behavior that are configured behind the scenes and avoids the
431+
// risk of customers inadvertently using a no-op retry strategy.
432+
if (strategy.maxAttempts() > 1
433+
&& (strategy instanceof BaseRetryStrategy)
434+
&& !((BaseRetryStrategy) strategy).hasRetryPredicates()
435+
) {
436+
RetryStrategy.Builder<?, ?> builder = strategy.toBuilder();
437+
SdkDefaultRetryStrategy.configureStrategy(builder);
438+
AwsRetryStrategy.configureStrategy(builder);
439+
config.option(SdkClientOption.RETRY_STRATEGY, builder.build());
440+
}
425441
return;
426442
}
427443
config.lazyOption(SdkClientOption.RETRY_STRATEGY, this::resolveAwsRetryStrategy);

core/retries/src/main/java/software/amazon/awssdk/retries/internal/BaseRetryStrategy.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,15 @@ protected int exceptionCost(RefreshRetryTokenRequest request) {
187187
return 0;
188188
}
189189

190+
/**
191+
* Returns true if there are retry predicates configured for this retry strategy.
192+
*
193+
* @return true if there are retry predicates configured for this retry strategy.
194+
*/
195+
public final boolean hasRetryPredicates() {
196+
return !retryPredicates.isEmpty();
197+
}
198+
190199
private DefaultRetryToken refreshToken(RefreshRetryTokenRequest request, AcquireResponse acquireResponse) {
191200
DefaultRetryToken token = asDefaultRetryToken(request.token());
192201
return token.toBuilder()

core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/retry/SdkDefaultRetryStrategy.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,22 @@ public static AdaptiveRetryStrategy.Builder adaptiveRetryStrategyBuilder() {
168168
return builder;
169169
}
170170

171+
/**
172+
* Configures a retry strategy using its builder to add SDK-generic retry exceptions.
173+
*
174+
* @param builder The builder to add the SDK-generic retry exceptions
175+
* @return The given builder
176+
*/
177+
public static RetryStrategy.Builder<?, ?> configureStrategy(RetryStrategy.Builder<?, ?> builder) {
178+
builder.retryOnException(SdkDefaultRetryStrategy::retryOnRetryableException)
179+
.retryOnException(SdkDefaultRetryStrategy::retryOnStatusCodes)
180+
.retryOnException(SdkDefaultRetryStrategy::retryOnClockSkewException)
181+
.retryOnException(SdkDefaultRetryStrategy::retryOnThrottlingCondition);
182+
SdkDefaultRetrySetting.RETRYABLE_EXCEPTIONS.forEach(builder::retryOnExceptionOrCauseInstanceOf);
183+
builder.treatAsThrottling(SdkDefaultRetryStrategy::treatAsThrottling);
184+
return builder;
185+
}
186+
171187
private static boolean treatAsThrottling(Throwable t) {
172188
if (t instanceof SdkException) {
173189
return RetryUtils.isThrottlingException((SdkException) t);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.protocol.tests.retry;
17+
18+
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
19+
import static com.github.tomakehurst.wiremock.client.WireMock.post;
20+
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
21+
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
22+
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
23+
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
24+
25+
import com.github.tomakehurst.wiremock.junit.WireMockRule;
26+
import com.github.tomakehurst.wiremock.stubbing.Scenario;
27+
import java.net.URI;
28+
import org.junit.Before;
29+
import org.junit.Rule;
30+
import org.junit.Test;
31+
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
32+
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
33+
import software.amazon.awssdk.regions.Region;
34+
import software.amazon.awssdk.retries.DefaultRetryStrategy;
35+
import software.amazon.awssdk.services.protocoljsonrpc.ProtocolJsonRpcClient;
36+
import software.amazon.awssdk.services.protocoljsonrpc.model.AllTypesRequest;
37+
import software.amazon.awssdk.services.protocoljsonrpc.model.AllTypesResponse;
38+
import software.amazon.awssdk.services.protocoljsonrpc.model.ProtocolJsonRpcException;
39+
40+
public class RetryStrategyBackfillsEmptyRetryConditions {
41+
42+
private static final String PATH = "/";
43+
private static final String JSON_BODY = "{\"StringMember\":\"foo\"}";
44+
45+
@Rule
46+
public WireMockRule wireMock = new WireMockRule(0);
47+
48+
private ProtocolJsonRpcClient client;
49+
50+
@Before
51+
public void setupClient() {
52+
client = ProtocolJsonRpcClient.builder()
53+
.credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("akid",
54+
"skid")))
55+
.region(Region.US_EAST_1)
56+
.overrideConfiguration(o -> o.retryStrategy(DefaultRetryStrategy.standardStrategyBuilder().build()))
57+
.endpointOverride(URI.create("http://localhost:" + wireMock.port()))
58+
.build();
59+
}
60+
61+
@Test
62+
public void shouldRetryOn500() {
63+
stubFor(post(urlEqualTo(PATH))
64+
.inScenario("retry at 500")
65+
.whenScenarioStateIs(Scenario.STARTED)
66+
.willSetStateTo("first attempt")
67+
.willReturn(aResponse()
68+
.withStatus(500)));
69+
70+
stubFor(post(urlEqualTo(PATH))
71+
.inScenario("retry at 500")
72+
.whenScenarioStateIs("first attempt")
73+
.willSetStateTo("second attempt")
74+
.willReturn(aResponse()
75+
.withStatus(200)
76+
.withBody(JSON_BODY)));
77+
78+
AllTypesResponse allTypesResponse = client.allTypes(AllTypesRequest.builder().build());
79+
assertThat(allTypesResponse).isNotNull();
80+
}
81+
82+
@Test
83+
public void shouldRetryOnRetryableAwsErrorCode() {
84+
stubFor(post(urlEqualTo(PATH))
85+
.inScenario("retry at PriorRequestNotComplete")
86+
.whenScenarioStateIs(Scenario.STARTED)
87+
.willSetStateTo("first attempt")
88+
.willReturn(aResponse()
89+
.withStatus(400)
90+
.withHeader("x-amzn-ErrorType", "PriorRequestNotComplete")
91+
.withBody("\"{\"__type\":\"PriorRequestNotComplete\",\"message\":\"Blah "
92+
+ "error\"}\"")));
93+
94+
stubFor(post(urlEqualTo(PATH))
95+
.inScenario("retry at PriorRequestNotComplete")
96+
.whenScenarioStateIs("first attempt")
97+
.willSetStateTo("second attempt")
98+
.willReturn(aResponse()
99+
.withStatus(200)
100+
.withBody(JSON_BODY)));
101+
102+
AllTypesResponse allTypesResponse = client.allTypes(AllTypesRequest.builder().build());
103+
assertThat(allTypesResponse).isNotNull();
104+
}
105+
106+
@Test
107+
public void shouldRetryOnAwsThrottlingErrorCode() {
108+
stubFor(post(urlEqualTo(PATH))
109+
.inScenario("retry at SlowDown")
110+
.whenScenarioStateIs(Scenario.STARTED)
111+
.willSetStateTo("first attempt")
112+
.willReturn(aResponse()
113+
.withStatus(400)
114+
.withHeader("x-amzn-ErrorType", "SlowDown")
115+
.withBody("\"{\"__type\":\"SlowDown\",\"message\":\"Blah "
116+
+ "error\"}\"")));
117+
118+
stubFor(post(urlEqualTo(PATH))
119+
.inScenario("retry at SlowDown")
120+
.whenScenarioStateIs("first attempt")
121+
.willSetStateTo("second attempt")
122+
.willReturn(aResponse()
123+
.withStatus(200)
124+
.withBody(JSON_BODY)));
125+
126+
AllTypesResponse allTypesResponse = client.allTypes(AllTypesRequest.builder().build());
127+
assertThat(allTypesResponse).isNotNull();
128+
}
129+
130+
@Test
131+
public void retryStrategyNone_shouldNotRetry() {
132+
stubFor(post(urlEqualTo(PATH))
133+
.inScenario("retry at 500")
134+
.whenScenarioStateIs(Scenario.STARTED)
135+
.willSetStateTo("first attempt")
136+
.willReturn(aResponse()
137+
.withStatus(500)));
138+
139+
stubFor(post(urlEqualTo(PATH))
140+
.inScenario("retry at 500")
141+
.whenScenarioStateIs("first attempt")
142+
.willSetStateTo("second attempt")
143+
.willReturn(aResponse()
144+
.withStatus(200)
145+
.withBody(JSON_BODY)));
146+
147+
ProtocolJsonRpcClient clientWithNoRetry =
148+
ProtocolJsonRpcClient.builder()
149+
.credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("akid",
150+
"skid")))
151+
.region(Region.US_EAST_1)
152+
.endpointOverride(URI.create("http://localhost:" + wireMock.port()))
153+
// When max attempts is 1 (i.e., just the first attempt is allowed but no retries),
154+
// the back filling should not happen.
155+
.overrideConfiguration(c -> c.retryStrategy(DefaultRetryStrategy.standardStrategyBuilder()
156+
.maxAttempts(1)
157+
.build()))
158+
.build();
159+
160+
assertThatThrownBy(() -> clientWithNoRetry.allTypes(AllTypesRequest.builder().build())).isInstanceOf(ProtocolJsonRpcException.class);
161+
}
162+
}

0 commit comments

Comments
 (0)