PageRenderTime 112ms CodeModel.GetById 22ms RepoModel.GetById 1ms app.codeStats 0ms

/src/RetryMiddleware.php

https://gitlab.com/github-cloud-corp/aws-sdk-php
PHP | 209 lines | 156 code | 24 blank | 29 comment | 12 complexity | 4230e69ef3f728cb84472da871a7699a MD5 | raw file
  1. <?php
  2. namespace Aws;
  3. use Aws\Exception\AwsException;
  4. use Exception;
  5. use Psr\Http\Message\RequestInterface;
  6. use GuzzleHttp\Promise;
  7. use GuzzleHttp\Promise\PromiseInterface;
  8. use GuzzleHttp\Psr7;
  9. /**
  10. * @internal Middleware that retries failures.
  11. */
  12. class RetryMiddleware
  13. {
  14. private static $retryStatusCodes = [
  15. 500 => true,
  16. 502 => true,
  17. 503 => true,
  18. 504 => true
  19. ];
  20. private static $retryCodes = [
  21. // Throttling error
  22. 'RequestLimitExceeded' => true,
  23. 'Throttling' => true,
  24. 'ThrottlingException' => true,
  25. 'ProvisionedThroughputExceededException' => true,
  26. 'RequestThrottled' => true,
  27. 'BandwidthLimitExceeded' => true,
  28. ];
  29. private $decider;
  30. private $delay;
  31. private $nextHandler;
  32. private $collectStats;
  33. public function __construct(
  34. callable $decider,
  35. callable $delay,
  36. callable $nextHandler,
  37. $collectStats = false
  38. ) {
  39. $this->decider = $decider;
  40. $this->delay = $delay;
  41. $this->nextHandler = $nextHandler;
  42. $this->collectStats = (bool) $collectStats;
  43. }
  44. /**
  45. * Creates a default AWS retry decider function.
  46. *
  47. * @param int $maxRetries
  48. *
  49. * @return callable
  50. */
  51. public static function createDefaultDecider($maxRetries = 3)
  52. {
  53. return function (
  54. $retries,
  55. CommandInterface $command,
  56. RequestInterface $request,
  57. ResultInterface $result = null,
  58. $error = null
  59. ) use ($maxRetries) {
  60. // Allow command-level options to override this value
  61. $maxRetries = null !== $command['@retries'] ?
  62. $command['@retries']
  63. : $maxRetries;
  64. if ($retries >= $maxRetries) {
  65. return false;
  66. } elseif (!$error) {
  67. return isset(self::$retryStatusCodes[$result['@metadata']['statusCode']]);
  68. } elseif (!($error instanceof AwsException)) {
  69. return false;
  70. } elseif ($error->isConnectionError()) {
  71. return true;
  72. } elseif (isset(self::$retryCodes[$error->getAwsErrorCode()])) {
  73. return true;
  74. } elseif (isset(self::$retryStatusCodes[$error->getStatusCode()])) {
  75. return true;
  76. } else {
  77. return false;
  78. }
  79. };
  80. }
  81. /**
  82. * Delay function that calculates an exponential delay.
  83. *
  84. * Exponential backoff with jitter, 100ms base, 20 sec ceiling
  85. *
  86. * @param $retries
  87. *
  88. * @return int
  89. */
  90. public static function exponentialDelay($retries)
  91. {
  92. return mt_rand(0, (int) min(20000, (int) pow(2, $retries - 1) * 100));
  93. }
  94. /**
  95. * @param CommandInterface $command
  96. * @param RequestInterface $request
  97. *
  98. * @return PromiseInterface
  99. */
  100. public function __invoke(
  101. CommandInterface $command,
  102. RequestInterface $request = null
  103. ) {
  104. $retries = 0;
  105. $requestStats = [];
  106. $handler = $this->nextHandler;
  107. $decider = $this->decider;
  108. $delay = $this->delay;
  109. $request = $this->addRetryHeader($request, 0, 0);
  110. $g = function ($value) use (
  111. $handler,
  112. $decider,
  113. $delay,
  114. $command,
  115. $request,
  116. &$retries,
  117. &$requestStats,
  118. &$g
  119. ) {
  120. $this->updateHttpStats($value, $requestStats);
  121. if ($value instanceof \Exception || $value instanceof \Throwable) {
  122. if (!$decider($retries, $command, $request, null, $value)) {
  123. return \GuzzleHttp\Promise\rejection_for(
  124. $this->bindStatsToReturn($value, $requestStats)
  125. );
  126. }
  127. } elseif ($value instanceof ResultInterface
  128. && !$decider($retries, $command, $request, $value, null)
  129. ) {
  130. return $this->bindStatsToReturn($value, $requestStats);
  131. }
  132. // Delay fn is called with 0, 1, ... so increment after the call.
  133. $delayBy = $delay($retries++);
  134. $command['@http']['delay'] = $delayBy;
  135. if ($this->collectStats) {
  136. $this->updateStats($retries, $delayBy, $requestStats);
  137. }
  138. // Update retry header with retry count and delayBy
  139. $request = $this->addRetryHeader($request, $retries, $delayBy);
  140. return $handler($command, $request)->then($g, $g);
  141. };
  142. return $handler($command, $request)->then($g, $g);
  143. }
  144. private function addRetryHeader($request, $retries, $delayBy)
  145. {
  146. return $request->withHeader('aws-sdk-retry', "{$retries}/{$delayBy}");
  147. }
  148. private function updateStats($retries, $delay, array &$stats)
  149. {
  150. if (!isset($stats['total_retry_delay'])) {
  151. $stats['total_retry_delay'] = 0;
  152. }
  153. $stats['total_retry_delay'] += $delay;
  154. $stats['retries_attempted'] = $retries;
  155. }
  156. private function updateHttpStats($value, array &$stats)
  157. {
  158. if (empty($stats['http'])) {
  159. $stats['http'] = [];
  160. }
  161. if ($value instanceof AwsException) {
  162. $resultStats = isset($value->getTransferInfo('http')[0])
  163. ? $value->getTransferInfo('http')[0]
  164. : [];
  165. $stats['http'] []= $resultStats;
  166. } elseif ($value instanceof ResultInterface) {
  167. $resultStats = isset($value['@metadata']['transferStats']['http'][0])
  168. ? $value['@metadata']['transferStats']['http'][0]
  169. : [];
  170. $stats['http'] []= $resultStats;
  171. }
  172. }
  173. private function bindStatsToReturn($return, array $stats)
  174. {
  175. if ($return instanceof ResultInterface) {
  176. if (!isset($return['@metadata'])) {
  177. $return['@metadata'] = [];
  178. }
  179. $return['@metadata']['transferStats'] = $stats;
  180. } elseif ($return instanceof AwsException) {
  181. $return->setTransferInfo($stats);
  182. }
  183. return $return;
  184. }
  185. }