Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
namespace Aws;
3
 
4
use Aws\Exception\AwsException;
5
use Aws\Retry\RetryHelperTrait;
6
use GuzzleHttp\Exception\RequestException;
7
use Psr\Http\Message\RequestInterface;
8
use GuzzleHttp\Promise\PromiseInterface;
9
use GuzzleHttp\Promise;
10
 
11
/**
12
 * Middleware that retries failures. V1 implemention that supports 'legacy' mode.
13
 *
14
 * @internal
15
 */
16
class RetryMiddleware
17
{
18
    use RetryHelperTrait;
19
 
20
    private static $retryStatusCodes = [
21
        500 => true,
22
        502 => true,
23
        503 => true,
24
        504 => true
25
    ];
26
 
27
    private static $retryCodes = [
28
        // Throttling error
29
        'RequestLimitExceeded'                   => true,
30
        'Throttling'                             => true,
31
        'ThrottlingException'                    => true,
32
        'ThrottledException'                     => true,
33
        'ProvisionedThroughputExceededException' => true,
34
        'RequestThrottled'                       => true,
35
        'BandwidthLimitExceeded'                 => true,
36
        'RequestThrottledException'              => true,
37
        'TooManyRequestsException'               => true,
38
        'IDPCommunicationError'                  => true,
39
        'EC2ThrottledException'                  => true,
40
    ];
41
 
42
    private $decider;
43
    private $delay;
44
    private $nextHandler;
45
    private $collectStats;
46
 
47
    public function __construct(
48
        callable $decider,
49
        callable $delay,
50
        callable $nextHandler,
51
        $collectStats = false
52
    ) {
53
        $this->decider = $decider;
54
        $this->delay = $delay;
55
        $this->nextHandler = $nextHandler;
56
        $this->collectStats = (bool) $collectStats;
57
    }
58
 
59
    /**
60
     * Creates a default AWS retry decider function.
61
     *
62
     * The optional $extraConfig parameter is an associative array
63
     * that specifies additional retry conditions on top of the ones specified
64
     * by default by the Aws\RetryMiddleware class, with the following keys:
65
     *
66
     * - errorCodes: (string[]) An indexed array of AWS exception codes to retry.
67
     *   Optional.
68
     * - statusCodes: (int[]) An indexed array of HTTP status codes to retry.
69
     *   Optional.
70
     * - curlErrors: (int[]) An indexed array of Curl error codes to retry. Note
71
     *   these should be valid Curl constants. Optional.
72
     *
73
     * @param int $maxRetries
74
     * @param array $extraConfig
75
     * @return callable
76
     */
77
    public static function createDefaultDecider(
78
        $maxRetries = 3,
79
        $extraConfig = []
80
    ) {
81
        $retryCurlErrors = [];
82
        if (extension_loaded('curl')) {
83
            $retryCurlErrors[CURLE_RECV_ERROR] = true;
84
        }
85
 
86
        return function (
87
            $retries,
88
            CommandInterface $command,
89
            RequestInterface $request,
90
            ResultInterface $result = null,
91
            $error = null
92
        ) use ($maxRetries, $retryCurlErrors, $extraConfig) {
93
            // Allow command-level options to override this value
94
            $maxRetries = null !== $command['@retries'] ?
95
                $command['@retries']
96
                : $maxRetries;
97
 
98
            $isRetryable = self::isRetryable(
99
                $result,
100
                $error,
101
                $retryCurlErrors,
102
                $extraConfig
103
            );
104
 
105
            if ($retries >= $maxRetries) {
106
                if (!empty($error)
107
                    && $error instanceof AwsException
108
                    && $isRetryable
109
                ) {
110
                    $error->setMaxRetriesExceeded();
111
                }
112
                return false;
113
            }
114
 
115
            return $isRetryable;
116
        };
117
    }
118
 
119
    private static function isRetryable(
120
        $result,
121
        $error,
122
        $retryCurlErrors,
123
        $extraConfig = []
124
    ) {
125
        $errorCodes = self::$retryCodes;
126
        if (!empty($extraConfig['error_codes'])
127
            && is_array($extraConfig['error_codes'])
128
        ) {
129
            foreach($extraConfig['error_codes'] as $code) {
130
                $errorCodes[$code] = true;
131
            }
132
        }
133
 
134
        $statusCodes = self::$retryStatusCodes;
135
        if (!empty($extraConfig['status_codes'])
136
            && is_array($extraConfig['status_codes'])
137
        ) {
138
            foreach($extraConfig['status_codes'] as $code) {
139
                $statusCodes[$code] = true;
140
            }
141
        }
142
 
143
        if (!empty($extraConfig['curl_errors'])
144
            && is_array($extraConfig['curl_errors'])
145
        ) {
146
            foreach($extraConfig['curl_errors'] as $code) {
147
                $retryCurlErrors[$code] = true;
148
            }
149
        }
150
 
151
        if (!$error) {
152
            if (!isset($result['@metadata']['statusCode'])) {
153
                return false;
154
            }
155
            return isset($statusCodes[$result['@metadata']['statusCode']]);
156
        }
157
 
158
        if (!($error instanceof AwsException)) {
159
            return false;
160
        }
161
 
162
        if ($error->isConnectionError()) {
163
            return true;
164
        }
165
 
166
        if (isset($errorCodes[$error->getAwsErrorCode()])) {
167
            return true;
168
        }
169
 
170
        if (isset($statusCodes[$error->getStatusCode()])) {
171
            return true;
172
        }
173
 
174
        if (count($retryCurlErrors)
175
            && ($previous = $error->getPrevious())
176
            && $previous instanceof RequestException
177
        ) {
178
            if (method_exists($previous, 'getHandlerContext')) {
179
                $context = $previous->getHandlerContext();
180
                return !empty($context['errno'])
181
                    && isset($retryCurlErrors[$context['errno']]);
182
            }
183
 
184
            $message = $previous->getMessage();
185
            foreach (array_keys($retryCurlErrors) as $curlError) {
186
                if (strpos($message, 'cURL error ' . $curlError . ':') === 0) {
187
                    return true;
188
                }
189
            }
190
        }
191
 
192
        return false;
193
    }
194
 
195
    /**
196
     * Delay function that calculates an exponential delay.
197
     *
198
     * Exponential backoff with jitter, 100ms base, 20 sec ceiling
199
     *
200
     * @param $retries - The number of retries that have already been attempted
201
     *
202
     * @return int
203
     *
204
     * @link https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
205
     */
206
    public static function exponentialDelay($retries)
207
    {
208
        return mt_rand(0, (int) min(20000, (int) pow(2, $retries) * 100));
209
    }
210
 
211
    /**
212
     * @param CommandInterface $command
213
     * @param RequestInterface $request
214
     *
215
     * @return PromiseInterface
216
     */
217
    public function __invoke(
218
        CommandInterface $command,
219
        RequestInterface $request = null
220
    ) {
221
        $retries = 0;
222
        $requestStats = [];
223
        $monitoringEvents = [];
224
        $handler = $this->nextHandler;
225
        $decider = $this->decider;
226
        $delay = $this->delay;
227
 
228
        $request = $this->addRetryHeader($request, 0, 0);
229
 
230
        $g = function ($value) use (
231
            $handler,
232
            $decider,
233
            $delay,
234
            $command,
235
            $request,
236
            &$retries,
237
            &$requestStats,
238
            &$monitoringEvents,
239
            &$g
240
        ) {
241
            $this->updateHttpStats($value, $requestStats);
242
 
243
            if ($value instanceof MonitoringEventsInterface) {
244
                $reversedEvents = array_reverse($monitoringEvents);
245
                $monitoringEvents = array_merge($monitoringEvents, $value->getMonitoringEvents());
246
                foreach ($reversedEvents as $event) {
247
                    $value->prependMonitoringEvent($event);
248
                }
249
            }
250
            if ($value instanceof \Exception || $value instanceof \Throwable) {
251
                if (!$decider($retries, $command, $request, null, $value)) {
252
                    return Promise\Create::rejectionFor(
253
                        $this->bindStatsToReturn($value, $requestStats)
254
                    );
255
                }
256
            } elseif ($value instanceof ResultInterface
257
                && !$decider($retries, $command, $request, $value, null)
258
            ) {
259
                return $this->bindStatsToReturn($value, $requestStats);
260
            }
261
 
262
            // Delay fn is called with 0, 1, ... so increment after the call.
263
            $delayBy = $delay($retries++);
264
            $command['@http']['delay'] = $delayBy;
265
            if ($this->collectStats) {
266
                $this->updateStats($retries, $delayBy, $requestStats);
267
            }
268
 
269
            // Update retry header with retry count and delayBy
270
            $request = $this->addRetryHeader($request, $retries, $delayBy);
271
 
272
            return $handler($command, $request)->then($g, $g);
273
        };
274
 
275
        return $handler($command, $request)->then($g, $g);
276
    }
277
}