/* * Copyright 2010-2015 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at: * * http://aws.amazon.com/apache2.0 * * This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES * OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and * limitations under the License. */ package com.amazonaws.retry; import com.amazonaws.AmazonClientException; import com.amazonaws.AmazonServiceException; import com.amazonaws.AmazonWebServiceRequest; import com.amazonaws.ClientConfiguration; import com.amazonaws.DefaultRequest; import com.amazonaws.Request; import com.amazonaws.http.AmazonHttpClient; import com.amazonaws.http.HttpClient; import com.amazonaws.http.HttpRequest; import com.amazonaws.http.HttpResponse; import com.amazonaws.http.HttpResponseHandler; import com.amazonaws.util.StringInputStream; import org.junit.Assert; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.lang.reflect.Field; import java.net.URI; import java.util.LinkedList; import java.util.List; /** Some utility class and method for testing RetryCondition */ public class RetryPolicyTestBase { protected static ClientConfiguration clientConfiguration = new ClientConfiguration(); protected static AmazonHttpClient testedClient = new AmazonHttpClient(clientConfiguration); protected static final AmazonWebServiceRequest originalRequest = new TestAmazonWebServiceRequest(); protected static final HttpResponseHandler<AmazonServiceException> errorResponseHandler = new TestHttpResponseHandler(); /** * The retry condition and back-off strategy implementations that record all * the context data passed into shouldRetry and calculateSleepTime methods. */ protected static ContextDataCollectionRetryCondition retryCondition; protected static ContextDataCollectionBackoffStrategy backoffStrategy; public static void injectMockHttpClient(AmazonHttpClient amazonHttpClient, HttpClient mockHttpClient) { try { Field f = AmazonHttpClient.class.getDeclaredField("httpClient"); f.setAccessible(true); f.set(amazonHttpClient, mockHttpClient); } catch (Exception e) { Assert.fail("Cannot inject the mock HttpClient object. " + e.getMessage()); } } @SuppressWarnings("rawtypes") public static Request<?> getSampleRequestWithRepeatableContent( AmazonWebServiceRequest amazonWebServiceRequest) { DefaultRequest<?> request = new DefaultRequest( amazonWebServiceRequest, "non-existent-service"); request.setEndpoint(URI.create("http://non-existent-service.amazonaws.com")); // StringInputStream#markSupported() returns true try { String content = "Some content that could be read for multiple times."; request.setContent(new StringInputStream(content)); request.addHeader("Content-Length", String.valueOf(content.length())); } catch (UnsupportedEncodingException e) { Assert.fail("Unable to set up the request content"); } return request; } @SuppressWarnings("rawtypes") public static Request<?> getSampleRequestWithNonRepeatableContent( AmazonWebServiceRequest amazonWebServiceRequest) { DefaultRequest<?> request = new DefaultRequest( amazonWebServiceRequest, "non-existent-service"); request.setEndpoint(URI.create("http://non-existent-service.amazonaws.com")); // NonRepeatableInputStream#markSupported() returns false String content = "Some content that could only be read once."; request.setContent(new NonRepeatableInputStream(content)); request.addHeader("Content-Length", String.valueOf(content.length())); return request; } public static class ContextDataCollectionRetryCondition extends ContextDataCollection implements RetryPolicy.RetryCondition { @Override public boolean shouldRetry(AmazonWebServiceRequest originalRequest, AmazonClientException exception, int retriesAttempted) { collect(originalRequest, exception, retriesAttempted); return true; } } public static class ContextDataCollectionBackoffStrategy extends ContextDataCollection implements RetryPolicy.BackoffStrategy { @Override public long delayBeforeNextRetry(AmazonWebServiceRequest originalRequest, AmazonClientException exception, int retriesAttempted) { collect(originalRequest, exception, retriesAttempted); return 0; // immediately retry to speed-up the test } } private static class ContextDataCollection { public List<AmazonWebServiceRequest> failedRequests = new LinkedList<AmazonWebServiceRequest>(); public List<AmazonClientException> exceptions = new LinkedList<AmazonClientException>(); public List<Integer> retriesAttemptedValues = new LinkedList<Integer>(); public void collect(AmazonWebServiceRequest originalRequest, AmazonClientException exception, int retriesAttempted) { failedRequests.add(originalRequest); exceptions.add(exception); retriesAttemptedValues.add(retriesAttempted); } } /** * Verifies the RetryCondition has collected the expected context * information. */ public static void verifyExpectedContextData(ContextDataCollection contextDataCollection, AmazonWebServiceRequest failedRequest, AmazonClientException expectedException, int expectedRetries) { Assert.assertEquals(expectedRetries, contextDataCollection.failedRequests.size()); Assert.assertEquals(expectedRetries, contextDataCollection.exceptions.size()); Assert.assertEquals(expectedRetries, contextDataCollection.retriesAttemptedValues.size()); if (expectedRetries > 0) { // It should keep getting the same original request instance for (AmazonWebServiceRequest seenRequest : contextDataCollection.failedRequests) { Assert.assertTrue(seenRequest == failedRequest); } // Verify the exceptions if (expectedException instanceof AmazonServiceException) { // It should get service exceptions with the expected error and // status code AmazonServiceException ase = (AmazonServiceException) expectedException; for (AmazonClientException seenException : contextDataCollection.exceptions) { Assert.assertTrue(seenException instanceof AmazonServiceException); Assert.assertEquals(ase.getErrorCode(), ((AmazonServiceException) seenException).getErrorCode()); Assert.assertEquals(ase.getStatusCode(), ((AmazonServiceException) seenException).getStatusCode()); } } else { // Client exceptions should have the same expected cause (the // same // throwable instance from the mock HttpClient). Throwable expectedCause = expectedException.getCause(); for (AmazonClientException seenException : contextDataCollection.exceptions) { Assert.assertTrue(expectedCause == seenException.getCause()); } } // It should get "retriesAttempted" values starting from 0 int expectedRetriesAttempted = 0; for (int seenRetriesAttempted : contextDataCollection.retriesAttemptedValues) { Assert.assertEquals(expectedRetriesAttempted++, seenRetriesAttempted); } } } public static class TestAmazonWebServiceRequest extends AmazonWebServiceRequest { } /** * An error response handler implementation that simply - keeps the status * code - sets the error code by the status text (which comes from the * reason phrase in the low-level response) */ public static class TestHttpResponseHandler implements HttpResponseHandler<AmazonServiceException> { @Override public AmazonServiceException handle( com.amazonaws.http.HttpResponse response) throws Exception { AmazonServiceException ase = new AmazonServiceException("Fake service exception."); ase.setStatusCode(response.getStatusCode()); ase.setErrorCode(response.getStatusText()); return ase; } @Override public boolean needsConnectionLeftOpen() { return false; } } /** * A mock HttpClient implementation that does nothing but throws the * specified IOException or RuntimeException upon any call on execute(...) * method. */ public static class ThrowingExceptionHttpClient implements HttpClient { private final Throwable t; /** * @param t An IOException or RuntimeException object. */ public ThrowingExceptionHttpClient(Throwable t) { this.t = t; } @Override public HttpResponse execute(HttpRequest request) throws IOException { if (t instanceof IOException) { throw (IOException) t; } else if (t instanceof RuntimeException) { throw (RuntimeException) t; } else { Assert.fail("The expected throwable should be either an IOException or RuntimeException."); return null; } } @Override public void shutdown() { // No op } } /** * A mock HttpClient implementation that does nothing but directly returns a * BasicHttpResponse object with the specified status code upon any call on * execute(...) method. */ public static class ReturnServiceErrorHttpClient implements HttpClient { private final int statusCode; private final String reasonPhrase; /** * @param statusCode The status code to be included in the error * response. */ public ReturnServiceErrorHttpClient(int statusCode, String reasonPhrase) { this.statusCode = statusCode; this.reasonPhrase = reasonPhrase; } @Override public HttpResponse execute(HttpRequest request) throws IOException { return HttpResponse.builder() .statusCode(statusCode) .statusText(reasonPhrase) .build(); } @Override public void shutdown() { // No op } } }