Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | 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 GuzzleHttp\Promise\Coroutine;
1441 ariadna 6
use GuzzleHttp\Promise\PromiseInterface;
1 efrain 7
use GuzzleHttp\Promise\PromisorInterface;
8
use GuzzleHttp\Promise\RejectedPromise;
9
 
10
/**
11
 * "Waiters" are associated with an AWS resource (e.g., EC2 instance), and poll
12
 * that resource and until it is in a particular state.
13
 
14
 * The Waiter object produces a promise that is either a.) resolved once the
15
 * waiting conditions are met, or b.) rejected if the waiting conditions cannot
16
 * be met or has exceeded the number of allowed attempts at meeting the
17
 * conditions. You can use waiters in a blocking or non-blocking way, depending
18
 * on whether you call wait() on the promise.
19
 
20
 * The configuration for the waiter must include information about the operation
21
 * and the conditions for wait completion.
22
 */
23
class Waiter implements PromisorInterface
24
{
25
    /** @var AwsClientInterface Client used to execute each attempt. */
26
    private $client;
27
 
28
    /** @var string Name of the waiter. */
29
    private $name;
30
 
31
    /** @var array Params to use with each attempt operation. */
32
    private $args;
33
 
34
    /** @var array Waiter configuration. */
35
    private $config;
36
 
37
    /** @var array Default configuration options. */
38
    private static $defaults = ['initDelay' => 0, 'before' => null];
39
 
40
    /** @var array Required configuration options. */
41
    private static $required = [
42
        'acceptors',
43
        'delay',
44
        'maxAttempts',
45
        'operation',
46
    ];
47
 
48
    /**
49
     * The array of configuration options include:
50
     *
51
     * - acceptors: (array) Array of acceptor options
52
     * - delay: (int) Number of seconds to delay between attempts
53
     * - maxAttempts: (int) Maximum number of attempts before failing
54
     * - operation: (string) Name of the API operation to use for polling
55
     * - before: (callable) Invoked before attempts. Accepts command and tries.
56
     *
57
     * @param AwsClientInterface $client Client used to execute commands.
58
     * @param string             $name   Waiter name.
59
     * @param array              $args   Command arguments.
60
     * @param array              $config Waiter config that overrides defaults.
61
     *
62
     * @throws \InvalidArgumentException if the configuration is incomplete.
63
     */
64
    public function __construct(
65
        AwsClientInterface $client,
66
        $name,
67
        array $args = [],
68
        array $config = []
69
    ) {
70
        $this->client = $client;
71
        $this->name = $name;
72
        $this->args = $args;
73
 
74
        // Prepare and validate config.
75
        $this->config = $config + self::$defaults;
76
        foreach (self::$required as $key) {
77
            if (!isset($this->config[$key])) {
78
                throw new \InvalidArgumentException(
79
                    'The provided waiter configuration was incomplete.'
80
                );
81
            }
82
        }
83
        if ($this->config['before'] && !is_callable($this->config['before'])) {
84
            throw new \InvalidArgumentException(
85
                'The provided "before" callback is not callable.'
86
            );
87
        }
1441 ariadna 88
        MetricsBuilder::appendMetricsCaptureMiddleware(
89
            $this->client->getHandlerList(),
90
            MetricsBuilder::WAITER
91
        );
1 efrain 92
    }
93
 
94
    /**
95
     * @return Coroutine
96
     */
1441 ariadna 97
    public function promise(): PromiseInterface
1 efrain 98
    {
99
        return Coroutine::of(function () {
100
            $name = $this->config['operation'];
101
            for ($state = 'retry', $attempt = 1; $state === 'retry'; $attempt++) {
102
                // Execute the operation.
103
                $args = $this->getArgsForAttempt($attempt);
104
                $command = $this->client->getCommand($name, $args);
105
                try {
106
                    if ($this->config['before']) {
107
                        $this->config['before']($command, $attempt);
108
                    }
109
                    $result = (yield $this->client->executeAsync($command));
110
                } catch (AwsException $e) {
111
                    $result = $e;
112
                }
113
 
114
                // Determine the waiter's state and what to do next.
115
                $state = $this->determineState($result);
116
                if ($state === 'success') {
117
                    yield $command;
118
                } elseif ($state === 'failed') {
119
                    $msg = "The {$this->name} waiter entered a failure state.";
120
                    if ($result instanceof \Exception) {
121
                        $msg .= ' Reason: ' . $result->getMessage();
122
                    }
123
                    yield new RejectedPromise(new \RuntimeException($msg));
124
                } elseif ($state === 'retry'
125
                    && $attempt >= $this->config['maxAttempts']
126
                ) {
127
                    $state = 'failed';
128
                    yield new RejectedPromise(new \RuntimeException(
129
                        "The {$this->name} waiter failed after attempt #{$attempt}."
130
                    ));
131
                }
132
            }
133
        });
134
    }
135
 
136
    /**
137
     * Gets the operation arguments for the attempt, including the delay.
138
     *
139
     * @param $attempt Number of the current attempt.
140
     *
141
     * @return mixed integer
142
     */
143
    private function getArgsForAttempt($attempt)
144
    {
145
        $args = $this->args;
146
 
147
        // Determine the delay.
148
        $delay = ($attempt === 1)
149
            ? $this->config['initDelay']
150
            : $this->config['delay'];
151
        if (is_callable($delay)) {
152
            $delay = $delay($attempt);
153
        }
154
 
155
        // Set the delay. (Note: handlers except delay in milliseconds.)
156
        if (!isset($args['@http'])) {
157
            $args['@http'] = [];
158
        }
159
        $args['@http']['delay'] = $delay * 1000;
160
 
161
        return $args;
162
    }
163
 
164
    /**
165
     * Determines the state of the waiter attempt, based on the result of
166
     * polling the resource. A waiter can have the state of "success", "failed",
167
     * or "retry".
168
     *
169
     * @param mixed $result
170
     *
171
     * @return string Will be "success", "failed", or "retry"
172
     */
173
    private function determineState($result)
174
    {
175
        foreach ($this->config['acceptors'] as $acceptor) {
176
            $matcher = 'matches' . ucfirst($acceptor['matcher']);
177
            if ($this->{$matcher}($result, $acceptor)) {
178
                return $acceptor['state'];
179
            }
180
        }
181
 
182
        return $result instanceof \Exception ? 'failed' : 'retry';
183
    }
184
 
185
    /**
186
     * @param Result $result   Result or exception.
187
     * @param array  $acceptor Acceptor configuration being checked.
188
     *
189
     * @return bool
190
     */
191
    private function matchesPath($result, array $acceptor)
192
    {
1441 ariadna 193
        return $result instanceof ResultInterface
194
            && $acceptor['expected'] === $result->search($acceptor['argument']);
1 efrain 195
    }
196
 
197
    /**
198
     * @param Result $result   Result or exception.
199
     * @param array  $acceptor Acceptor configuration being checked.
200
     *
201
     * @return bool
202
     */
203
    private function matchesPathAll($result, array $acceptor)
204
    {
205
        if (!($result instanceof ResultInterface)) {
206
            return false;
207
        }
208
 
209
        $actuals = $result->search($acceptor['argument']) ?: [];
1441 ariadna 210
        // If is empty or not evaluates to an array it must return false.
211
        if (empty($actuals) || !is_array($actuals)) {
212
            return false;
213
        }
214
 
1 efrain 215
        foreach ($actuals as $actual) {
216
            if ($actual != $acceptor['expected']) {
217
                return false;
218
            }
219
        }
220
 
221
        return true;
222
    }
223
 
224
    /**
225
     * @param Result $result   Result or exception.
226
     * @param array  $acceptor Acceptor configuration being checked.
227
     *
228
     * @return bool
229
     */
230
    private function matchesPathAny($result, array $acceptor)
231
    {
232
        if (!($result instanceof ResultInterface)) {
233
            return false;
234
        }
235
 
236
        $actuals = $result->search($acceptor['argument']) ?: [];
1441 ariadna 237
        // If is empty or not evaluates to an array it must return false.
238
        if (empty($actuals) || !is_array($actuals)) {
239
            return false;
240
        }
241
 
1 efrain 242
        return in_array($acceptor['expected'], $actuals);
243
    }
244
 
245
    /**
246
     * @param Result $result   Result or exception.
247
     * @param array  $acceptor Acceptor configuration being checked.
248
     *
249
     * @return bool
250
     */
251
    private function matchesStatus($result, array $acceptor)
252
    {
253
        if ($result instanceof ResultInterface) {
254
            return $acceptor['expected'] == $result['@metadata']['statusCode'];
255
        }
256
 
257
        if ($result instanceof AwsException && $response = $result->getResponse()) {
258
            return $acceptor['expected'] == $response->getStatusCode();
259
        }
260
 
261
        return false;
262
    }
263
 
264
    /**
265
     * @param Result $result   Result or exception.
266
     * @param array  $acceptor Acceptor configuration being checked.
267
     *
268
     * @return bool
269
     */
270
    private function matchesError($result, array $acceptor)
271
    {
1441 ariadna 272
        // If expected is true then the $result should be an instance of
273
        // AwsException, otherwise it should not.
274
        if (isset($acceptor['expected']) && is_bool($acceptor['expected'])) {
275
            return $acceptor['expected'] === ($result instanceof AwsException);
276
        }
277
 
1 efrain 278
        if ($result instanceof AwsException) {
279
            return $result->isConnectionError()
280
                || $result->getAwsErrorCode() == $acceptor['expected'];
281
        }
282
 
283
        return false;
284
    }
285
}