Skip to content

Commit f3749a1

Browse files
authored
Add S3 custom 503 throttling detection (#6845)
* Add S3 custom 503 throttling detection * Fix checkstyle * Add functional tests * Add changelog * Fix assertions * Fix conditional blocks
1 parent 0441df6 commit f3749a1

4 files changed

Lines changed: 167 additions & 0 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "bugfix",
3+
"category": "Amazon S3",
4+
"contributor": "",
5+
"description": "Add custom 503 throttling detection for S3 head operations"
6+
}

services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/handlers/ExceptionTranslationInterceptor.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,17 @@ public Throwable modifyException(Context.FailedExecution context, ExecutionAttri
7575
.message(message)
7676
.build();
7777
}
78+
} else if (exception.statusCode() == 503) {
79+
if ("Slow Down".equals(errorDetails.sdkHttpResponse().statusText().orElse(null))) {
80+
return S3Exception.builder()
81+
.awsErrorDetails(fillErrorDetails(errorDetails, "SlowDown",
82+
"Please reduce your request rate."))
83+
.statusCode(503)
84+
.requestId(requestId)
85+
.extendedRequestId(extendedRequestId)
86+
.message(message)
87+
.build();
88+
}
7889
} else if (errorDetails.errorMessage() == null) {
7990
// Populate the error message using the HTTP response status text. Usually that's just the value from the
8091
// HTTP spec (e.g. "Forbidden"), but sometimes S3 throws some more useful things in there, like "Slow Down".
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
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.services.s3.functionaltests;
17+
18+
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
19+
import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl;
20+
import static com.github.tomakehurst.wiremock.client.WireMock.head;
21+
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
22+
import static org.assertj.core.api.Assertions.assertThat;
23+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
24+
25+
import com.github.tomakehurst.wiremock.junit.WireMockRule;
26+
import java.net.URI;
27+
import org.junit.Before;
28+
import org.junit.Rule;
29+
import org.junit.Test;
30+
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
31+
import software.amazon.awssdk.regions.Region;
32+
import software.amazon.awssdk.services.s3.S3Client;
33+
import software.amazon.awssdk.services.s3.model.S3Exception;
34+
35+
public class HeadOperationsThrottlingTest {
36+
37+
@Rule
38+
public WireMockRule mockServer = new WireMockRule(0);
39+
40+
private S3Client client;
41+
42+
@Before
43+
public void setup() {
44+
client = S3Client.builder()
45+
.endpointOverride(URI.create("http://localhost:" + mockServer.port()))
46+
.credentialsProvider(() -> AwsBasicCredentials.create("test", "test"))
47+
.forcePathStyle(true)
48+
.region(Region.US_EAST_1)
49+
.build();
50+
}
51+
52+
@Test
53+
public void headObject503SlowDown_shouldBeThrottlingException() {
54+
stubFor(head(anyUrl()).willReturn(aResponse().withStatus(503).withStatusMessage("Slow Down")));
55+
56+
assertThatThrownBy(() -> client.headObject(r -> r.bucket("bucket").key("key")))
57+
.isInstanceOfSatisfying(S3Exception.class, e -> {
58+
assertThat(e.statusCode()).isEqualTo(503);
59+
assertThat(e.isThrottlingException()).isTrue();
60+
assertThat(e.awsErrorDetails().errorCode()).isEqualTo("SlowDown");
61+
});
62+
}
63+
64+
@Test
65+
public void headBucket503SlowDown_shouldBeThrottlingException() {
66+
stubFor(head(anyUrl()).willReturn(aResponse().withStatus(503).withStatusMessage("Slow Down")));
67+
68+
assertThatThrownBy(() -> client.headBucket(r -> r.bucket("bucket")))
69+
.isInstanceOfSatisfying(S3Exception.class, e -> {
70+
assertThat(e.statusCode()).isEqualTo(503);
71+
assertThat(e.isThrottlingException()).isTrue();
72+
assertThat(e.awsErrorDetails().errorCode()).isEqualTo("SlowDown");
73+
});
74+
}
75+
76+
@Test
77+
public void headObject503OtherException_shouldNotBeThrottlingException() {
78+
stubFor(head(anyUrl()).willReturn(aResponse().withStatus(503).withStatusMessage("Service Unavailable")));
79+
80+
assertThatThrownBy(() -> client.headObject(r -> r.bucket("bucket").key("key")))
81+
.isInstanceOfSatisfying(S3Exception.class, e -> {
82+
assertThat(e.statusCode()).isEqualTo(503);
83+
assertThat(e.isThrottlingException()).isFalse();
84+
assertThat(e.awsErrorDetails().errorCode()).isNull();
85+
});
86+
}
87+
}

services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/handlers/ExceptionTranslationInterceptorTest.java

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,46 @@ public void otherRequest_shouldNotThrowException() {
9898
assertThat(interceptor.modifyException(failedExecution, new ExecutionAttributes())).isEqualTo(s3Exception);
9999
}
100100

101+
@Test
102+
public void headObject503SlowDown_shouldBeThrottlingException() {
103+
S3Exception s3Exception = create503ThrottlingException();
104+
Context.FailedExecution failedExecution = getFailedExecution(s3Exception,
105+
HeadObjectRequest.builder().build());
106+
S3Exception modifiedException = (S3Exception) interceptor.modifyException(failedExecution, new ExecutionAttributes());
107+
assertThat(modifiedException.awsErrorDetails().errorCode()).isEqualTo("SlowDown");
108+
assertThat(modifiedException.isThrottlingException()).isTrue();
109+
}
110+
111+
@Test
112+
public void headBucket503SlowDown_shouldBeThrottlingException() {
113+
S3Exception s3Exception = create503ThrottlingException();
114+
Context.FailedExecution failedExecution = getFailedExecution(s3Exception,
115+
HeadBucketRequest.builder().build());
116+
S3Exception modifiedException = (S3Exception) interceptor.modifyException(failedExecution, new ExecutionAttributes());
117+
assertThat(modifiedException.awsErrorDetails().errorCode()).isEqualTo("SlowDown");
118+
assertThat(modifiedException.isThrottlingException()).isTrue();
119+
}
120+
121+
@Test
122+
public void headBucket503ServiceUnavailable_shouldNotBeThrottlingException() {
123+
S3Exception s3Exception = create503NonThrottlingException();
124+
Context.FailedExecution failedExecution = getFailedExecution(s3Exception,
125+
HeadBucketRequest.builder().build());
126+
S3Exception modifiedException = (S3Exception) interceptor.modifyException(failedExecution, new ExecutionAttributes());
127+
assertThat(modifiedException.awsErrorDetails().errorCode()).isNull();
128+
assertThat(modifiedException.isThrottlingException()).isFalse();
129+
}
130+
131+
@Test
132+
public void headObject503ServiceUnavailable_shouldNotBeThrottlingException() {
133+
S3Exception s3Exception = create503NonThrottlingException();
134+
Context.FailedExecution failedExecution = getFailedExecution(s3Exception,
135+
HeadObjectRequest.builder().build());
136+
S3Exception modifiedException = (S3Exception) interceptor.modifyException(failedExecution, new ExecutionAttributes());
137+
assertThat(modifiedException.awsErrorDetails().errorCode()).isNull();
138+
assertThat(modifiedException.isThrottlingException()).isFalse();
139+
}
140+
101141
private S3Exception create404S3Exception() {
102142
return (S3Exception) S3Exception.builder()
103143
.awsErrorDetails(AwsErrorDetails.builder()
@@ -118,4 +158,27 @@ private S3Exception create403S3Exception() {
118158
.statusCode(403)
119159
.build();
120160
}
161+
162+
private S3Exception create503ThrottlingException() {
163+
return (S3Exception) S3Exception.builder()
164+
.awsErrorDetails(AwsErrorDetails.builder()
165+
.sdkHttpResponse(SdkHttpFullResponse.builder()
166+
.statusText(
167+
"Slow Down")
168+
.build())
169+
.build())
170+
.statusCode(503)
171+
.build();
172+
}
173+
174+
private S3Exception create503NonThrottlingException() {
175+
return (S3Exception) S3Exception.builder()
176+
.awsErrorDetails(AwsErrorDetails.builder()
177+
.sdkHttpResponse(SdkHttpFullResponse.builder()
178+
.statusText("Service Unavailable")
179+
.build())
180+
.build())
181+
.statusCode(503)
182+
.build();
183+
}
121184
}

0 commit comments

Comments
 (0)