RetryMiddleware.php 6.3 KB

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