Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401]

   1  <?php
   2  
   3  namespace MongoDB\Operation;
   4  
   5  use Exception;
   6  use MongoDB\Driver\Exception\RuntimeException;
   7  use MongoDB\Driver\Session;
   8  use function call_user_func;
   9  use function time;
  10  
  11  /**
  12   * @internal
  13   */
  14  class WithTransaction
  15  {
  16      /** @var callable */
  17      private $callback;
  18  
  19      /** @var array */
  20      private $transactionOptions;
  21  
  22      /**
  23       * @see Session::startTransaction for supported transaction options
  24       *
  25       * @param callable $callback           A callback that will be invoked within the transaction
  26       * @param array    $transactionOptions Additional options that are passed to Session::startTransaction
  27       */
  28      public function __construct(callable $callback, array $transactionOptions = [])
  29      {
  30          $this->callback = $callback;
  31          $this->transactionOptions = $transactionOptions;
  32      }
  33  
  34      /**
  35       * Execute the operation in the given session
  36       *
  37       * This helper takes care of retrying the commit operation or the entire
  38       * transaction if an error occurs.
  39       *
  40       * If the commit fails because of an UnknownTransactionCommitResult error, the
  41       * commit is retried without re-invoking the callback.
  42       * If the commit fails because of a TransientTransactionError, the entire
  43       * transaction will be retried. In this case, the callback will be invoked
  44       * again. It is important that the logic inside the callback is idempotent.
  45       *
  46       * In case of failures, the commit or transaction are retried until 120 seconds
  47       * from the initial call have elapsed. After that, no retries will happen and
  48       * the helper will throw the last exception received from the driver.
  49       *
  50       * @see Client::startSession
  51       *
  52       * @param Session $session A session object as retrieved by Client::startSession
  53       * @return void
  54       * @throws RuntimeException for driver errors while committing the transaction
  55       * @throws Exception for any other errors, including those thrown in the callback
  56       */
  57      public function execute(Session $session)
  58      {
  59          $startTime = time();
  60  
  61          while (true) {
  62              $session->startTransaction($this->transactionOptions);
  63  
  64              try {
  65                  call_user_func($this->callback, $session);
  66              } catch (Exception $e) {
  67                  if ($session->isInTransaction()) {
  68                      $session->abortTransaction();
  69                  }
  70  
  71                  if ($e instanceof RuntimeException &&
  72                      $e->hasErrorLabel('TransientTransactionError') &&
  73                      ! $this->isTransactionTimeLimitExceeded($startTime)
  74                  ) {
  75                      continue;
  76                  }
  77  
  78                  throw $e;
  79              }
  80  
  81              if (! $session->isInTransaction()) {
  82                  // Assume callback intentionally ended the transaction
  83                  return;
  84              }
  85  
  86              while (true) {
  87                  try {
  88                      $session->commitTransaction();
  89                  } catch (RuntimeException $e) {
  90                      if ($e->getCode() !== 50 /* MaxTimeMSExpired */ &&
  91                          $e->hasErrorLabel('UnknownTransactionCommitResult') &&
  92                          ! $this->isTransactionTimeLimitExceeded($startTime)
  93                      ) {
  94                          // Retry committing the transaction
  95                          continue;
  96                      }
  97  
  98                      if ($e->hasErrorLabel('TransientTransactionError') &&
  99                          ! $this->isTransactionTimeLimitExceeded($startTime)
 100                      ) {
 101                          // Restart the transaction, invoking the callback again
 102                          continue 2;
 103                      }
 104  
 105                      throw $e;
 106                  }
 107  
 108                  // Commit was successful
 109                  break;
 110              }
 111  
 112              // Transaction was successful
 113              break;
 114          }
 115      }
 116  
 117      /**
 118       * Returns whether the time limit for retrying transactions in the convenient transaction API has passed
 119       *
 120       * @param int $startTime The time the transaction was started
 121       * @return bool
 122       */
 123      private function isTransactionTimeLimitExceeded($startTime)
 124      {
 125          return time() - $startTime >= 120;
 126      }
 127  }