Waiter.php 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. <?php
  2. declare(strict_types=1);
  3. namespace AsyncAws\Core;
  4. use AsyncAws\Core\Exception\Http\HttpException;
  5. use AsyncAws\Core\Exception\Http\NetworkException;
  6. use AsyncAws\Core\Exception\LogicException;
  7. /**
  8. * The waiter promise is always returned from every API call to a waiter.
  9. */
  10. class Waiter
  11. {
  12. public const STATE_SUCCESS = 'success';
  13. public const STATE_FAILURE = 'failure';
  14. public const STATE_PENDING = 'pending';
  15. protected const WAIT_TIMEOUT = 30.0;
  16. protected const WAIT_DELAY = 5.0;
  17. /**
  18. * @var AbstractApi|null
  19. */
  20. protected $awsClient;
  21. /**
  22. * Input used to build the API request that generate this Waiter.
  23. *
  24. * @var object|null
  25. */
  26. protected $input;
  27. /**
  28. * @var Response
  29. */
  30. private $response;
  31. /**
  32. * Whether or not a new response should be fetched.
  33. *
  34. * @var bool
  35. */
  36. private $needRefresh = false;
  37. /**
  38. * @var string|null
  39. */
  40. private $finalState;
  41. /**
  42. * @var bool
  43. */
  44. private $resolved = false;
  45. public function __construct(Response $response, AbstractApi $awsClient, $request)
  46. {
  47. $this->response = $response;
  48. $this->awsClient = $awsClient;
  49. $this->input = $request;
  50. }
  51. public function __destruct()
  52. {
  53. if (!$this->resolved) {
  54. $this->resolve();
  55. }
  56. }
  57. final public function isSuccess(): bool
  58. {
  59. return self::STATE_SUCCESS === $this->getState();
  60. }
  61. final public function isFailure(): bool
  62. {
  63. return self::STATE_FAILURE === $this->getState();
  64. }
  65. final public function isPending(): bool
  66. {
  67. return self::STATE_PENDING === $this->getState();
  68. }
  69. final public function getState(): string
  70. {
  71. if (null !== $this->finalState) {
  72. return $this->finalState;
  73. }
  74. if ($this->needRefresh) {
  75. $this->stealResponse($this->refreshState());
  76. }
  77. try {
  78. $this->response->resolve();
  79. $exception = null;
  80. } catch (HttpException $exception) {
  81. // use $exception later
  82. } finally {
  83. $this->resolved = true;
  84. $this->needRefresh = true;
  85. }
  86. $state = $this->extractState($this->response, $exception);
  87. switch ($state) {
  88. case self::STATE_SUCCESS:
  89. case self::STATE_FAILURE:
  90. $this->finalState = $state;
  91. break;
  92. case self::STATE_PENDING:
  93. break;
  94. default:
  95. throw new LogicException(sprintf('Unexpected state "%s" from Waiter "%s".', $state, __CLASS__));
  96. }
  97. return $state;
  98. }
  99. /**
  100. * Make sure the actual request is executed.
  101. *
  102. * @param float|null $timeout Duration in seconds before aborting. When null wait until the end of execution.
  103. *
  104. * @return bool false on timeout. True if the response has returned with as status code.
  105. *
  106. * @throws NetworkException
  107. */
  108. final public function resolve(?float $timeout = null): bool
  109. {
  110. try {
  111. return $this->response->resolve($timeout);
  112. } catch (HttpException $exception) {
  113. return true;
  114. } finally {
  115. $this->resolved = true;
  116. }
  117. }
  118. /**
  119. * Returns info on the current request.
  120. *
  121. * @return array{
  122. * resolved: bool,
  123. * body_downloaded: bool,
  124. * response: \Symfony\Contracts\HttpClient\ResponseInterface,
  125. * status: int,
  126. * }
  127. */
  128. final public function info(): array
  129. {
  130. return $this->response->info();
  131. }
  132. final public function cancel(): void
  133. {
  134. $this->response->cancel();
  135. $this->needRefresh = true;
  136. $this->resolved = true;
  137. }
  138. /**
  139. * Wait until the state is success.
  140. * Stopped when the state become Failure or the defined timeout is reached.
  141. *
  142. * @param float $timeout Duration in seconds before aborting
  143. * @param float $delay Duration in seconds between each check
  144. *
  145. * @return bool true if a final state was reached
  146. */
  147. final public function wait(?float $timeout = null, ?float $delay = null): bool
  148. {
  149. if (null !== $this->finalState) {
  150. return true;
  151. }
  152. $timeout = $timeout ?? static::WAIT_TIMEOUT;
  153. $delay = $delay ?? static::WAIT_DELAY;
  154. $start = microtime(true);
  155. while (true) {
  156. if ($this->needRefresh) {
  157. $this->stealResponse($this->refreshState());
  158. }
  159. // If request times out
  160. if (!$this->resolve($timeout - (microtime(true) - $start))) {
  161. break;
  162. }
  163. $this->getState();
  164. // If we reached a final state
  165. if ($this->finalState) {
  166. return true;
  167. }
  168. // If the timeout will expire during our sleep, then exit early.
  169. if ($delay > $timeout - (microtime(true) - $start)) {
  170. break;
  171. }
  172. usleep((int) ceil($delay * 1000000));
  173. }
  174. return false;
  175. }
  176. protected function extractState(Response $response, ?HttpException $exception): string
  177. {
  178. return self::STATE_PENDING;
  179. }
  180. protected function refreshState(): Waiter
  181. {
  182. return $this;
  183. }
  184. private function stealResponse(self $waiter): void
  185. {
  186. $this->response = $waiter->response;
  187. $this->resolved = $waiter->resolved;
  188. $waiter->resolved = true;
  189. $this->needRefresh = false;
  190. }
  191. }