PluginProbe
Media Cloud Sync / 1.2.12
Media Cloud Sync v1.2.12
1.4.0 1.3.12 1.3.11 1.3.10 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.2.0 1.2.10 1.2.11 1.2.12 1.2.13 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 1.3.0 All 34 releases
media-cloud-sync / includes / sdk / s3 / Aws / RetryMiddleware.php

RetryMiddleware.php in Media Cloud Sync 1.2.12, at includes/sdk/s3/Aws/RetryMiddleware.php

192 lines 7.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Dudlewebs\WPMCS\s3\Aws;
4
5 use Dudlewebs\WPMCS\s3\Aws\Exception\AwsException;
6 use Dudlewebs\WPMCS\s3\Aws\Retry\RetryHelperTrait;
7 use Dudlewebs\WPMCS\s3\GuzzleHttp\Exception\RequestException;
8 use Dudlewebs\WPMCS\s3\Psr\Http\Message\RequestInterface;
9 use Dudlewebs\WPMCS\s3\GuzzleHttp\Promise\PromiseInterface;
10 use Dudlewebs\WPMCS\s3\GuzzleHttp\Promise;
11 /**
12 * Middleware that retries failures. V1 implemention that supports 'legacy' mode.
13 *
14 * @internal
15 */
16 class RetryMiddleware
17 {
18 use RetryHelperTrait;
19 private static $retryStatusCodes = [500 => \true, 502 => \true, 503 => \true, 504 => \true];
20 private static $retryCodes = [
21 // Throttling error
22 'RequestLimitExceeded' => \true,
23 'Throttling' => \true,
24 'ThrottlingException' => \true,
25 'ThrottledException' => \true,
26 'ProvisionedThroughputExceededException' => \true,
27 'RequestThrottled' => \true,
28 'BandwidthLimitExceeded' => \true,
29 'RequestThrottledException' => \true,
30 'TooManyRequestsException' => \true,
31 'IDPCommunicationError' => \true,
32 'EC2ThrottledException' => \true,
33 ];
34 private $decider;
35 private $delay;
36 private $nextHandler;
37 private $collectStats;
38 public function __construct(callable $decider, callable $delay, callable $nextHandler, $collectStats = \false)
39 {
40 $this->decider = $decider;
41 $this->delay = $delay;
42 $this->nextHandler = $nextHandler;
43 $this->collectStats = (bool) $collectStats;
44 }
45 /**
46 * Creates a default AWS retry decider function.
47 *
48 * The optional $extraConfig parameter is an associative array
49 * that specifies additional retry conditions on top of the ones specified
50 * by default by the Aws\RetryMiddleware class, with the following keys:
51 *
52 * - errorCodes: (string[]) An indexed array of AWS exception codes to retry.
53 * Optional.
54 * - statusCodes: (int[]) An indexed array of HTTP status codes to retry.
55 * Optional.
56 * - curlErrors: (int[]) An indexed array of Curl error codes to retry. Note
57 * these should be valid Curl constants. Optional.
58 *
59 * @param int $maxRetries
60 * @param array $extraConfig
61 * @return callable
62 */
63 public static function createDefaultDecider($maxRetries = 3, $extraConfig = [])
64 {
65 $retryCurlErrors = [];
66 if (\extension_loaded('curl')) {
67 $retryCurlErrors[\CURLE_RECV_ERROR] = \true;
68 }
69 return function ($retries, CommandInterface $command, RequestInterface $request, ResultInterface $result = null, $error = null) use($maxRetries, $retryCurlErrors, $extraConfig) {
70 // Allow command-level options to override this value
71 $maxRetries = null !== $command['@retries'] ? $command['@retries'] : $maxRetries;
72 $isRetryable = self::isRetryable($result, $error, $retryCurlErrors, $extraConfig);
73 if ($retries >= $maxRetries) {
74 if (!empty($error) && $error instanceof AwsException && $isRetryable) {
75 $error->setMaxRetriesExceeded();
76 }
77 return \false;
78 }
79 return $isRetryable;
80 };
81 }
82 private static function isRetryable($result, $error, $retryCurlErrors, $extraConfig = [])
83 {
84 $errorCodes = self::$retryCodes;
85 if (!empty($extraConfig['error_codes']) && \is_array($extraConfig['error_codes'])) {
86 foreach ($extraConfig['error_codes'] as $code) {
87 $errorCodes[$code] = \true;
88 }
89 }
90 $statusCodes = self::$retryStatusCodes;
91 if (!empty($extraConfig['status_codes']) && \is_array($extraConfig['status_codes'])) {
92 foreach ($extraConfig['status_codes'] as $code) {
93 $statusCodes[$code] = \true;
94 }
95 }
96 if (!empty($extraConfig['curl_errors']) && \is_array($extraConfig['curl_errors'])) {
97 foreach ($extraConfig['curl_errors'] as $code) {
98 $retryCurlErrors[$code] = \true;
99 }
100 }
101 if (!$error) {
102 if (!isset($result['@metadata']['statusCode'])) {
103 return \false;
104 }
105 return isset($statusCodes[$result['@metadata']['statusCode']]);
106 }
107 if (!$error instanceof AwsException) {
108 return \false;
109 }
110 if ($error->isConnectionError()) {
111 return \true;
112 }
113 if (isset($errorCodes[$error->getAwsErrorCode()])) {
114 return \true;
115 }
116 if (isset($statusCodes[$error->getStatusCode()])) {
117 return \true;
118 }
119 if (\count($retryCurlErrors) && ($previous = $error->getPrevious()) && $previous instanceof RequestException) {
120 if (\method_exists($previous, 'getHandlerContext')) {
121 $context = $previous->getHandlerContext();
122 return !empty($context['errno']) && isset($retryCurlErrors[$context['errno']]);
123 }
124 $message = $previous->getMessage();
125 foreach (\array_keys($retryCurlErrors) as $curlError) {
126 if (\strpos($message, 'cURL error ' . $curlError . ':') === 0) {
127 return \true;
128 }
129 }
130 }
131 return \false;
132 }
133 /**
134 * Delay function that calculates an exponential delay.
135 *
136 * Exponential backoff with jitter, 100ms base, 20 sec ceiling
137 *
138 * @param $retries - The number of retries that have already been attempted
139 *
140 * @return int
141 *
142 * @link https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
143 */
144 public static function exponentialDelay($retries)
145 {
146 return \mt_rand(0, (int) \min(20000, (int) \pow(2, $retries) * 100));
147 }
148 /**
149 * @param CommandInterface $command
150 * @param RequestInterface $request
151 *
152 * @return PromiseInterface
153 */
154 public function __invoke(CommandInterface $command, RequestInterface $request = null)
155 {
156 $retries = 0;
157 $requestStats = [];
158 $monitoringEvents = [];
159 $handler = $this->nextHandler;
160 $decider = $this->decider;
161 $delay = $this->delay;
162 $request = $this->addRetryHeader($request, 0, 0);
163 $g = function ($value) use($handler, $decider, $delay, $command, $request, &$retries, &$requestStats, &$monitoringEvents, &$g) {
164 $this->updateHttpStats($value, $requestStats);
165 if ($value instanceof MonitoringEventsInterface) {
166 $reversedEvents = \array_reverse($monitoringEvents);
167 $monitoringEvents = \array_merge($monitoringEvents, $value->getMonitoringEvents());
168 foreach ($reversedEvents as $event) {
169 $value->prependMonitoringEvent($event);
170 }
171 }
172 if ($value instanceof \Exception || $value instanceof \Throwable) {
173 if (!$decider($retries, $command, $request, null, $value)) {
174 return Promise\Create::rejectionFor($this->bindStatsToReturn($value, $requestStats));
175 }
176 } elseif ($value instanceof ResultInterface && !$decider($retries, $command, $request, $value, null)) {
177 return $this->bindStatsToReturn($value, $requestStats);
178 }
179 // Delay fn is called with 0, 1, ... so increment after the call.
180 $delayBy = $delay($retries++);
181 $command['@http']['delay'] = $delayBy;
182 if ($this->collectStats) {
183 $this->updateStats($retries, $delayBy, $requestStats);
184 }
185 // Update retry header with retry count and delayBy
186 $request = $this->addRetryHeader($request, $retries, $delayBy);
187 return $handler($command, $request)->then($g, $g);
188 };
189 return $handler($command, $request)->then($g, $g);
190 }
191 }
192