Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403] [Versions 402 and 403]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  namespace core\lock;
  18  
  19  use coding_exception;
  20  
  21  /**
  22   * This is a db record locking factory.
  23   *
  24   * This lock factory uses record locks relying on sql of the form "SET XXX where YYY" and checking if the
  25   * value was set. It supports timeouts, autorelease and can work on any DB. The downside - is this
  26   * will always be slower than some shared memory type locking function.
  27   *
  28   * @package   core
  29   * @category  lock
  30   * @copyright Damyon Wiese 2013
  31   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  32   */
  33  class db_record_lock_factory implements lock_factory {
  34  
  35      /** @var \moodle_database $db Hold a reference to the global $DB */
  36      protected $db;
  37  
  38      /** @var string $type Used to prefix lock keys */
  39      protected $type;
  40  
  41      /** @var array $openlocks - List of held locks - used by auto-release */
  42      protected $openlocks = array();
  43  
  44      /**
  45       * Is available.
  46       * @return boolean - True if this lock type is available in this environment.
  47       */
  48      public function is_available() {
  49          return true;
  50      }
  51  
  52      /**
  53       * Almighty constructor.
  54       * @param string $type - Used to prefix lock keys.
  55       */
  56      public function __construct($type) {
  57          global $DB;
  58  
  59          $this->type = $type;
  60          // Save a reference to the global $DB so it will not be released while we still have open locks.
  61          $this->db = $DB;
  62  
  63          \core_shutdown_manager::register_function(array($this, 'auto_release'));
  64      }
  65  
  66      /**
  67       * Return information about the blocking behaviour of the lock type on this platform.
  68       * @return boolean - True
  69       */
  70      public function supports_timeout() {
  71          return true;
  72      }
  73  
  74      /**
  75       * Will this lock type will be automatically released when a process ends.
  76       *
  77       * @return boolean - True (shutdown handler)
  78       */
  79      public function supports_auto_release() {
  80          return true;
  81      }
  82  
  83      /**
  84       * @deprecated since Moodle 3.10.
  85       */
  86      public function supports_recursion() {
  87          throw new coding_exception('The function supports_recursion() has been removed, please do not use it anymore.');
  88      }
  89  
  90      /**
  91       * This function generates a unique token for the lock to use.
  92       * It is important that this token is not solely based on time as this could lead
  93       * to duplicates in a clustered environment (especially on VMs due to poor time precision).
  94       */
  95      protected function generate_unique_token() {
  96          return \core\uuid::generate();
  97      }
  98  
  99      /**
 100       * Create and get a lock
 101       * @param string $resource - The identifier for the lock. Should use frankenstyle prefix.
 102       * @param int $timeout - The number of seconds to wait for a lock before giving up.
 103       * @param int $maxlifetime - Unused by this lock type.
 104       * @return boolean - true if a lock was obtained.
 105       */
 106      public function get_lock($resource, $timeout, $maxlifetime = 86400) {
 107  
 108          $token = $this->generate_unique_token();
 109          $now = time();
 110          $giveuptime = $now + $timeout;
 111          $expires = $now + $maxlifetime;
 112  
 113          $resourcekey = $this->type . '_' . $resource;
 114  
 115          if (!$this->db->record_exists('lock_db', array('resourcekey' => $resourcekey))) {
 116              $record = new \stdClass();
 117              $record->resourcekey = $resourcekey;
 118              $result = $this->db->insert_record('lock_db', $record);
 119          }
 120  
 121          $params = array('expires' => $expires,
 122                          'token' => $token,
 123                          'resourcekey' => $resourcekey,
 124                          'now' => $now);
 125          $sql = 'UPDATE {lock_db}
 126                     SET
 127                         expires = :expires,
 128                         owner = :token
 129                   WHERE
 130                         resourcekey = :resourcekey AND
 131                         (owner IS NULL OR expires < :now)';
 132  
 133          do {
 134              $now = time();
 135              $params['now'] = $now;
 136              $this->db->execute($sql, $params);
 137  
 138              $countparams = array('owner' => $token, 'resourcekey' => $resourcekey);
 139              $result = $this->db->count_records('lock_db', $countparams);
 140              $locked = $result === 1;
 141              if (!$locked && $timeout > 0) {
 142                  usleep(rand(10000, 250000)); // Sleep between 10 and 250 milliseconds.
 143              }
 144              // Try until the giveup time.
 145          } while (!$locked && $now < $giveuptime);
 146  
 147          if ($locked) {
 148              $this->openlocks[$token] = 1;
 149              return new lock($token, $this);
 150          }
 151  
 152          return false;
 153      }
 154  
 155      /**
 156       * Release a lock that was previously obtained with @lock.
 157       * @param lock $lock - a lock obtained from this factory.
 158       * @return boolean - true if the lock is no longer held (including if it was never held).
 159       */
 160      public function release_lock(lock $lock) {
 161          $params = array('noexpires' => null,
 162                          'token' => $lock->get_key(),
 163                          'noowner' => null);
 164  
 165          $sql = 'UPDATE {lock_db}
 166                      SET
 167                          expires = :noexpires,
 168                          owner = :noowner
 169                      WHERE
 170                          owner = :token';
 171          $result = $this->db->execute($sql, $params);
 172          if ($result) {
 173              unset($this->openlocks[$lock->get_key()]);
 174          }
 175          return $result;
 176      }
 177  
 178      /**
 179       * @deprecated since Moodle 3.10.
 180       */
 181      public function extend_lock() {
 182          throw new coding_exception('The function extend_lock() has been removed, please do not use it anymore.');
 183      }
 184  
 185      /**
 186       * Auto release any open locks on shutdown.
 187       * This is required, because we may be using persistent DB connections.
 188       */
 189      public function auto_release() {
 190          // Called from the shutdown handler. Must release all open locks.
 191          foreach ($this->openlocks as $key => $unused) {
 192              $lock = new lock($key, $this);
 193              $lock->release();
 194          }
 195      }
 196  }