/*
* Copyright 2010-2016 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.
*/
using Amazon.Util;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Threading;
namespace Amazon.Runtime.Internal
{
///
/// The default implementation of the retry policy.
///
public partial class DefaultRetryPolicy : RetryPolicy
{
// This parameter sets the cost of making a retry call on a request.The default value is set at 5.
private const int THROTTLE_RETRY_REQUEST_COST = 5;
//maximum capacity in a bucket set to 100.
private const int THROTTLED_RETRIES = 100;
// For every successful request, lesser value capacity would be released. This
// is done to ensure that the bucket has a strategy for filling up if an explosion of bad retry requests
// were to deplete the entire capacity.The default value is set at 1.
private const int THROTTLE_REQUEST_COST = 1;
//The status code returned from a service request when an invalid endpoint is used.
private const int INVALID_ENDPOINT_EXCEPTION_STATUSCODE = 421;
//Holds on to the singleton instance.
private static readonly CapacityManager _capacityManagerInstance = new CapacityManager(THROTTLED_RETRIES, THROTTLE_RETRY_REQUEST_COST, THROTTLE_REQUEST_COST);
private int _maxBackoffInMilliseconds = (int)TimeSpan.FromSeconds(30).TotalMilliseconds;
//This parameter serves as the value to the CapacityManager.Container datastructure.
//Its properties include the available capacity left for making a retry request and the maximum
//capacity size.
private RetryCapacity _retryCapacity;
// Set of HTTP status codes to retry on.
private ICollection _httpStatusCodesToRetryOn = new HashSet
{
HttpStatusCode.InternalServerError,
HttpStatusCode.ServiceUnavailable,
HttpStatusCode.BadGateway,
HttpStatusCode.GatewayTimeout
};
// Set of web exception status codes to retry on.
private ICollection _webExceptionStatusesToRetryOn = new HashSet
{
WebExceptionStatus.ConnectFailure,
#if !PCL // These statuses are not available on WinRT
WebExceptionStatus.ConnectionClosed,
WebExceptionStatus.KeepAliveFailure,
WebExceptionStatus.NameResolutionFailure,
WebExceptionStatus.ReceiveFailure,
WebExceptionStatus.SendFailure,
WebExceptionStatus.Timeout,
#else // WinRT does not expose the status as public enums so we hard code the status numbers.
(WebExceptionStatus)8,
(WebExceptionStatus)12,
(WebExceptionStatus)1,
(WebExceptionStatus)3,
(WebExceptionStatus)4,
(WebExceptionStatus)14,
#endif
};
private static readonly HashSet _netStandardRetryErrorMessages = new HashSet(StringComparer.OrdinalIgnoreCase)
{
"The server returned an invalid or unrecognized response",
"The connection with the server was terminated abnormally",
"An error occurred while sending the request.",
"Failed sending data to the peer"
};
// Set of AWS error codes to retry on.
private ICollection _errorCodesToRetryOn = new HashSet
{
"Throttling",
"ThrottlingException",
"ProvisionedThroughputExceededException",
"RequestTimeout",
"RequestThrottledException"
};
///
/// The maximum value of exponential backoff in milliseconds, which will be used to wait
/// before retrying a request.
///
public int MaxBackoffInMilliseconds
{
get { return _maxBackoffInMilliseconds; }
set { _maxBackoffInMilliseconds = value; }
}
///
/// List of HTTP Status codes codes which are returned as part of the error response.
/// These status codes will be retried.
///
public ICollection HttpStatusCodesToRetryOn
{
get { return _httpStatusCodesToRetryOn; }
}
///
/// List of AWS specific error codes which are returned as part of the error response.
/// These error codes will be retried.
///
public ICollection ErrorCodesToRetryOn
{
get { return _errorCodesToRetryOn; }
}
///
/// List of WebExceptionStatus for a WebException which will be retried.
///
public ICollection WebExceptionStatusesToRetryOn
{
get { return _webExceptionStatusesToRetryOn; }
}
///
/// Constructor for DefaultRetryPolicy.
///
/// The maximum number of retries before throwing
/// back a exception. This does not count the initial request.
public DefaultRetryPolicy(int maxRetries)
{
this.MaxRetries = maxRetries;
}
///
/// Constructor for DefaultRetryPolicy.
///
/// The Client config object. This is used to
/// retrieve the maximum number of retries before throwing
/// back a exception(This does not count the initial request) and
/// the service URL for the request.
public DefaultRetryPolicy(IClientConfig config)
{
this.MaxRetries = config.MaxErrorRetry;
if (config.ThrottleRetries)
{
string serviceURL = config.DetermineServiceURL();
_retryCapacity = _capacityManagerInstance.GetRetryCapacity(serviceURL);
}
}
///
/// Returns true if the request is in a state where it can be retried, else false.
///
/// Request context containing the state of the request.
/// Returns true if the request is in a state where it can be retried, else false.
public override bool CanRetry(IExecutionContext executionContext)
{
return executionContext.RequestContext.Request.IsRequestStreamRewindable();
}
///
/// Return true if the request should be retried.
///
/// Request context containing the state of the request.
/// The exception thrown by the previous request.
/// Return true if the request should be retried.
public override bool RetryForException(IExecutionContext executionContext, Exception exception)
{
return RetryForExceptionSync(exception, executionContext);
}
///
/// Virtual method that gets called when a retry request is initiated. If retry throttling is
/// enabled, the value returned is true if the required capacity is retured, false otherwise.
/// If retry throttling is disabled, true is returned.
///
/// The execution context which contains both the
/// requests and response context.
public override bool OnRetry(IExecutionContext executionContext)
{
return OnRetry(executionContext, false);
}
///
/// Virtual method that gets called when a retry request is initiated. If retry throttling is
/// enabled, the value returned is true if the required capacity is retured, false otherwise.
/// If retry throttling is disabled, true is returned.
///
/// The execution context which contains both the
/// requests and response context.
/// true to bypass any attempt to acquire capacity on a retry
public override bool OnRetry(IExecutionContext executionContext, bool bypassAcquireCapacity)
{
if (!bypassAcquireCapacity && executionContext.RequestContext.ClientConfig.ThrottleRetries && _retryCapacity != null)
{
return _capacityManagerInstance.TryAcquireCapacity(_retryCapacity);
}
else
{
return true;
}
}
///
/// Virtual method that gets called on a success Response. If its a retry success response, the entire
/// retry acquired capacity is released(default is 5). If its just a success response a lesser value capacity
/// is released(default is 1).
///
/// Request context containing the state of the request.
public override void NotifySuccess(IExecutionContext executionContext)
{
if(executionContext.RequestContext.ClientConfig.ThrottleRetries && _retryCapacity!=null)
{
_capacityManagerInstance.TryReleaseCapacity(executionContext.RequestContext.Retries>0 ? true:false, _retryCapacity);
}
}
///
/// Perform the processor-bound portion of the RetryForException logic.
/// This is shared by the sync, async, and APM versions of the RetryForException method.
///
/// The exception thrown by the previous request.
/// Return true if the request should be retried.
private bool RetryForExceptionSync(Exception exception)
{
return RetryForExceptionSync(exception, null);
}
///
/// Perform the processor-bound portion of the RetryForException logic.
/// This is shared by the sync, async, and APM versions of the RetryForException method.
///
/// The exception thrown by the previous request.
/// Request context containing the state of the request.
/// Return true if the request should be retried.
private bool RetryForExceptionSync(Exception exception, IExecutionContext executionContext)
{
// An IOException was thrown by the underlying http client.
if (exception is IOException)
{
#if !PCL && !NETSTANDARD // ThreadAbortException is not PCL and NetStandard
// Don't retry IOExceptions that are caused by a ThreadAbortException
if (IsInnerException(exception))
return false;
#endif
// Retry all other IOExceptions
return true;
}
#if NETSTANDARD
// Version 7.35 libcurl which is the default version installed with Ubuntu 14.04
// has issues under high concurrency causing response streams being disposed
// during unmarshalling. To work around this issue will add the ObjectDisposedException
// to the list of exceptions to retry.
if (IsInnerException(exception))
return true;
if (exception is System.Net.Http.HttpRequestException)
{
if (ContainErrorMessage(exception))
return true;
}
if (exception is OperationCanceledException && !executionContext.RequestContext.CancellationToken.IsCancellationRequested)
{
//OperationCanceledException thrown by HttpClient not the CancellationToken supplied by the user.
//This exception can wrap at least IOExceptions, ObjectDisposedExceptions and should be retried.
//It will only get in here if the OperationCanceledException thrown from HttpClient did not contain
//an inner exception which is handled in the HttpRequestMessageFactory.
return true;
}
#endif
// A AmazonServiceException was thrown by ErrorHandler
var serviceException = exception as AmazonServiceException;
if (serviceException != null)
{
// 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.
//
// 502 and 504 are returned by proxies. These can also be returned for
// S3 accelerate requests which are served by CloudFront.
if (this.HttpStatusCodesToRetryOn.Contains(serviceException.StatusCode))
{
return true;
}
// Throttling is reported as a 400 or 503 error from 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 ((serviceException.StatusCode == HttpStatusCode.BadRequest ||
serviceException.StatusCode == HttpStatusCode.ServiceUnavailable))
{
string errorCode = serviceException.ErrorCode;
if (this.ErrorCodesToRetryOn.Contains(errorCode))
{
return true;
}
}
//Check for Invalid Endpoint Exception indicating that the Endpoint Discovery
//endpoint used was invalid for the request. One retry attempt is allowed for this
//type of exception.
if (serviceException.StatusCode == (HttpStatusCode)INVALID_ENDPOINT_EXCEPTION_STATUSCODE)
{
if(executionContext.RequestContext.EndpointDiscoveryRetries < 1)
{
executionContext.RequestContext.EndpointDiscoveryRetries++;
return true;
}
return false;
}
WebException webException;
if (IsInnerException(exception, out webException))
{
if (this.WebExceptionStatusesToRetryOn.Contains(webException.Status))
{
return true;
}
}
}
return false;
}
///
/// Checks if the retry limit is reached.
///
/// Request context containing the state of the request.
/// Return false if the request can be retried, based on number of retries.
public override bool RetryLimitReached(IExecutionContext executionContext)
{
return executionContext.RequestContext.Retries >= this.MaxRetries;
}
///
/// Waits before retrying a request. The default policy implements a exponential backoff.
///
/// Request context containing the state of the request.
public override void WaitBeforeRetry(IExecutionContext executionContext)
{
DefaultRetryPolicy.WaitBeforeRetry(executionContext.RequestContext.Retries, this.MaxBackoffInMilliseconds);
}
public static void WaitBeforeRetry(int retries, int maxBackoffInMilliseconds)
{
AWSSDKUtils.Sleep(CalculateRetryDelay(retries, maxBackoffInMilliseconds));
}
private static int CalculateRetryDelay(int retries, int maxBackoffInMilliseconds)
{
int delay;
if (retries < 12 ) delay = Convert.ToInt32(Math.Pow(4, retries) * 100.0);
else delay = Int32.MaxValue;
if (retries > 0 && (delay > maxBackoffInMilliseconds || delay <= 0))
delay = maxBackoffInMilliseconds;
return delay;
}
protected static bool ContainErrorMessage(Exception exception)
{
if (exception == null)
return false;
if (_netStandardRetryErrorMessages.Contains(exception.Message))
return true;
return ContainErrorMessage(exception.InnerException);
}
protected static bool IsInnerException(Exception exception)
where T : Exception
{
T innerException;
return IsInnerException(exception, out innerException);
}
protected static bool IsInnerException(Exception exception, out T inner)
where T : Exception
{
inner = null;
var innerExceptionType = typeof(T);
var currentException = exception;
while (currentException.InnerException != null)
{
inner = currentException.InnerException as T;
if (inner != null)
{
return true;
}
currentException = currentException.InnerException;
}
return false;
}
}
}