/*
* Copyright 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 AWSSDK.Runtime.Internal.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 legacy retry policy.
///
public partial class DefaultRetryPolicy : RetryPolicy
{
//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(throttleRetryCount: 100, throttleRetryCost: 5, throttleCost: 1);
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"
};
///
/// The maximum value of exponential backoff in milliseconds, which will be used to wait
/// before retrying a request. The default is 30000 milliseconds.
///
public int MaxBackoffInMilliseconds { get; set; } = 30000;
///
/// 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)
{
RetryCapacity = _capacityManagerInstance.GetRetryCapacity(GetRetryCapacityKey(config));
}
}
///
/// 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, 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)
{
return OnRetry(executionContext, bypassAcquireCapacity, 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
/// true if the error that will be retried is a throtting error
public override bool OnRetry(IExecutionContext executionContext, bool bypassAcquireCapacity, bool isThrottlingError)
{
if (!bypassAcquireCapacity && executionContext.RequestContext.ClientConfig.ThrottleRetries && RetryCapacity != null)
{
return _capacityManagerInstance.TryAcquireCapacity(RetryCapacity, executionContext.RequestContext.LastCapacityType);
}
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.ReleaseCapacity(executionContext.RequestContext.LastCapacityType, 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)
{
// AmazonServiceException is thrown by ErrorHandler if it is this type of exception.
var serviceException = exception as AmazonServiceException;
// 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. Only the error code should be used to determine if an
// error is a throttling error.
if (IsThrottlingError(exception))
{
return true;
}
// Check for transient errors, 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 (IsTransientError(executionContext, exception) || IsServiceTimeoutError(exception))
{
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;
}
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);
}
///
/// Waits for an amount of time using an exponential backoff algorithm.
///
/// The request retry index. The first request is expected to be 0 while
/// the first retry will be 1.
/// The max number of milliseconds to wait
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;
}
[Obsolete("This method is no longer used within DefaultRetryPolicy")]
protected static bool ContainErrorMessage(Exception exception)
{
return ContainErrorMessage(exception, _netStandardRetryErrorMessages);
}
[Obsolete("This method has been moved to AWSSDK.Runtime.Internal.Util.ExceptionUtils")]
protected static bool IsInnerException(Exception exception)
where T : Exception
{
return ExceptionUtils.IsInnerException(exception);
}
[Obsolete("This method has been moved to AWSSDK.Runtime.Internal.Util.ExceptionUtils")]
protected static bool IsInnerException(Exception exception, out T inner)
where T : Exception
{
return ExceptionUtils.IsInnerException(exception, out inner);
}
}
}