blob: 6b894a68137047f8cf9e04c7b598c7b3fc1071a6 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hadoop.fs.s3a;
import static org.apache.hadoop.fs.s3a.AWSCredentialProviderList.maybeTranslateCredentialException;
import static org.apache.hadoop.fs.s3a.Constants.*;
import static org.apache.hadoop.fs.s3a.S3ATestUtils.sdkClientException;
import static org.apache.hadoop.fs.s3a.S3ATestUtils.verifyExceptionClass;
import static org.apache.hadoop.fs.s3a.S3AUtils.*;
import static org.apache.hadoop.fs.s3a.audit.AuditIntegration.maybeTranslateAuditException;
import static org.apache.hadoop.fs.s3a.impl.ErrorTranslation.maybeExtractChannelException;
import static org.apache.hadoop.fs.s3a.impl.InternalConstants.*;
import static org.apache.hadoop.test.LambdaTestUtils.verifyCause;
import static org.junit.Assert.*;
import java.io.EOFException;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.nio.file.AccessDeniedException;
import java.util.concurrent.ExecutionException;
import java.util.function.Consumer;
import org.assertj.core.api.Assertions;
import org.junit.Before;
import software.amazon.awssdk.awscore.exception.AwsErrorDetails;
import software.amazon.awssdk.awscore.exception.AwsServiceException;
import software.amazon.awssdk.core.exception.ApiCallAttemptTimeoutException;
import software.amazon.awssdk.core.exception.ApiCallTimeoutException;
import software.amazon.awssdk.core.exception.SdkException;
import software.amazon.awssdk.http.SdkHttpResponse;
import software.amazon.awssdk.services.s3.model.S3Exception;
import org.junit.Test;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.s3a.api.UnsupportedRequestException;
import org.apache.hadoop.fs.s3a.audit.AuditFailureException;
import org.apache.hadoop.fs.s3a.audit.AuditOperationRejectedException;
import org.apache.hadoop.fs.s3a.impl.ErrorTranslation;
import org.apache.hadoop.io.retry.RetryPolicy;
import org.apache.hadoop.test.AbstractHadoopTestBase;
import org.apache.http.NoHttpResponseException;
import static org.apache.hadoop.test.GenericTestUtils.assertExceptionContains;
/**
* Unit test suite covering translation of AWS/network exceptions to S3A exceptions,
* and retry/recovery policies.
*/
@SuppressWarnings("ThrowableNotThrown")
public class TestS3AExceptionTranslation extends AbstractHadoopTestBase {
public static final String WFOPENSSL_0035_STREAM_IS_CLOSED =
"Unable to execute HTTP request: "
+ ErrorTranslation.OPENSSL_STREAM_CLOSED
+ " Stream is closed";
/**
* Retry policy to use in tests.
*/
private S3ARetryPolicy retryPolicy;
@Before
public void setup() {
retryPolicy = new S3ARetryPolicy(new Configuration(false));
}
@Test
public void test301ContainsRegion() throws Exception {
String region = "us-west-1";
AwsErrorDetails redirectError = AwsErrorDetails.builder()
.sdkHttpResponse(
SdkHttpResponse.builder().putHeader(BUCKET_REGION_HEADER, region).build())
.build();
S3Exception s3Exception = createS3Exception("wrong region",
SC_301_MOVED_PERMANENTLY,
redirectError);
AWSRedirectException ex = verifyTranslated(
AWSRedirectException.class, s3Exception);
assertStatusCode(SC_301_MOVED_PERMANENTLY, ex);
assertNotNull(ex.getMessage());
assertContained(ex.getMessage(), region);
assertContained(ex.getMessage(), AWS_REGION);
assertExceptionContains(AWS_REGION, ex, "region");
assertExceptionContains(region, ex, "region name");
}
protected void assertContained(String text, String contained) {
assertTrue("string \""+ contained + "\" not found in \"" + text + "\"",
text != null && text.contains(contained));
}
protected <E extends Throwable> E verifyTranslated(
int status,
Class<E> expected) throws Exception {
return verifyTranslated(expected, createS3Exception(status));
}
@Test
public void test400isBad() throws Exception {
verifyTranslated(SC_400_BAD_REQUEST, AWSBadRequestException.class);
}
@Test
public void test401isNotPermittedFound() throws Exception {
verifyTranslated(SC_401_UNAUTHORIZED, AccessDeniedException.class);
}
@Test
public void test403isNotPermittedFound() throws Exception {
verifyTranslated(SC_403_FORBIDDEN, AccessDeniedException.class);
}
/**
* 404 defaults to FileNotFound.
*/
@Test
public void test404isNotFound() throws Exception {
verifyTranslated(SC_404_NOT_FOUND, FileNotFoundException.class);
}
/**
* 404 + NoSuchBucket == Unknown bucket.
*/
@Test
public void testUnknownBucketException() throws Exception {
S3Exception ex404 = createS3Exception(b -> b
.statusCode(SC_404_NOT_FOUND)
.awsErrorDetails(AwsErrorDetails.builder()
.errorCode(ErrorTranslation.AwsErrorCodes.E_NO_SUCH_BUCKET)
.build()));
verifyTranslated(
UnknownStoreException.class,
ex404);
}
@Test
public void test410isNotFound() throws Exception {
verifyTranslated(SC_410_GONE, FileNotFoundException.class);
}
@Test
public void test416isEOF() throws Exception {
// 416 maps the the subclass of EOFException
final IOException ex = verifyTranslated(SC_416_RANGE_NOT_SATISFIABLE,
RangeNotSatisfiableEOFException.class);
Assertions.assertThat(ex)
.isInstanceOf(EOFException.class);
}
@Test
public void testGenericS3Exception() throws Exception {
// S3 exception of no known type
AWSS3IOException ex = verifyTranslated(
AWSS3IOException.class,
createS3Exception(451));
assertStatusCode(451, ex);
}
@Test
public void testGenericServiceS3Exception() throws Exception {
// service exception of no known type
AwsServiceException ase = AwsServiceException.builder()
.message("unwind")
.statusCode(SC_500_INTERNAL_SERVER_ERROR)
.build();
AWSServiceIOException ex = verifyTranslated(
AWSStatus500Exception.class,
ase);
assertStatusCode(SC_500_INTERNAL_SERVER_ERROR, ex);
}
protected void assertStatusCode(int expected, AWSServiceIOException ex) {
assertNotNull("Null exception", ex);
if (expected != ex.statusCode()) {
throw new AssertionError("Expected status code " + expected
+ "but got " + ex.statusCode(),
ex);
}
}
@Test
public void testGenericClientException() throws Exception {
// Generic Amazon exception
verifyTranslated(AWSClientIOException.class,
SdkException.builder().message("").build());
}
private static S3Exception createS3Exception(
Consumer<S3Exception.Builder> consumer) {
S3Exception.Builder builder = S3Exception.builder()
.awsErrorDetails(AwsErrorDetails.builder()
.build());
consumer.accept(builder);
return (S3Exception) builder.build();
}
private static S3Exception createS3Exception(int code) {
return createS3Exception(b -> b.message("").statusCode(code));
}
private static S3Exception createS3Exception(String message, int code,
AwsErrorDetails additionalDetails) {
S3Exception source = (S3Exception) S3Exception.builder()
.message(message)
.statusCode(code)
.awsErrorDetails(additionalDetails)
.build();
return source;
}
private static <E extends Throwable> E verifyTranslated(Class<E> clazz,
SdkException exception) throws Exception {
// Verifying that the translated exception have the correct error message.
IOException ioe = translateException("test", "/", exception);
assertExceptionContains(exception.getMessage(), ioe,
"Translated Exception should contain the error message of the "
+ "actual exception");
return verifyExceptionClass(clazz, ioe);
}
private void assertContainsInterrupted(boolean expected, Throwable thrown)
throws Throwable {
boolean wasInterrupted = containsInterruptedException(thrown) != null;
if (wasInterrupted != expected) {
throw thrown;
}
}
@Test
public void testInterruptExceptionDetecting() throws Throwable {
InterruptedException interrupted = new InterruptedException("irq");
assertContainsInterrupted(true, interrupted);
IOException ioe = new IOException("ioe");
assertContainsInterrupted(false, ioe);
assertContainsInterrupted(true, ioe.initCause(interrupted));
assertContainsInterrupted(true,
new InterruptedIOException("ioirq"));
}
@Test(expected = InterruptedIOException.class)
public void testExtractInterrupted() throws Throwable {
throw extractException("", "",
new ExecutionException(
SdkException.builder()
.cause(new InterruptedException(""))
.build()));
}
@Test(expected = InterruptedIOException.class)
public void testExtractInterruptedIO() throws Throwable {
throw extractException("", "",
new ExecutionException(
SdkException.builder()
.cause(new InterruptedIOException(""))
.build()));
}
@Test
public void testTranslateCredentialException() throws Throwable {
verifyExceptionClass(AccessDeniedException.class,
maybeTranslateCredentialException("/",
new CredentialInitializationException("Credential initialization failed")));
}
@Test
public void testTranslateNestedCredentialException() throws Throwable {
final AccessDeniedException ex =
verifyExceptionClass(AccessDeniedException.class,
maybeTranslateCredentialException("/",
sdkClientException("",
new CredentialInitializationException("Credential initialization failed"))));
// unwrap and verify that the initial client exception has been stripped
final Throwable cause = ex.getCause();
Assertions.assertThat(cause)
.isInstanceOf(CredentialInitializationException.class);
CredentialInitializationException cie = (CredentialInitializationException) cause;
Assertions.assertThat(cie.retryable())
.describedAs("Retryable flag")
.isFalse();
}
@Test
public void testTranslateNonCredentialException() throws Throwable {
Assertions.assertThat(
maybeTranslateCredentialException("/",
sdkClientException("not a credential exception", null)))
.isNull();
Assertions.assertThat(
maybeTranslateCredentialException("/",
sdkClientException("", sdkClientException("not a credential exception", null))))
.isNull();
}
@Test
public void testTranslateAuditException() throws Throwable {
verifyExceptionClass(AccessDeniedException.class,
maybeTranslateAuditException("/",
new AuditFailureException("failed")));
}
@Test
public void testTranslateNestedAuditException() throws Throwable {
verifyExceptionClass(AccessDeniedException.class,
maybeTranslateAuditException("/",
sdkClientException("", new AuditFailureException("failed"))));
}
@Test
public void testTranslateNestedAuditRejectedException() throws Throwable {
final UnsupportedRequestException ex =
verifyExceptionClass(UnsupportedRequestException.class,
maybeTranslateAuditException("/",
sdkClientException("", new AuditOperationRejectedException("rejected"))));
Assertions.assertThat(ex.getCause())
.isInstanceOf(AuditOperationRejectedException.class);
}
@Test
public void testTranslateNonAuditException() throws Throwable {
Assertions.assertThat(
maybeTranslateAuditException("/",
sdkClientException("not an audit exception", null)))
.isNull();
Assertions.assertThat(
maybeTranslateAuditException("/",
sdkClientException("", sdkClientException("not an audit exception", null))))
.isNull();
}
/**
* 504 gateway timeout is translated to a {@link AWSApiCallTimeoutException}.
*/
@Test
public void test504ToTimeout() throws Throwable {
AWSApiCallTimeoutException ex =
verifyExceptionClass(AWSApiCallTimeoutException.class,
translateException("test", "/", createS3Exception(504)));
verifyCause(S3Exception.class, ex);
}
/**
* SDK ApiCallTimeoutException is translated to a
* {@link AWSApiCallTimeoutException}.
*/
@Test
public void testApiCallTimeoutExceptionToTimeout() throws Throwable {
AWSApiCallTimeoutException ex =
verifyExceptionClass(AWSApiCallTimeoutException.class,
translateException("test", "/",
ApiCallTimeoutException.builder()
.message("timeout")
.build()));
verifyCause(ApiCallTimeoutException.class, ex);
}
/**
* SDK ApiCallAttemptTimeoutException is translated to a
* {@link AWSApiCallTimeoutException}.
*/
@Test
public void testApiCallAttemptTimeoutExceptionToTimeout() throws Throwable {
AWSApiCallTimeoutException ex =
verifyExceptionClass(AWSApiCallTimeoutException.class,
translateException("test", "/",
ApiCallAttemptTimeoutException.builder()
.message("timeout")
.build()));
verifyCause(ApiCallAttemptTimeoutException.class, ex);
// and confirm these timeouts are retried.
assertRetried(ex);
}
@Test
public void testChannelExtraction() throws Throwable {
verifyExceptionClass(HttpChannelEOFException.class,
maybeExtractChannelException("", "/",
new NoHttpResponseException("no response")));
}
@Test
public void testShadedChannelExtraction() throws Throwable {
verifyExceptionClass(HttpChannelEOFException.class,
maybeExtractChannelException("", "/",
shadedNoHttpResponse()));
}
@Test
public void testOpenSSLErrorChannelExtraction() throws Throwable {
verifyExceptionClass(HttpChannelEOFException.class,
maybeExtractChannelException("", "/",
sdkClientException(WFOPENSSL_0035_STREAM_IS_CLOSED, null)));
}
/**
* Test handling of the unshaded HTTP client exception.
*/
@Test
public void testRawNoHttpResponseExceptionRetry() throws Throwable {
assertRetried(
verifyExceptionClass(HttpChannelEOFException.class,
translateException("test", "/",
sdkClientException(new NoHttpResponseException("no response")))));
}
/**
* Test handling of the shaded HTTP client exception.
*/
@Test
public void testShadedNoHttpResponseExceptionRetry() throws Throwable {
assertRetried(
verifyExceptionClass(HttpChannelEOFException.class,
translateException("test", "/",
sdkClientException(shadedNoHttpResponse()))));
}
@Test
public void testOpenSSLErrorRetry() throws Throwable {
assertRetried(
verifyExceptionClass(HttpChannelEOFException.class,
translateException("test", "/",
sdkClientException(WFOPENSSL_0035_STREAM_IS_CLOSED, null))));
}
/**
* Create a shaded NoHttpResponseException.
* @return an exception.
*/
private static Exception shadedNoHttpResponse() {
return new software.amazon.awssdk.thirdparty.org.apache.http.NoHttpResponseException("shaded");
}
/**
* Assert that an exception is retried.
* @param ex exception
* @throws Exception failure during retry policy evaluation.
*/
private void assertRetried(final Exception ex) throws Exception {
assertRetryOutcome(ex, RetryPolicy.RetryAction.RetryDecision.RETRY);
}
/**
* Assert that the retry policy is as expected for a given exception.
* @param ex exception
* @param decision expected decision
* @throws Exception failure during retry policy evaluation.
*/
private void assertRetryOutcome(
final Exception ex,
final RetryPolicy.RetryAction.RetryDecision decision) throws Exception {
Assertions.assertThat(retryPolicy.shouldRetry(ex, 0, 0, true).action)
.describedAs("retry policy for exception %s", ex)
.isEqualTo(decision);
}
}