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 39 and 401] [Versions 401 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  /**
  18   * This is a db record locking factory.
  19   *
  20   * @package    core
  21   * @category   lock
  22   * @copyright  Damyon Wiese 2013
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  namespace core\lock;
  27  
  28  defined('MOODLE_INTERNAL') || die();
  29  
  30  /**
  31   * This is a db record locking factory.
  32   *
  33   * This lock factory uses record locks relying on sql of the form "SET XXX where YYY" and checking if the
  34   * value was set. It supports timeouts, autorelease and can work on any DB. The downside - is this
  35   * will always be slower than some shared memory type locking function.
  36   *
  37   * @package   core
  38   * @category  lock
  39   * @copyright Damyon Wiese 2013
  40   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  41   */
  42  class db_record_lock_factory implements lock_factory {
  43  
  44      /** @var \moodle_database $db Hold a reference to the global $DB */
  45      protected $db;
  46  
  47      /** @var string $type Used to prefix lock keys */
  48      protected $type;
  49  
  50      /** @var array $openlocks - List of held locks - used by auto-release */
  51      protected $openlocks = array();
  52  
  53      /**
  54       * Is available.
  55       * @return boolean - True if this lock type is available in this environment.
  56       */
  57      public function is_available() {
  58          return true;
  59      }
  60  
  61      /**
  62       * Almighty constructor.
  63       * @param string $type - Used to prefix lock keys.
  64       */
  65      public function __construct($type) {
  66          global $DB;
  67  
  68          $this->type = $type;
  69          // Save a reference to the global $DB so it will not be released while we still have open locks.
  70          $this->db = $DB;
  71  
  72          \core_shutdown_manager::register_function(array($this, 'auto_release'));
  73      }
  74  
  75      /**
  76       * Return information about the blocking behaviour of the lock type on this platform.
  77       * @return boolean - True
  78       */
  79      public function supports_timeout() {
  80          return true;
  81      }
  82  
  83      /**
  84       * Will this lock type will be automatically released when a process ends.
  85       *
  86       * @return boolean - True (shutdown handler)
  87       */
  88      public function supports_auto_release() {
  89          return true;
  90      }
  91  
  92      /**
  93       * Multiple locks for the same resource can be held by a single process.
  94       *
  95       * @deprecated since Moodle 3.10.
  96       * @return boolean - False - not process specific.
  97       */
  98      public function supports_recursion() {
  99          debugging('The function supports_recursion() is deprecated, please do not use it anymore.',
 100              DEBUG_DEVELOPER);
 101          return false;
 102      }
 103  
 104      /**
 105       * This function generates a unique token for the lock to use.
 106       * It is important that this token is not solely based on time as this could lead
 107       * to duplicates in a clustered environment (especially on VMs due to poor time precision).
 108       */
 109      protected function generate_unique_token() {
 110          return \core\uuid::generate();
 111      }
 112  
 113      /**
 114       * Create and get a lock
 115       * @param string $resource - The identifier for the lock. Should use frankenstyle prefix.
 116       * @param int $timeout - The number of seconds to wait for a lock before giving up.
 117       * @param int $maxlifetime - Unused by this lock type.
 118       * @return boolean - true if a lock was obtained.
 119       */
 120      public function get_lock($resource, $timeout, $maxlifetime = 86400) {
 121  
 122          $token = $this->generate_unique_token();
 123          $now = time();
 124          $giveuptime = $now + $timeout;
 125          $expires = $now + $maxlifetime;
 126  
 127          $resourcekey = $this->type . '_' . $resource;
 128  
 129          if (!$this->db->record_exists('lock_db', array('resourcekey' => $resourcekey))) {
 130              $record = new \stdClass();
 131              $record->resourcekey = $resourcekey;
 132              $result = $this->db->insert_record('lock_db', $record);
 133          }
 134  
 135          $params = array('expires' => $expires,
 136                          'token' => $token,
 137                          'resourcekey' => $resourcekey,
 138                          'now' => $now);
 139          $sql = 'UPDATE {lock_db}
 140                     SET
 141                         expires = :expires,
 142                         owner = :token
 143                   WHERE
 144                         resourcekey = :resourcekey AND
 145                         (owner IS NULL OR expires < :now)';
 146  
 147          do {
 148              $now = time();
 149              $params['now'] = $now;
 150              $this->db->execute($sql, $params);
 151  
 152              $countparams = array('owner' => $token, 'resourcekey' => $resourcekey);
 153              $result = $this->db->count_records('lock_db', $countparams);
 154              $locked = $result === 1;
 155              if (!$locked && $timeout > 0) {
 156                  usleep(rand(10000, 250000)); // Sleep between 10 and 250 milliseconds.
 157              }
 158              // Try until the giveup time.
 159          } while (!$locked && $now < $giveuptime);
 160  
 161          if ($locked) {
 162              $this->openlocks[$token] = 1;
 163              return new lock($token, $this);
 164          }
 165  
 166          return false;
 167      }
 168  
 169      /**
 170       * Release a lock that was previously obtained with @lock.
 171       * @param lock $lock - a lock obtained from this factory.
 172       * @return boolean - true if the lock is no longer held (including if it was never held).
 173       */
 174      public function release_lock(lock $lock) {
 175          $params = array('noexpires' => null,
 176                          'token' => $lock->get_key(),
 177                          'noowner' => null);
 178  
 179          $sql = 'UPDATE {lock_db}
 180                      SET
 181                          expires = :noexpires,
 182                          owner = :noowner
 183                      WHERE
 184                          owner = :token';
 185          $result = $this->db->execute($sql, $params);
 186          if ($result) {
 187              unset($this->openlocks[$lock->get_key()]);
 188          }
 189          return $result;
 190      }
 191  
 192      /**
 193       * Extend a lock that was previously obtained with @lock.
 194       *
 195       * @deprecated since Moodle 3.10.
 196       * @param lock $lock - a lock obtained from this factory.
 197       * @param int $maxlifetime - the new lifetime for the lock (in seconds).
 198       * @return boolean - true if the lock was extended.
 199       */
 200      public function extend_lock(lock $lock, $maxlifetime = 86400) {
 201          debugging('The function extend_lock() is deprecated, please do not use it anymore.',
 202              DEBUG_DEVELOPER);
 203  
 204          $now = time();
 205          $expires = $now + $maxlifetime;
 206          $params = array('expires' => $expires,
 207                          'token' => $lock->get_key());
 208  
 209          $sql = 'UPDATE {lock_db}
 210                      SET
 211                          expires = :expires,
 212                      WHERE
 213                          owner = :token';
 214  
 215          $this->db->execute($sql, $params);
 216          $countparams = array('owner' => $lock->get_key());
 217          $result = $this->count_records('lock_db', $countparams);
 218  
 219          return $result === 0;
 220      }
 221  
 222      /**
 223       * Auto release any open locks on shutdown.
 224       * This is required, because we may be using persistent DB connections.
 225       */
 226      public function auto_release() {
 227          // Called from the shutdown handler. Must release all open locks.
 228          foreach ($this->openlocks as $key => $unused) {
 229              $lock = new lock($key, $this);
 230              $lock->release();
 231          }
 232      }
 233  }