Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

Differences Between: [Versions 400 and 402] [Versions 400 and 403]

   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  if (!class_exists('Google_Client')) {
  19    require_once dirname(__FILE__) . '/../autoload.php';
  20  }
  21  
  22  /**
  23   * A task runner with exponential backoff support.
  24   *
  25   * @see https://developers.google.com/drive/web/handle-errors#implementing_exponential_backoff
  26   */
  27  class Google_Task_Runner
  28  {
  29    /**
  30     * @var integer $maxDelay The max time (in seconds) to wait before a retry.
  31     */
  32    private $maxDelay = 60;
  33    /**
  34     * @var integer $delay The previous delay from which the next is calculated.
  35     */
  36    private $delay = 1;
  37  
  38    /**
  39     * @var integer $factor The base number for the exponential back off.
  40     */
  41    private $factor = 2;
  42    /**
  43     * @var float $jitter A random number between -$jitter and $jitter will be
  44     * added to $factor on each iteration to allow for a better distribution of
  45     * retries.
  46     */
  47    private $jitter = 0.5;
  48  
  49    /**
  50     * @var integer $attempts The number of attempts that have been tried so far.
  51     */
  52    private $attempts = 0;
  53    /**
  54     * @var integer $maxAttempts The max number of attempts allowed.
  55     */
  56    private $maxAttempts = 1;
  57  
  58    /**
  59     * @var Google_Client $client The current API client.
  60     */
  61    private $client;
  62  
  63    /**
  64     * @var string $name The name of the current task (used for logging).
  65     */
  66    private $name;
  67    /**
  68     * @var callable $action The task to run and possibly retry.
  69     */
  70    private $action;
  71    /**
  72     * @var array $arguments The task arguments.
  73     */
  74    private $arguments;
  75  
  76    /**
  77     * Creates a new task runner with exponential backoff support.
  78     *
  79     * @param Google_Client $client The current API client
  80     * @param string $name The name of the current task (used for logging)
  81     * @param callable $action The task to run and possibly retry
  82     * @param array $arguments The task arguments
  83     * @throws Google_Task_Exception when misconfigured
  84     */
  85    public function __construct(
  86        Google_Client $client,
  87        $name,
  88        $action,
  89        array $arguments = array()
  90    ) {
  91      $config = (array) $client->getClassConfig('Google_Task_Runner');
  92  
  93      if (isset($config['initial_delay'])) {
  94        if ($config['initial_delay'] < 0) {
  95          throw new Google_Task_Exception(
  96              'Task configuration `initial_delay` must not be negative.'
  97          );
  98        }
  99  
 100        $this->delay = $config['initial_delay'];
 101      }
 102  
 103      if (isset($config['max_delay'])) {
 104        if ($config['max_delay'] <= 0) {
 105          throw new Google_Task_Exception(
 106              'Task configuration `max_delay` must be greater than 0.'
 107          );
 108        }
 109  
 110        $this->maxDelay = $config['max_delay'];
 111      }
 112  
 113      if (isset($config['factor'])) {
 114        if ($config['factor'] <= 0) {
 115          throw new Google_Task_Exception(
 116              'Task configuration `factor` must be greater than 0.'
 117          );
 118        }
 119  
 120        $this->factor = $config['factor'];
 121      }
 122  
 123      if (isset($config['jitter'])) {
 124        if ($config['jitter'] <= 0) {
 125          throw new Google_Task_Exception(
 126              'Task configuration `jitter` must be greater than 0.'
 127          );
 128        }
 129  
 130        $this->jitter = $config['jitter'];
 131      }
 132  
 133      if (isset($config['retries'])) {
 134        if ($config['retries'] < 0) {
 135          throw new Google_Task_Exception(
 136              'Task configuration `retries` must not be negative.'
 137          );
 138        }
 139        $this->maxAttempts += $config['retries'];
 140      }
 141  
 142      if (!is_callable($action)) {
 143          throw new Google_Task_Exception(
 144              'Task argument `$action` must be a valid callable.'
 145          );
 146      }
 147  
 148      $this->name = $name;
 149      $this->client = $client;
 150      $this->action = $action;
 151      $this->arguments = $arguments;
 152    }
 153  
 154    /**
 155     * Checks if a retry can be attempted.
 156     *
 157     * @return boolean
 158     */
 159    public function canAttmpt()
 160    {
 161      return $this->attempts < $this->maxAttempts;
 162    }
 163  
 164    /**
 165     * Runs the task and (if applicable) automatically retries when errors occur.
 166     *
 167     * @return mixed
 168     * @throws Google_Task_Retryable on failure when no retries are available.
 169     */
 170    public function run()
 171    {
 172      while ($this->attempt()) {
 173        try {
 174          return call_user_func_array($this->action, $this->arguments);
 175        } catch (Google_Task_Retryable $exception) {
 176          $allowedRetries = $exception->allowedRetries();
 177  
 178          if (!$this->canAttmpt() || !$allowedRetries) {
 179            throw $exception;
 180          }
 181  
 182          if ($allowedRetries > 0) {
 183            $this->maxAttempts = min(
 184                $this->maxAttempts,
 185                $this->attempts + $allowedRetries
 186            );
 187          }
 188        }
 189      }
 190    }
 191  
 192    /**
 193     * Runs a task once, if possible. This is useful for bypassing the `run()`
 194     * loop.
 195     *
 196     * NOTE: If this is not the first attempt, this function will sleep in
 197     * accordance to the backoff configurations before running the task.
 198     *
 199     * @return boolean
 200     */
 201    public function attempt()
 202    {
 203      if (!$this->canAttmpt()) {
 204        return false;
 205      }
 206  
 207      if ($this->attempts > 0) {
 208        $this->backOff();
 209      }
 210  
 211      $this->attempts++;
 212      return true;
 213    }
 214  
 215    /**
 216     * Sleeps in accordance to the backoff configurations.
 217     */
 218    private function backOff()
 219    {
 220      $delay = $this->getDelay();
 221  
 222      $this->client->getLogger()->debug(
 223          'Retrying task with backoff',
 224          array(
 225              'request' => $this->name,
 226              'retry' => $this->attempts,
 227              'backoff_seconds' => $delay
 228          )
 229      );
 230  
 231      usleep($delay * 1000000);
 232    }
 233  
 234    /**
 235     * Gets the delay (in seconds) for the current backoff period.
 236     *
 237     * @return float
 238     */
 239    private function getDelay()
 240    {
 241      $jitter = $this->getJitter();
 242      $factor = $this->attempts > 1 ? $this->factor + $jitter : 1 + abs($jitter);
 243  
 244      return $this->delay = min($this->maxDelay, $this->delay * $factor);
 245    }
 246  
 247    /**
 248     * Gets the current jitter (random number between -$this->jitter and
 249     * $this->jitter).
 250     *
 251     * @return float
 252     */
 253    private function getJitter()
 254    {
 255      return $this->jitter * 2 * mt_rand() / mt_getrandmax() - $this->jitter;
 256    }
 257  }