| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277 | <?phpnamespace Aws;use Aws\Exception\AwsException;use Aws\Retry\RetryHelperTrait;use GuzzleHttp\Exception\RequestException;use Psr\Http\Message\RequestInterface;use GuzzleHttp\Promise\PromiseInterface;use GuzzleHttp\Promise;/** * Middleware that retries failures. V1 implemention that supports 'legacy' mode. * * @internal */class RetryMiddleware{    use RetryHelperTrait;    private static $retryStatusCodes = [        500 => true,        502 => true,        503 => true,        504 => true    ];    private static $retryCodes = [        // Throttling error        'RequestLimitExceeded'                   => true,        'Throttling'                             => true,        'ThrottlingException'                    => true,        'ThrottledException'                     => true,        'ProvisionedThroughputExceededException' => true,        'RequestThrottled'                       => true,        'BandwidthLimitExceeded'                 => true,        'RequestThrottledException'              => true,        'TooManyRequestsException'               => true,        'IDPCommunicationError'                  => true,        'EC2ThrottledException'                  => true,    ];    private $decider;    private $delay;    private $nextHandler;    private $collectStats;    public function __construct(        callable $decider,        callable $delay,        callable $nextHandler,        $collectStats = false    ) {        $this->decider = $decider;        $this->delay = $delay;        $this->nextHandler = $nextHandler;        $this->collectStats = (bool) $collectStats;    }    /**     * Creates a default AWS retry decider function.     *     * The optional $extraConfig parameter is an associative array     * that specifies additional retry conditions on top of the ones specified     * by default by the Aws\RetryMiddleware class, with the following keys:     *     * - errorCodes: (string[]) An indexed array of AWS exception codes to retry.     *   Optional.     * - statusCodes: (int[]) An indexed array of HTTP status codes to retry.     *   Optional.     * - curlErrors: (int[]) An indexed array of Curl error codes to retry. Note     *   these should be valid Curl constants. Optional.     *     * @param int $maxRetries     * @param array $extraConfig     * @return callable     */    public static function createDefaultDecider(        $maxRetries = 3,        $extraConfig = []    ) {        $retryCurlErrors = [];        if (extension_loaded('curl')) {            $retryCurlErrors[CURLE_RECV_ERROR] = true;        }        return function (            $retries,            CommandInterface $command,            RequestInterface $request,            ResultInterface $result = null,            $error = null        ) use ($maxRetries, $retryCurlErrors, $extraConfig) {            // Allow command-level options to override this value            $maxRetries = null !== $command['@retries'] ?                $command['@retries']                : $maxRetries;            $isRetryable = self::isRetryable(                $result,                $error,                $retryCurlErrors,                $extraConfig            );            if ($retries >= $maxRetries) {                if (!empty($error)                    && $error instanceof AwsException                    && $isRetryable                ) {                    $error->setMaxRetriesExceeded();                }                return false;            }            return $isRetryable;        };    }    private static function isRetryable(        $result,        $error,        $retryCurlErrors,        $extraConfig = []    ) {        $errorCodes = self::$retryCodes;        if (!empty($extraConfig['error_codes'])            && is_array($extraConfig['error_codes'])        ) {            foreach($extraConfig['error_codes'] as $code) {                $errorCodes[$code] = true;            }        }        $statusCodes = self::$retryStatusCodes;        if (!empty($extraConfig['status_codes'])            && is_array($extraConfig['status_codes'])        ) {            foreach($extraConfig['status_codes'] as $code) {                $statusCodes[$code] = true;            }        }        if (!empty($extraConfig['curl_errors'])            && is_array($extraConfig['curl_errors'])        ) {            foreach($extraConfig['curl_errors'] as $code) {                $retryCurlErrors[$code] = true;            }        }        if (!$error) {            if (!isset($result['@metadata']['statusCode'])) {                return false;            }            return isset($statusCodes[$result['@metadata']['statusCode']]);        }        if (!($error instanceof AwsException)) {            return false;        }        if ($error->isConnectionError()) {            return true;        }        if (isset($errorCodes[$error->getAwsErrorCode()])) {            return true;        }        if (isset($statusCodes[$error->getStatusCode()])) {            return true;        }        if (count($retryCurlErrors)            && ($previous = $error->getPrevious())            && $previous instanceof RequestException        ) {            if (method_exists($previous, 'getHandlerContext')) {                $context = $previous->getHandlerContext();                return !empty($context['errno'])                    && isset($retryCurlErrors[$context['errno']]);            }            $message = $previous->getMessage();            foreach (array_keys($retryCurlErrors) as $curlError) {                if (strpos($message, 'cURL error ' . $curlError . ':') === 0) {                    return true;                }            }        }        return false;    }    /**     * Delay function that calculates an exponential delay.     *     * Exponential backoff with jitter, 100ms base, 20 sec ceiling     *     * @param $retries - The number of retries that have already been attempted     *     * @return int     *     * @link https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/     */    public static function exponentialDelay($retries)    {        return mt_rand(0, (int) min(20000, (int) pow(2, $retries) * 100));    }    /**     * @param CommandInterface $command     * @param RequestInterface $request     *     * @return PromiseInterface     */    public function __invoke(        CommandInterface $command,        RequestInterface $request = null    ) {        $retries = 0;        $requestStats = [];        $monitoringEvents = [];        $handler = $this->nextHandler;        $decider = $this->decider;        $delay = $this->delay;        $request = $this->addRetryHeader($request, 0, 0);        $g = function ($value) use (            $handler,            $decider,            $delay,            $command,            $request,            &$retries,            &$requestStats,            &$monitoringEvents,            &$g        ) {            $this->updateHttpStats($value, $requestStats);            if ($value instanceof MonitoringEventsInterface) {                $reversedEvents = array_reverse($monitoringEvents);                $monitoringEvents = array_merge($monitoringEvents, $value->getMonitoringEvents());                foreach ($reversedEvents as $event) {                    $value->prependMonitoringEvent($event);                }            }            if ($value instanceof \Exception || $value instanceof \Throwable) {                if (!$decider($retries, $command, $request, null, $value)) {                    return Promise\Create::rejectionFor(                        $this->bindStatsToReturn($value, $requestStats)                    );                }            } elseif ($value instanceof ResultInterface                && !$decider($retries, $command, $request, $value, null)            ) {                return $this->bindStatsToReturn($value, $requestStats);            }            // Delay fn is called with 0, 1, ... so increment after the call.            $delayBy = $delay($retries++);            $command['@http']['delay'] = $delayBy;            if ($this->collectStats) {                $this->updateStats($retries, $delayBy, $requestStats);            }            // Update retry header with retry count and delayBy            $request = $this->addRetryHeader($request, $retries, $delayBy);            return $handler($command, $request)->then($g, $g);        };        return $handler($command, $request)->then($g, $g);    }}
 |