| 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 | }
 |