Без опису

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. <?php
  2. /*
  3. * Copyright 2014 Google Inc.
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. /**
  18. * A task runner with exponential backoff support.
  19. *
  20. * @see https://developers.google.com/drive/web/handle-errors#implementing_exponential_backoff
  21. */
  22. class Google_Task_Runner
  23. {
  24. const TASK_RETRY_NEVER = 0;
  25. const TASK_RETRY_ONCE = 1;
  26. const TASK_RETRY_ALWAYS = -1;
  27. /**
  28. * @var integer $maxDelay The max time (in seconds) to wait before a retry.
  29. */
  30. private $maxDelay = 60;
  31. /**
  32. * @var integer $delay The previous delay from which the next is calculated.
  33. */
  34. private $delay = 1;
  35. /**
  36. * @var integer $factor The base number for the exponential back off.
  37. */
  38. private $factor = 2;
  39. /**
  40. * @var float $jitter A random number between -$jitter and $jitter will be
  41. * added to $factor on each iteration to allow for a better distribution of
  42. * retries.
  43. */
  44. private $jitter = 0.5;
  45. /**
  46. * @var integer $attempts The number of attempts that have been tried so far.
  47. */
  48. private $attempts = 0;
  49. /**
  50. * @var integer $maxAttempts The max number of attempts allowed.
  51. */
  52. private $maxAttempts = 1;
  53. /**
  54. * @var callable $action The task to run and possibly retry.
  55. */
  56. private $action;
  57. /**
  58. * @var array $arguments The task arguments.
  59. */
  60. private $arguments;
  61. /**
  62. * @var array $retryMap Map of errors with retry counts.
  63. */
  64. protected $retryMap = [
  65. '500' => self::TASK_RETRY_ALWAYS,
  66. '503' => self::TASK_RETRY_ALWAYS,
  67. 'rateLimitExceeded' => self::TASK_RETRY_ALWAYS,
  68. 'userRateLimitExceeded' => self::TASK_RETRY_ALWAYS,
  69. 6 => self::TASK_RETRY_ALWAYS, // CURLE_COULDNT_RESOLVE_HOST
  70. 7 => self::TASK_RETRY_ALWAYS, // CURLE_COULDNT_CONNECT
  71. 28 => self::TASK_RETRY_ALWAYS, // CURLE_OPERATION_TIMEOUTED
  72. 35 => self::TASK_RETRY_ALWAYS, // CURLE_SSL_CONNECT_ERROR
  73. 52 => self::TASK_RETRY_ALWAYS // CURLE_GOT_NOTHING
  74. ];
  75. /**
  76. * Creates a new task runner with exponential backoff support.
  77. *
  78. * @param array $config The task runner config
  79. * @param string $name The name of the current task (used for logging)
  80. * @param callable $action The task to run and possibly retry
  81. * @param array $arguments The task arguments
  82. * @throws Google_Task_Exception when misconfigured
  83. */
  84. public function __construct(
  85. $config,
  86. $name,
  87. $action,
  88. array $arguments = array()
  89. ) {
  90. if (isset($config['initial_delay'])) {
  91. if ($config['initial_delay'] < 0) {
  92. throw new Google_Task_Exception(
  93. 'Task configuration `initial_delay` must not be negative.'
  94. );
  95. }
  96. $this->delay = $config['initial_delay'];
  97. }
  98. if (isset($config['max_delay'])) {
  99. if ($config['max_delay'] <= 0) {
  100. throw new Google_Task_Exception(
  101. 'Task configuration `max_delay` must be greater than 0.'
  102. );
  103. }
  104. $this->maxDelay = $config['max_delay'];
  105. }
  106. if (isset($config['factor'])) {
  107. if ($config['factor'] <= 0) {
  108. throw new Google_Task_Exception(
  109. 'Task configuration `factor` must be greater than 0.'
  110. );
  111. }
  112. $this->factor = $config['factor'];
  113. }
  114. if (isset($config['jitter'])) {
  115. if ($config['jitter'] <= 0) {
  116. throw new Google_Task_Exception(
  117. 'Task configuration `jitter` must be greater than 0.'
  118. );
  119. }
  120. $this->jitter = $config['jitter'];
  121. }
  122. if (isset($config['retries'])) {
  123. if ($config['retries'] < 0) {
  124. throw new Google_Task_Exception(
  125. 'Task configuration `retries` must not be negative.'
  126. );
  127. }
  128. $this->maxAttempts += $config['retries'];
  129. }
  130. if (!is_callable($action)) {
  131. throw new Google_Task_Exception(
  132. 'Task argument `$action` must be a valid callable.'
  133. );
  134. }
  135. $this->action = $action;
  136. $this->arguments = $arguments;
  137. }
  138. /**
  139. * Checks if a retry can be attempted.
  140. *
  141. * @return boolean
  142. */
  143. public function canAttempt()
  144. {
  145. return $this->attempts < $this->maxAttempts;
  146. }
  147. /**
  148. * Runs the task and (if applicable) automatically retries when errors occur.
  149. *
  150. * @return mixed
  151. * @throws Google_Task_Retryable on failure when no retries are available.
  152. */
  153. public function run()
  154. {
  155. while ($this->attempt()) {
  156. try {
  157. return call_user_func_array($this->action, $this->arguments);
  158. } catch (Google_Service_Exception $exception) {
  159. $allowedRetries = $this->allowedRetries(
  160. $exception->getCode(),
  161. $exception->getErrors()
  162. );
  163. if (!$this->canAttempt() || !$allowedRetries) {
  164. throw $exception;
  165. }
  166. if ($allowedRetries > 0) {
  167. $this->maxAttempts = min(
  168. $this->maxAttempts,
  169. $this->attempts + $allowedRetries
  170. );
  171. }
  172. }
  173. }
  174. }
  175. /**
  176. * Runs a task once, if possible. This is useful for bypassing the `run()`
  177. * loop.
  178. *
  179. * NOTE: If this is not the first attempt, this function will sleep in
  180. * accordance to the backoff configurations before running the task.
  181. *
  182. * @return boolean
  183. */
  184. public function attempt()
  185. {
  186. if (!$this->canAttempt()) {
  187. return false;
  188. }
  189. if ($this->attempts > 0) {
  190. $this->backOff();
  191. }
  192. $this->attempts++;
  193. return true;
  194. }
  195. /**
  196. * Sleeps in accordance to the backoff configurations.
  197. */
  198. private function backOff()
  199. {
  200. $delay = $this->getDelay();
  201. usleep($delay * 1000000);
  202. }
  203. /**
  204. * Gets the delay (in seconds) for the current backoff period.
  205. *
  206. * @return float
  207. */
  208. private function getDelay()
  209. {
  210. $jitter = $this->getJitter();
  211. $factor = $this->attempts > 1 ? $this->factor + $jitter : 1 + abs($jitter);
  212. return $this->delay = min($this->maxDelay, $this->delay * $factor);
  213. }
  214. /**
  215. * Gets the current jitter (random number between -$this->jitter and
  216. * $this->jitter).
  217. *
  218. * @return float
  219. */
  220. private function getJitter()
  221. {
  222. return $this->jitter * 2 * mt_rand() / mt_getrandmax() - $this->jitter;
  223. }
  224. /**
  225. * Gets the number of times the associated task can be retried.
  226. *
  227. * NOTE: -1 is returned if the task can be retried indefinitely
  228. *
  229. * @return integer
  230. */
  231. public function allowedRetries($code, $errors = array())
  232. {
  233. if (isset($this->retryMap[$code])) {
  234. return $this->retryMap[$code];
  235. }
  236. if (
  237. !empty($errors) &&
  238. isset($errors[0]['reason'], $this->retryMap[$errors[0]['reason']])
  239. ) {
  240. return $this->retryMap[$errors[0]['reason']];
  241. }
  242. return 0;
  243. }
  244. public function setRetryMap($retryMap)
  245. {
  246. $this->retryMap = $retryMap;
  247. }
  248. }