/*
* 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 standard retry policy.
///
public partial class StandardRetryPolicy : RetryPolicy
{
private static Random _randomJitter = new Random();
//The status code returned from a service request when an invalid endpoint is used.
private const int INVALID_ENDPOINT_EXCEPTION_STATUSCODE = 421;
protected static CapacityManager CapacityManagerInstance { get; set; } = new CapacityManager(throttleRetryCount: 100, throttleRetryCost: 5, throttleCost: 1, timeoutRetryCost: 10);
///
/// The maximum value of exponential backoff in milliseconds, which will be used to wait
/// before retrying a request. The default is 20000 milliseconds.
///
public int MaxBackoffInMilliseconds { get; set; } = 20000;
///
/// Constructor for StandardRetryPolicy.
///
/// The maximum number of retries before throwing
/// back a exception. This does not count the initial request.
public StandardRetryPolicy(int maxRetries)
{
this.MaxRetries = maxRetries;
}
///
/// Constructor for StandardRetryPolicy.
///
/// 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 StandardRetryPolicy(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);
}
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)
{
var requestContext = executionContext.RequestContext;
CapacityManagerInstance.ReleaseCapacity(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.
protected 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.
protected 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 with
/// jitter algorithm.
///
/// Request context containing the state of the request.
public override void WaitBeforeRetry(IExecutionContext executionContext)
{
StandardRetryPolicy.WaitBeforeRetry(executionContext.RequestContext.Retries, this.MaxBackoffInMilliseconds);
}
///
/// Waits for an amount of time using an exponential backoff with jitter 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));
}
protected static int CalculateRetryDelay(int retries, int maxBackoffInMilliseconds)
{
double jitter;
lock (_randomJitter) {
jitter = _randomJitter.NextDouble();
}
return Convert.ToInt32(Math.Min(jitter * Math.Pow(2, retries - 1) * 1000.0, maxBackoffInMilliseconds));
}
}
}