pt.lunacloud.http.AmazonHttpClient.java Source code

Java tutorial

Introduction

Here is the source code for pt.lunacloud.http.AmazonHttpClient.java

Source

/*
 * Copyright 2010-2013 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.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. 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 pt.lunacloud.http;

import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;

import javax.net.ssl.SSLContext;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;

import pt.lunacloud.LunacloudClientException;
import pt.lunacloud.LunacloudServiceException;
import pt.lunacloud.AmazonWebServiceRequest;
import pt.lunacloud.AmazonWebServiceResponse;
import pt.lunacloud.ClientConfiguration;
import pt.lunacloud.Request;
import pt.lunacloud.ResponseMetadata;
import pt.lunacloud.LunacloudServiceException.ErrorType;
import pt.lunacloud.handlers.RequestHandler;
import pt.lunacloud.internal.CRC32MismatchException;
import pt.lunacloud.internal.CustomBackoffStrategy;
import pt.lunacloud.util.AWSRequestMetrics;
import pt.lunacloud.util.CountingInputStream;
import pt.lunacloud.util.ResponseMetadataCache;
import pt.lunacloud.util.TimingInfo;
import pt.lunacloud.util.AWSRequestMetrics.Field;

public class AmazonHttpClient {

    /**
     * Logger providing detailed information on requests/responses. Users can
     * enable this logger to get access to AWS request IDs for responses,
     * individual requests and parameters sent to AWS, etc.
     */
    private static final Log requestLog = LogFactory.getLog("com.amazonaws.request");

    /**
     * Logger for more detailed debugging information, that might not be as
     * useful for end users (ex: HTTP client configuration, etc).
     */
    static final Log log = LogFactory.getLog(AmazonHttpClient.class);

    /** Internal client for sending HTTP requests */
    private final HttpClient httpClient;

    /** Maximum exponential back-off time before retrying a request */
    private static final int MAX_BACKOFF_IN_MILLISECONDS = 20 * 1000;

    /** Client configuration options, such as proxy settings, max retries, etc. */
    private final ClientConfiguration config;

    /** Cache of metadata for recently executed requests for diagnostic purposes */
    private final ResponseMetadataCache responseMetadataCache = new ResponseMetadataCache(50);

    private static final Random random = new Random();

    private static HttpRequestFactory httpRequestFactory = new HttpRequestFactory();
    private static HttpClientFactory httpClientFactory = new HttpClientFactory();

    /** Internal system property to enable advanced timing info collection. */
    public static final String PROFILING_SYSTEM_PROPERTY = "com.amazonaws.sdk.enableRuntimeProfiling";

    static {
        // Customers have reported XML parsing issues with the following
        // JVM versions, which don't occur with more recent versions, so
        // if we detect any of these, give customers a heads up.
        List<String> problematicJvmVersions = Arrays.asList(new String[] { "1.6.0_06", "1.6.0_13", "1.6.0_17", });
        String jvmVersion = System.getProperty("java.version");
        if (problematicJvmVersions.contains(jvmVersion)) {
            log.warn("Detected a possible problem with the current JVM version (" + jvmVersion + ").  "
                    + "If you experience XML parsing problems using the SDK, try upgrading to a more recent JVM update.");
        }
    }

    /**
     * Constructs a new AWS client using the specified client configuration
     * options (ex: max retry attempts, proxy settings, etc).
     *
     * @param clientConfiguration
     *            Configuration options specifying how this client will
     *            communicate with AWS (ex: proxy settings, retry count, etc.).
     */
    public AmazonHttpClient(ClientConfiguration clientConfiguration) {
        this.config = clientConfiguration;
        this.httpClient = httpClientFactory.createHttpClient(config);
    }

    /**
     * Returns additional response metadata for an executed request. Response
     * metadata isn't considered part of the standard results returned by an
     * operation, so it's accessed instead through this diagnostic interface.
     * Response metadata is typically used for troubleshooting issues with AWS
     * support staff when services aren't acting as expected.
     *
     * @param request
     *            A previously executed AmazonWebServiceRequest object, whose
     *            response metadata is desired.
     *
     * @return The response metadata for the specified request, otherwise null
     *         if there is no response metadata available for the request.
     */
    public ResponseMetadata getResponseMetadataForRequest(AmazonWebServiceRequest request) {
        return responseMetadataCache.get(request);
    }

    /**
     * Disables the default strict hostname verification in this client and
     * instead uses a browser compatible hostname verification strategy (i.e.
     * cert hostname wildcards are evaulated more liberally).
     */
    public void disableStrictHostnameVerification() {
        try {
            SchemeRegistry schemeRegistry = httpClient.getConnectionManager().getSchemeRegistry();

            SSLSocketFactory sf = new SSLSocketFactory(SSLContext.getDefault(),
                    SSLSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
            Scheme https = new Scheme("https", 443, sf);

            schemeRegistry.register(https);
        } catch (NoSuchAlgorithmException e) {
            throw new LunacloudClientException(
                    "Unable to access default SSL context to disable strict hostname verification");
        }
    }

    /**
     * Executes the request and returns the result.
     *
     * @param request
     *            The AmazonWebServices request to send to the remote server
     * @param responseHandler
     *            A response handler to accept a successful response from the
     *            remote server
     * @param errorResponseHandler
     *            A response handler to accept an unsuccessful response from the
     *            remote server
     * @param executionContext
     *            Additional information about the context of this web service
     *            call
     */
    public <T> T execute(Request<?> request, HttpResponseHandler<AmazonWebServiceResponse<T>> responseHandler,
            HttpResponseHandler<LunacloudServiceException> errorResponseHandler, ExecutionContext executionContext)
            throws LunacloudClientException, LunacloudServiceException {
        long startTime = System.currentTimeMillis();

        if (executionContext == null)
            throw new LunacloudClientException("Internal SDK Error: No execution context parameter specified.");
        List<RequestHandler> requestHandlers = executionContext.getRequestHandlers();
        if (requestHandlers == null)
            requestHandlers = new ArrayList<RequestHandler>();

        // Apply any additional service specific request handlers that need to be run
        for (RequestHandler requestHandler : requestHandlers) {
            requestHandler.beforeRequest(request);
        }

        try {
            T t = executeHelper(request, responseHandler, errorResponseHandler, executionContext);
            TimingInfo timingInfo = executionContext.getAwsRequestMetrics().getTimingInfo();
            timingInfo.setEndTime(System.currentTimeMillis());

            for (RequestHandler handler : requestHandlers) {
                try {
                    handler.afterResponse(request, t, timingInfo);
                } catch (ClassCastException cce) {
                }
            }
            return t;
        } catch (LunacloudClientException e) {
            for (RequestHandler handler : requestHandlers) {
                handler.afterError(request, e);
            }
            throw e;
        }
    }

    /**
     * Internal method to execute the HTTP method given.
     *
     * @see AmazonHttpClient#execute(Request, HttpResponseHandler, HttpResponseHandler)
     * @see AmazonHttpClient#execute(Request, HttpResponseHandler, HttpResponseHandler, ExecutionContext)
     */
    private <T extends Object> T executeHelper(Request<?> request,
            HttpResponseHandler<AmazonWebServiceResponse<T>> responseHandler,
            HttpResponseHandler<LunacloudServiceException> errorResponseHandler, ExecutionContext executionContext)
            throws LunacloudClientException, LunacloudServiceException {

        /*
         * Depending on which response handler we end up choosing to handle the
         * HTTP response, it might require us to leave the underlying HTTP
         * connection open, depending on whether or not it reads the complete
         * HTTP response stream from the HTTP connection, or if delays reading
         * any of the content until after a response is returned to the caller.
         */
        boolean leaveHttpConnectionOpen = false;

        AWSRequestMetrics awsRequestMetrics = executionContext.getAwsRequestMetrics();
        /* add the service endpoint to the logs. You can infer service name from service endpoint */
        awsRequestMetrics.addProperty(Field.ServiceName.name(), request.getServiceName());
        awsRequestMetrics.addProperty(Field.ServiceEndpoint.name(), request.getEndpoint());

        // Apply whatever request options we know how to handle, such as user-agent.
        applyRequestData(request);

        int retryCount = 0;
        URI redirectedURI = null;
        HttpEntity entity = null;
        LunacloudServiceException exception = null;

        // Make a copy of the original request params and headers so that we can
        // permute it in this loop and start over with the original every time.
        Map<String, String> originalParameters = new HashMap<String, String>();
        originalParameters.putAll(request.getParameters());
        Map<String, String> originalHeaders = new HashMap<String, String>();
        originalHeaders.putAll(request.getHeaders());

        while (true) {
            awsRequestMetrics.setCounter(Field.AttemptCount.name(), retryCount + 1);
            if (retryCount > 0) {
                request.setParameters(originalParameters);
                request.setHeaders(originalHeaders);
            }

            HttpRequestBase httpRequest = null;
            org.apache.http.HttpResponse response = null;

            try {
                // Sign the request if a signer was provided
                if (executionContext.getSigner() != null && executionContext.getCredentials() != null) {
                    awsRequestMetrics.startEvent(Field.RequestSigningTime.name());
                    executionContext.getSigner().sign(request, executionContext.getCredentials());
                    awsRequestMetrics.endEvent(Field.RequestSigningTime.name());
                }

                if (requestLog.isDebugEnabled()) {
                    requestLog.debug("Sending Request: " + request.toString());
                }

                httpRequest = httpRequestFactory.createHttpRequest(request, config, entity, executionContext);

                if (httpRequest instanceof HttpEntityEnclosingRequest) {
                    entity = ((HttpEntityEnclosingRequest) httpRequest).getEntity();
                }

                if (redirectedURI != null) {
                    httpRequest.setURI(redirectedURI);
                }

                if (retryCount > 0) {
                    awsRequestMetrics.startEvent(Field.RetryPauseTime.name());
                    pauseExponentially(retryCount, exception, executionContext.getCustomBackoffStrategy());
                    awsRequestMetrics.endEvent(Field.RetryPauseTime.name());
                }

                if (entity != null) {
                    InputStream content = entity.getContent();
                    if (retryCount > 0) {
                        if (content.markSupported()) {
                            content.reset();
                            content.mark(-1);
                        }
                    } else {
                        if (content.markSupported()) {
                            content.mark(-1);
                        }
                    }
                }

                exception = null;

                awsRequestMetrics.startEvent(Field.HttpRequestTime.name());
                response = httpClient.execute(httpRequest);
                awsRequestMetrics.endEvent(Field.HttpRequestTime.name());

                if (isRequestSuccessful(response)) {

                    awsRequestMetrics.addProperty(Field.StatusCode.name(),
                            response.getStatusLine().getStatusCode());

                    /*
                     * If we get back any 2xx status code, then we know we should
                     * treat the service call as successful.
                     */
                    leaveHttpConnectionOpen = responseHandler.needsConnectionLeftOpen();
                    return handleResponse(request, responseHandler, httpRequest, response, executionContext);
                } else if (isTemporaryRedirect(response)) {
                    /*
                     * S3 sends 307 Temporary Redirects if you try to delete an
                     * EU bucket from the US endpoint. If we get a 307, we'll
                     * point the HTTP method to the redirected location, and let
                     * the next retry deliver the request to the right location.
                     */
                    Header[] locationHeaders = response.getHeaders("location");
                    String redirectedLocation = locationHeaders[0].getValue();
                    log.debug("Redirecting to: " + redirectedLocation);
                    redirectedURI = URI.create(redirectedLocation);
                    httpRequest.setURI(redirectedURI);
                    awsRequestMetrics.addProperty(Field.StatusCode.name(),
                            response.getStatusLine().getStatusCode());
                    awsRequestMetrics.addProperty(Field.RedirectLocation.name(), redirectedLocation);
                    awsRequestMetrics.addProperty(Field.AWSRequestID.name(), null);

                } else {
                    leaveHttpConnectionOpen = errorResponseHandler.needsConnectionLeftOpen();
                    exception = handleErrorResponse(request, errorResponseHandler, httpRequest, response);
                    awsRequestMetrics.addProperty(Field.AWSRequestID.name(), exception.getRequestId());
                    awsRequestMetrics.addProperty(Field.AWSErrorCode.name(), exception.getErrorCode());
                    awsRequestMetrics.addProperty(Field.StatusCode.name(), exception.getStatusCode());

                    if (!shouldRetry(httpRequest, exception, retryCount)) {
                        throw exception;
                    }
                    resetRequestAfterError(request, exception);
                }
            } catch (IOException ioe) {
                log.info("Unable to execute HTTP request: " + ioe.getMessage(), ioe);
                awsRequestMetrics.addProperty(Field.Exception.name(), ioe.toString());
                awsRequestMetrics.addProperty(Field.AWSRequestID.name(), null);

                if (!shouldRetry(httpRequest, ioe, retryCount)) {
                    throw new LunacloudClientException("Unable to execute HTTP request: " + ioe.getMessage(), ioe);
                }
                resetRequestAfterError(request, ioe);
            } finally {
                retryCount++;

                /*
                 * Some response handlers need to manually manage the HTTP
                 * connection and will take care of releasing the connection on
                 * their own, but if this response handler doesn't need the
                 * connection left open, we go ahead and release the it to free
                 * up resources.
                 */
                if (!leaveHttpConnectionOpen) {
                    try {
                        response.getEntity().getContent().close();
                    } catch (Throwable t) {
                    }
                }
            }
        } /* end while (true) */
    }

    /**
     * Resets the specified request, so that it can be sent again, after
     * receiving the specified error. If a problem is encountered with resetting
     * the request, then an AmazonClientException is thrown with the original
     * error as the cause (not an error about being unable to reset the stream).
     *
     * @param request
     *            The request being executed that failed and needs to be reset.
     * @param cause
     *            The original error that caused the request to fail.
     *
     * @throws LunacloudClientException
     *             If the request can't be reset.
     */
    private void resetRequestAfterError(Request<?> request, Exception cause) throws LunacloudClientException {
        if (request.getContent() != null && request.getContent().markSupported()) {
            try {
                request.getContent().reset();
            } catch (IOException e) {
                // This exception comes from being unable to reset the input stream,
                // so throw the original, more meaningful exception
                throw new LunacloudClientException(
                        "Encountered an exception and couldn't reset the stream to retry", cause);
            }
        }
    }

    /**
     * Applies any additional options set in the request.
     */
    private void applyRequestData(Request<?> request) {
        if (config.getUserAgent() != null) {
            request.addHeader("User-Agent", config.getUserAgent());
        }

        if (request.getOriginalRequest() != null && request.getOriginalRequest().getRequestClientOptions() != null
                && request.getOriginalRequest().getRequestClientOptions().getClientMarker() != null) {
            request.addHeader("User-Agent", createUserAgentString(config.getUserAgent(),
                    request.getOriginalRequest().getRequestClientOptions().getClientMarker()));
        }
    }

    /**
     * Appends the given user-agent string to the existing one and returns it.
     */
    private static String createUserAgentString(String existingUserAgentString, String userAgent) {
        if (existingUserAgentString.contains(userAgent)) {
            return existingUserAgentString;
        } else {
            return existingUserAgentString.trim() + " " + userAgent.trim();
        }
    }

    /**
     * Shuts down this HTTP client object, releasing any resources that might be
     * held open. This is an optional method, and callers are not expected to
     * call it, but can if they want to explicitly release any open resources.
     * Once a client has been shutdown, it cannot be used to make more requests.
     */
    public void shutdown() {
        IdleConnectionReaper.removeConnectionManager(httpClient.getConnectionManager());
        httpClient.getConnectionManager().shutdown();
    }

    /**
     * Returns true if a failed request should be retried.
     *
     * @param method
     *            The current HTTP method being executed.
     * @param exception
     *            The exception from the failed request.
     * @param retries
     *            The number of times the current request has been attempted.
     *
     * @return True if the failed request should be retried.
     */
    private boolean shouldRetry(HttpRequestBase method, Exception exception, int retries) {
        if (retries >= config.getMaxErrorRetry())
            return false;

        if (method instanceof HttpEntityEnclosingRequest) {
            HttpEntity entity = ((HttpEntityEnclosingRequest) method).getEntity();
            if (entity != null && !entity.isRepeatable()) {
                if (log.isDebugEnabled()) {
                    log.debug("Entity not repeatable");
                }
                return false;
            }
        }

        if (exception instanceof IOException) {
            if (log.isDebugEnabled()) {
                log.debug("Retrying on " + exception.getClass().getName() + ": " + exception.getMessage());
            }
            return true;
        }

        if (exception instanceof LunacloudServiceException) {
            LunacloudServiceException ase = (LunacloudServiceException) exception;

            /*
             * For 500 internal server errors and 503 service
             * unavailable errors, we want to retry, but we need to use
             * an exponential back-off strategy so that we don't overload
             * a server with a flood of retries. If we've surpassed our
             * retry limit we handle the error response as a non-retryable
             * error and go ahead and throw it back to the user as an exception.
             */
            if (ase.getStatusCode() == HttpStatus.SC_INTERNAL_SERVER_ERROR
                    || ase.getStatusCode() == HttpStatus.SC_SERVICE_UNAVAILABLE) {
                return true;
            }

            /*
             * Throttling is reported as a 400 error from newer services. To try
             * and smooth out an occasional throttling error, we'll pause and
             * retry, hoping that the pause is long enough for the request to
             * get through the next time.
             */
            if (isThrottlingException(ase))
                return true;
        }

        return false;
    }

    private boolean isTemporaryRedirect(org.apache.http.HttpResponse response) {
        int status = response.getStatusLine().getStatusCode();
        return status == HttpStatus.SC_TEMPORARY_REDIRECT && response.getHeaders("Location") != null
                && response.getHeaders("Location").length > 0;
    }

    private boolean isRequestSuccessful(org.apache.http.HttpResponse response) {
        int status = response.getStatusLine().getStatusCode();
        return status / 100 == HttpStatus.SC_OK / 100;
    }

    /**
     * Handles a successful response from a service call by unmarshalling the
     * results using the specified response handler.
     *
     * @param <T>
     *            The type of object expected in the response.
     *
     * @param request
     *            The original request that generated the response being
     *            handled.
     * @param responseHandler
     *            The response unmarshaller used to interpret the contents of
     *            the response.
     * @param method
     *            The HTTP method that was invoked, and contains the contents of
     *            the response.
     * @param executionContext
     *            Extra state information about the request currently being
     *            executed.
     * @return The contents of the response, unmarshalled using the specified
     *         response handler.
     *
     * @throws IOException
     *             If any problems were encountered reading the response
     *             contents from the HTTP method object.
     */
    private <T> T handleResponse(Request<?> request,
            HttpResponseHandler<AmazonWebServiceResponse<T>> responseHandler, HttpRequestBase method,
            org.apache.http.HttpResponse apacheHttpResponse, ExecutionContext executionContext) throws IOException {

        HttpResponse httpResponse = createResponse(method, request, apacheHttpResponse);
        if (responseHandler.needsConnectionLeftOpen() && method instanceof HttpEntityEnclosingRequest) {
            HttpEntityEnclosingRequest httpEntityEnclosingRequest = (HttpEntityEnclosingRequest) method;
            httpResponse.setContent(new HttpMethodReleaseInputStream(httpEntityEnclosingRequest));
        }

        try {
            CountingInputStream countingInputStream = null;
            if (System.getProperty(PROFILING_SYSTEM_PROPERTY) != null) {
                countingInputStream = new CountingInputStream(httpResponse.getContent());
                httpResponse.setContent(countingInputStream);
            }

            AWSRequestMetrics awsRequestMetrics = executionContext.getAwsRequestMetrics();
            awsRequestMetrics.startEvent(Field.ResponseProcessingTime.name());
            AmazonWebServiceResponse<? extends T> awsResponse = responseHandler.handle(httpResponse);
            awsRequestMetrics.endEvent(Field.ResponseProcessingTime.name());
            if (countingInputStream != null) {
                awsRequestMetrics.setCounter(Field.BytesProcessed.name(), countingInputStream.getByteCount());
            }

            if (awsResponse == null)
                throw new RuntimeException("Unable to unmarshall response metadata");

            responseMetadataCache.add(request.getOriginalRequest(), awsResponse.getResponseMetadata());

            if (requestLog.isDebugEnabled()) {
                requestLog
                        .debug("Received successful response: " + apacheHttpResponse.getStatusLine().getStatusCode()
                                + ", AWS Request ID: " + awsResponse.getRequestId());
            }
            awsRequestMetrics.addProperty(Field.AWSRequestID.name(), awsResponse.getRequestId());

            return awsResponse.getResult();
        } catch (CRC32MismatchException e) {
            throw e;
        } catch (Exception e) {
            String errorMessage = "Unable to unmarshall response (" + e.getMessage() + ")";
            throw new LunacloudClientException(errorMessage, e);
        }
    }

    /**
     * Responsible for handling an error response, including unmarshalling the
     * error response into the most specific exception type possible, and
     * throwing the exception.
     *
     * @param request
     *            The request that generated the error response being handled.
     * @param errorResponseHandler
     *            The response handler responsible for unmarshalling the error
     *            response.
     * @param method
     *            The HTTP method containing the actual response content.
     *
     * @throws IOException
     *             If any problems are encountering reading the error response.
     */
    private LunacloudServiceException handleErrorResponse(Request<?> request,
            HttpResponseHandler<LunacloudServiceException> errorResponseHandler, HttpRequestBase method,
            org.apache.http.HttpResponse apacheHttpResponse) throws IOException {

        int status = apacheHttpResponse.getStatusLine().getStatusCode();
        HttpResponse response = createResponse(method, request, apacheHttpResponse);
        if (errorResponseHandler.needsConnectionLeftOpen() && method instanceof HttpEntityEnclosingRequestBase) {
            HttpEntityEnclosingRequestBase entityEnclosingRequest = (HttpEntityEnclosingRequestBase) method;
            response.setContent(new HttpMethodReleaseInputStream(entityEnclosingRequest));
        }

        LunacloudServiceException exception = null;
        try {
            exception = errorResponseHandler.handle(response);
            requestLog.debug("Received error response: " + exception.toString());
        } catch (Exception e) {
            // If the errorResponseHandler doesn't work, then check for error
            // responses that don't have any content
            if (status == 413) {
                exception = new LunacloudServiceException("Request entity too large");
                exception.setServiceName(request.getServiceName());
                exception.setStatusCode(413);
                exception.setErrorType(ErrorType.Client);
                exception.setErrorCode("Request entity too large");
            } else if (status == 503 && "Service Unavailable"
                    .equalsIgnoreCase(apacheHttpResponse.getStatusLine().getReasonPhrase())) {
                exception = new LunacloudServiceException("Service unavailable");
                exception.setServiceName(request.getServiceName());
                exception.setStatusCode(503);
                exception.setErrorType(ErrorType.Service);
                exception.setErrorCode("Service unavailable");
            } else {
                String errorMessage = "Unable to unmarshall error response (" + e.getMessage() + ")";
                throw new LunacloudClientException(errorMessage, e);
            }
        }

        exception.setStatusCode(status);
        exception.setServiceName(request.getServiceName());
        exception.fillInStackTrace();
        return exception;
    }

    /**
     * Creates and initializes an HttpResponse object suitable to be passed to
     * an HTTP response handler object.
     *
     * @param method
     *            The HTTP method that was invoked to get the response.
     * @param request
     *            The HTTP request associated with the response.
     *
     * @return The new, initialized HttpResponse object ready to be passed to an
     *         HTTP response handler object.
     *
     * @throws IOException
     *             If there were any problems getting any response information
     *             from the HttpClient method object.
     */
    private HttpResponse createResponse(HttpRequestBase method, Request<?> request,
            org.apache.http.HttpResponse apacheHttpResponse) throws IOException {
        HttpResponse httpResponse = new HttpResponse(request, method);

        if (apacheHttpResponse.getEntity() != null) {
            httpResponse.setContent(apacheHttpResponse.getEntity().getContent());
        }

        httpResponse.setStatusCode(apacheHttpResponse.getStatusLine().getStatusCode());
        httpResponse.setStatusText(apacheHttpResponse.getStatusLine().getReasonPhrase());
        for (Header header : apacheHttpResponse.getAllHeaders()) {
            httpResponse.addHeader(header.getName(), header.getValue());
        }

        return httpResponse;
    }

    /**
     * Exponential sleep on failed request to avoid flooding a service with
     * retries.
     *
     * @param retries
     *            Current retry count.
     * @param previousException
     *            Exception information for the previous attempt, if any.
     */
    private void pauseExponentially(int retries, LunacloudServiceException previousException,
            CustomBackoffStrategy backoffStrategy) {
        long delay = 0;
        if (backoffStrategy != null) {
            delay = backoffStrategy.getBackoffPeriod(retries);
        } else {
            long scaleFactor = 300;
            if (isThrottlingException(previousException)) {
                scaleFactor = 500 + random.nextInt(100);
            }
            delay = (long) (Math.pow(2, retries) * scaleFactor);
        }

        delay = Math.min(delay, MAX_BACKOFF_IN_MILLISECONDS);
        if (log.isDebugEnabled()) {
            log.debug("Retriable error detected, " + "will retry in " + delay + "ms, attempt number: " + retries);
        }

        try {
            Thread.sleep(delay);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new LunacloudClientException(e.getMessage(), e);
        }
    }

    /**
     * Returns true if the specified exception is a throttling error.
     *
     * @param ase
     *            The exception to test.
     *
     * @return True if the exception resulted from a throttling error message
     *         from a service, otherwise false.
     */
    private boolean isThrottlingException(LunacloudServiceException ase) {
        if (ase == null)
            return false;
        return "Throttling".equals(ase.getErrorCode()) || "ThrottlingException".equals(ase.getErrorCode())
                || "ProvisionedThroughputExceededException".equals(ase.getErrorCode());
    }

    @Override
    protected void finalize() throws Throwable {
        this.shutdown();
        super.finalize();
    }

}