Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.
   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   * File locking for the Cache API
  19   *
  20   * @package    cachelock_file
  21   * @category   cache
  22   * @copyright  2012 Sam Hemelryk
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  defined('MOODLE_INTERNAL') || die();
  27  
  28  /**
  29   * File locking plugin
  30   *
  31   * @copyright  2012 Sam Hemelryk
  32   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  33   */
  34  class cachelock_file implements cache_lock_interface {
  35  
  36      /**
  37       * The name of the cache lock instance
  38       * @var string
  39       */
  40      protected $name;
  41  
  42      /**
  43       * The absolute directory in which lock files will be created and looked for.
  44       * @var string
  45       */
  46      protected $cachedir;
  47  
  48      /**
  49       * The maximum life in seconds for a lock file. By default null for none.
  50       * @var int|null
  51       */
  52      protected $maxlife = null;
  53  
  54      /**
  55       * The number of attempts to acquire a lock when blocking is required before throwing an exception.
  56       * @var int
  57       */
  58      protected $blockattempts = 100;
  59  
  60      /**
  61       * An array containing the locks that have been acquired but not released so far.
  62       * @var array Array of key => lock file path
  63       */
  64      protected $locks = array();
  65  
  66      /**
  67       * Initialises the cache lock instance.
  68       *
  69       * @param string $name The name of the cache lock
  70       * @param array $configuration
  71       */
  72      public function __construct($name, array $configuration = array()) {
  73          $this->name = $name;
  74          if (!array_key_exists('dir', $configuration)) {
  75              $this->cachedir = make_cache_directory(md5($name));
  76          } else {
  77              $dir = $configuration['dir'];
  78              if (strpos($dir, '/') !== false && strpos($dir, '.') !== 0) {
  79                  // This looks like an absolute path.
  80                  if (file_exists($dir) && is_dir($dir) && is_writable($dir)) {
  81                      $this->cachedir = $dir;
  82                  }
  83              }
  84              if (empty($this->cachedir)) {
  85                  $dir = preg_replace('#[^a-zA-Z0-9_]#', '_', $dir);
  86                  $this->cachedir = make_cache_directory($dir);
  87              }
  88          }
  89          if (array_key_exists('maxlife', $configuration) && is_number($configuration['maxlife'])) {
  90              $maxlife = (int)$configuration['maxlife'];
  91              // Minimum lock time is 60 seconds.
  92              $this->maxlife = max($maxlife, 60);
  93          }
  94          if (array_key_exists('blockattempts', $configuration) && is_number($configuration['blockattempts'])) {
  95              $this->blockattempts = (int)$configuration['blockattempts'];
  96          }
  97      }
  98  
  99      /**
 100       * Acquire a lock.
 101       *
 102       * If the lock can be acquired:
 103       *      This function will return true.
 104       *
 105       * If the lock cannot be acquired the result of this method is determined by the block param:
 106       *      $block = true (default)
 107       *          The function will block any further execution unti the lock can be acquired.
 108       *          This involves the function attempting to acquire the lock and the sleeping for a period of time. This process
 109       *          will be repeated until the lock is required or until a limit is hit (100 by default) in which case a cache
 110       *          exception will be thrown.
 111       *      $block = false
 112       *          The function will return false immediately.
 113       *
 114       * If a max life has been specified and the lock can not be acquired then the lock file will be checked against this time.
 115       * In the case that the file exceeds that max time it will be forcefully deleted.
 116       * Because this can obviously be a dangerous thing it is not used by default. If it is used it should be set high enough that
 117       * we can be as sure as possible that the executing code has completed.
 118       *
 119       * @param string $key The key that we want to lock
 120       * @param string $ownerid A unique identifier for the owner of this lock. Not used by default.
 121       * @param bool $block True if we want the program block further execution until the lock has been acquired.
 122       * @return bool
 123       * @throws cache_exception If block is set to true and more than 100 attempts have been made to acquire a lock.
 124       */
 125      public function lock($key, $ownerid, $block = false) {
 126          // Get the name of the lock file we want to use.
 127          $lockfile = $this->get_lock_file($key);
 128  
 129          // Attempt to create a handle to the lock file.
 130          // Mode xb is the secret to this whole function.
 131          //   x = Creates the file and opens it for writing. If the file already exists fopen returns false and a warning is thrown.
 132          //   b = Forces binary mode.
 133          $result = @fopen($lockfile, 'xb');
 134  
 135          // Check if we could create the file or not.
 136          if ($result === false) {
 137              // Lock exists already.
 138              if ($this->maxlife !== null && !array_key_exists($key, $this->locks)) {
 139                  $mtime = filemtime($lockfile);
 140                  if ($mtime < time() - $this->maxlife) {
 141                      $this->unlock($key, true);
 142                      $result = $this->lock($key, false);
 143                      if ($result) {
 144                          return true;
 145                      }
 146                  }
 147              }
 148              if ($block) {
 149                  // OK we are blocking. We had better sleep and then retry to lock.
 150                  $iterations = 0;
 151                  $maxiterations = $this->blockattempts;
 152                  while (($result = $this->lock($key, false)) === false) {
 153                      // Usleep causes the application to cleep to x microseconds.
 154                      // Before anyone asks there are 1'000'000 microseconds to a second.
 155                      usleep(rand(1000, 50000)); // Sleep between 1 and 50 milliseconds.
 156                      $iterations++;
 157                      if ($iterations > $maxiterations) {
 158                          // BOOM! We've exceeded the maximum number of iterations we want to block for.
 159                          throw new cache_exception('ex_unabletolock');
 160                      }
 161                  }
 162              }
 163  
 164              return false;
 165          } else {
 166              // We have the lock.
 167              fclose($result);
 168              $this->locks[$key] = $lockfile;
 169              return true;
 170          }
 171      }
 172  
 173      /**
 174       * Releases an acquired lock.
 175       *
 176       * For more details see {@link cache_lock::unlock()}
 177       *
 178       * @param string $key
 179       * @param string $ownerid A unique identifier for the owner of this lock. Not used by default.
 180       * @param bool $forceunlock If set to true the lock will be removed if it exists regardless of whether or not we own it.
 181       * @return bool
 182       */
 183      public function unlock($key, $ownerid, $forceunlock = false) {
 184          if (array_key_exists($key, $this->locks)) {
 185              @unlink($this->locks[$key]);
 186              unset($this->locks[$key]);
 187              return true;
 188          } else if ($forceunlock) {
 189              $lockfile = $this->get_lock_file($key);
 190              if (file_exists($lockfile)) {
 191                  @unlink($lockfile);
 192              }
 193              return true;
 194          }
 195          // You cannot unlock a file you didn't lock.
 196          return false;
 197      }
 198  
 199      /**
 200       * Checks if the given key is locked.
 201       *
 202       * @param string $key
 203       * @param string $ownerid
 204       */
 205      public function check_state($key, $ownerid) {
 206          if (array_key_exists($key, $this->locks)) {
 207              // The key is locked and we own it.
 208              return true;
 209          }
 210          $lockfile = $this->get_lock_file($key);
 211          if (file_exists($lockfile)) {
 212              // The key is locked and we don't own it.
 213              return false;
 214          }
 215          return null;
 216      }
 217  
 218      /**
 219       * Gets the name to use for a lock file.
 220       *
 221       * @param string $key
 222       * @return string
 223       */
 224      protected function get_lock_file($key) {
 225          return $this->cachedir.'/'. $key .'.lock';
 226      }
 227  
 228      /**
 229       * Cleans up the instance what it is no longer needed.
 230       */
 231      public function __destruct() {
 232          foreach ($this->locks as $lockfile) {
 233              // Naught, naughty developers.
 234              @unlink($lockfile);
 235          }
 236      }
 237  }