Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

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