PageRenderTime 52ms CodeModel.GetById 24ms RepoModel.GetById 1ms app.codeStats 0ms

/src/Google/Task/Runner.php

https://gitlab.com/crsr/google-api-php-client
PHP | 284 lines | 149 code | 33 blank | 102 comment | 20 complexity | 13db5ea10d6adaf72758030726fcfc12 MD5 | raw file
  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 string $name The name of the current task (used for logging).
  55. */
  56. private $name;
  57. /**
  58. * @var callable $action The task to run and possibly retry.
  59. */
  60. private $action;
  61. /**
  62. * @var array $arguments The task arguments.
  63. */
  64. private $arguments;
  65. /**
  66. * @var array $retryMap Map of errors with retry counts.
  67. */
  68. protected $retryMap = [
  69. '500' => self::TASK_RETRY_ALWAYS,
  70. '503' => self::TASK_RETRY_ALWAYS,
  71. 'rateLimitExceeded' => self::TASK_RETRY_ALWAYS,
  72. 'userRateLimitExceeded' => self::TASK_RETRY_ALWAYS,
  73. 6 => self::TASK_RETRY_ALWAYS, // CURLE_COULDNT_RESOLVE_HOST
  74. 7 => self::TASK_RETRY_ALWAYS, // CURLE_COULDNT_CONNECT
  75. 28 => self::TASK_RETRY_ALWAYS, // CURLE_OPERATION_TIMEOUTED
  76. 35 => self::TASK_RETRY_ALWAYS, // CURLE_SSL_CONNECT_ERROR
  77. 52 => self::TASK_RETRY_ALWAYS // CURLE_GOT_NOTHING
  78. ];
  79. /**
  80. * Creates a new task runner with exponential backoff support.
  81. *
  82. * @param array $config The task runner config
  83. * @param string $name The name of the current task (used for logging)
  84. * @param callable $action The task to run and possibly retry
  85. * @param array $arguments The task arguments
  86. * @throws Google_Task_Exception when misconfigured
  87. */
  88. public function __construct(
  89. $config,
  90. $name,
  91. $action,
  92. array $arguments = array()
  93. ) {
  94. if (isset($config['initial_delay'])) {
  95. if ($config['initial_delay'] < 0) {
  96. throw new Google_Task_Exception(
  97. 'Task configuration `initial_delay` must not be negative.'
  98. );
  99. }
  100. $this->delay = $config['initial_delay'];
  101. }
  102. if (isset($config['max_delay'])) {
  103. if ($config['max_delay'] <= 0) {
  104. throw new Google_Task_Exception(
  105. 'Task configuration `max_delay` must be greater than 0.'
  106. );
  107. }
  108. $this->maxDelay = $config['max_delay'];
  109. }
  110. if (isset($config['factor'])) {
  111. if ($config['factor'] <= 0) {
  112. throw new Google_Task_Exception(
  113. 'Task configuration `factor` must be greater than 0.'
  114. );
  115. }
  116. $this->factor = $config['factor'];
  117. }
  118. if (isset($config['jitter'])) {
  119. if ($config['jitter'] <= 0) {
  120. throw new Google_Task_Exception(
  121. 'Task configuration `jitter` must be greater than 0.'
  122. );
  123. }
  124. $this->jitter = $config['jitter'];
  125. }
  126. if (isset($config['retries'])) {
  127. if ($config['retries'] < 0) {
  128. throw new Google_Task_Exception(
  129. 'Task configuration `retries` must not be negative.'
  130. );
  131. }
  132. $this->maxAttempts += $config['retries'];
  133. }
  134. if (!is_callable($action)) {
  135. throw new Google_Task_Exception(
  136. 'Task argument `$action` must be a valid callable.'
  137. );
  138. }
  139. $this->name = $name;
  140. $this->action = $action;
  141. $this->arguments = $arguments;
  142. }
  143. /**
  144. * Checks if a retry can be attempted.
  145. *
  146. * @return boolean
  147. */
  148. public function canAttempt()
  149. {
  150. return $this->attempts < $this->maxAttempts;
  151. }
  152. /**
  153. * Runs the task and (if applicable) automatically retries when errors occur.
  154. *
  155. * @return mixed
  156. * @throws Google_Task_Retryable on failure when no retries are available.
  157. */
  158. public function run()
  159. {
  160. while ($this->attempt()) {
  161. try {
  162. return call_user_func_array($this->action, $this->arguments);
  163. } catch (Google_Service_Exception $exception) {
  164. $allowedRetries = $this->allowedRetries(
  165. $exception->getCode(),
  166. $exception->getErrors()
  167. );
  168. if (!$this->canAttempt() || !$allowedRetries) {
  169. throw $exception;
  170. }
  171. if ($allowedRetries > 0) {
  172. $this->maxAttempts = min(
  173. $this->maxAttempts,
  174. $this->attempts + $allowedRetries
  175. );
  176. }
  177. }
  178. }
  179. }
  180. /**
  181. * Runs a task once, if possible. This is useful for bypassing the `run()`
  182. * loop.
  183. *
  184. * NOTE: If this is not the first attempt, this function will sleep in
  185. * accordance to the backoff configurations before running the task.
  186. *
  187. * @return boolean
  188. */
  189. public function attempt()
  190. {
  191. if (!$this->canAttempt()) {
  192. return false;
  193. }
  194. if ($this->attempts > 0) {
  195. $this->backOff();
  196. }
  197. $this->attempts++;
  198. return true;
  199. }
  200. /**
  201. * Sleeps in accordance to the backoff configurations.
  202. */
  203. private function backOff()
  204. {
  205. $delay = $this->getDelay();
  206. usleep($delay * 1000000);
  207. }
  208. /**
  209. * Gets the delay (in seconds) for the current backoff period.
  210. *
  211. * @return float
  212. */
  213. private function getDelay()
  214. {
  215. $jitter = $this->getJitter();
  216. $factor = $this->attempts > 1 ? $this->factor + $jitter : 1 + abs($jitter);
  217. return $this->delay = min($this->maxDelay, $this->delay * $factor);
  218. }
  219. /**
  220. * Gets the current jitter (random number between -$this->jitter and
  221. * $this->jitter).
  222. *
  223. * @return float
  224. */
  225. private function getJitter()
  226. {
  227. return $this->jitter * 2 * mt_rand() / mt_getrandmax() - $this->jitter;
  228. }
  229. /**
  230. * Gets the number of times the associated task can be retried.
  231. *
  232. * NOTE: -1 is returned if the task can be retried indefinitely
  233. *
  234. * @return integer
  235. */
  236. public function allowedRetries($code, $errors = array())
  237. {
  238. if (isset($this->retryMap[$code])) {
  239. return $this->retryMap[$code];
  240. }
  241. if (!empty($errors) && isset($errors[0]['reason']) &&
  242. isset($this->retryMap[$errors[0]['reason']])) {
  243. return $this->retryMap[$errors[0]['reason']];
  244. }
  245. return 0;
  246. }
  247. public function setRetryMap($retryMap)
  248. {
  249. $this->retryMap = $retryMap;
  250. }
  251. }