MDL-25290 cache: Added cache locking plugin and converted locking implementations...
authorSam Hemelryk <sam@moodle.com>
Mon, 17 Sep 2012 23:22:40 +0000 (11:22 +1200)
committerSam Hemelryk <sam@moodle.com>
Sun, 7 Oct 2012 20:53:51 +0000 (09:53 +1300)
20 files changed:
cache/classes/config.php
cache/classes/dummystore.php
cache/classes/factory.php
cache/classes/helper.php
cache/classes/interfaces.php
cache/classes/loaders.php
cache/lib.php
cache/locallib.php
cache/locks/file/lang/en/cachelock_file.php [new file with mode: 0644]
cache/locks/file/lib.php [moved from cache/classes/lock.php with 59% similarity]
cache/stores/file/lib.php
cache/stores/memcache/lib.php
cache/stores/memcached/lib.php
cache/stores/mongodb/lib.php
cache/stores/session/lib.php
cache/stores/static/lib.php
cache/tests/cache_test.php
lang/en/cache.php
lib/moodlelib.php
lib/phpunit/classes/util.php

index be09cdc..d45b84e 100644 (file)
@@ -65,6 +65,12 @@ class cache_config {
      */
     protected $configdefinitionmappings = array();
 
+    /**
+     * An array of configured cache lock instances.
+     * @var array
+     */
+    protected $configlocks = array();
+
     /**
      * Please use cache_config::instance to get an instance of the cache config that is ready to be used.
      */
@@ -114,8 +120,32 @@ class cache_config {
 
         $this->configstores = array();
         $this->configdefinitions = array();
+        $this->configlocks = array();
         $this->configmodemappings = array();
         $this->configdefinitionmappings = array();
+        $this->configlockmappings = array();
+
+        // Filter the lock instances
+        $defaultlock = null;
+        foreach ($configuration['locks'] as $conf) {
+            if (!is_array($conf)) {
+                // Something is very wrong here.
+                continue;
+            }
+            if (!array_key_exists('name', $conf)) {
+                // Not a valid definition configuration
+                continue;
+            }
+            $name = $conf['name'];
+            if (array_key_exists($name, $this->configlocks)) {
+                debugging('Duplicate cache lock detected. This should never happen.', DEBUG_DEVELOPER);
+                continue;
+            }
+            if ($defaultlock === null || !empty($this->configlocks['default'])) {
+                $defaultlock = $name;
+            }
+            $this->configlocks[$name] = $conf;
+        }
 
         // Filter the stores
         $availableplugins = cache_helper::early_get_cache_plugins();
@@ -151,6 +181,10 @@ class cache_config {
             }
             $store['class'] = $class;
             $store['default'] = !empty($store['default']);
+            if (!array_key_exists('lock', $store) || !array_key_exists($this->configlocks, $store['lock'])) {
+                $store['lock'] = $defaultlock;
+            }
+
             $this->configstores[$store['name']] = $store;
         }
 
@@ -256,6 +290,9 @@ class cache_config {
         if (!array_key_exists('definitionmappings', $configuration) || !is_array($configuration['definitionmappings'])) {
             $configuration['definitionmappings'] = array();
         }
+        if (!array_key_exists('locks', $configuration) || !is_array($configuration['locks'])) {
+            $configuration['locks'] = array();
+        }
 
         return $configuration;
     }
@@ -394,4 +431,33 @@ class cache_config {
     public function get_definition_mappings() {
         return $this->configdefinitionmappings;
     }
+
+    /**
+     * Returns an array of the configured locks.
+     * @return array
+     */
+    public function get_locks() {
+        return $this->configlocks;
+    }
+
+    /**
+     * Returns the lock store configuration to use with a given store.
+     * @param string $storename
+     * @return array
+     * @throws cache_exception
+     */
+    public function get_lock_for_store($storename) {
+        if (array_key_exists($storename, $this->configstores)) {
+            if (array_key_exists($this->configstores[$storename]['lock'], $this->configlocks)) {
+                $lock = $this->configstores[$storename]['lock'];
+                return $this->configlocks[$lock];
+            }
+        }
+        foreach ($this->configlocks as $lockconf) {
+            if (!empty($lockconf['default'])) {
+                return $lockconf;
+            }
+        }
+        throw new cache_exception('ex_nodefaultlock');
+    }
 }
\ No newline at end of file
index ca6a806..2d691a8 100644 (file)
@@ -266,4 +266,12 @@ class cachestore_dummy implements cache_store {
         $cache->initialise($definition);
         return $cache;;
     }
+
+    /**
+     * Returns the name of this instance.
+     * @return string
+     */
+    public function my_name() {
+        return $this->name;
+    }
 }
\ No newline at end of file
index 7fc6983..be123d0 100644 (file)
@@ -76,6 +76,12 @@ class cache_factory {
      */
     protected $definitions = array();
 
+    /**
+     * An array of lock plugins.
+     * @var array
+     */
+    protected $lockplugins = null;
+
     /**
      * Returns an instance of the cache_factor method.
      *
@@ -106,6 +112,7 @@ class cache_factory {
         $factory->stores = array();
         $factory->configs = array();
         $factory->definitions = array();
+        $factory->lockplugins = null; // MUST be null in order to force its regeneration.
     }
 
     /**
@@ -165,7 +172,7 @@ class cache_factory {
     }
 
     /**
-     * Common protected method to create a cache instance given a definition.
+     * Common public method to create a cache instance given a definition.
      *
      * This is used by the static make methods.
      *
@@ -304,4 +311,29 @@ class cache_factory {
         $store->initialise($definition);
         return $store;
     }
+
+    /**
+     * Returns a lock instance ready for use.
+     *
+     * @param array $config
+     * @return cache_lock_interface
+     */
+    public function create_lock_instance(array $config) {
+        if (!array_key_exists('name', $config) || !array_key_exists('type', $config)) {
+            throw new coding_exception('Invalid cache lock instance provided');
+        }
+        $name = $config['name'];
+        $type = $config['type'];
+        unset($config['name']);
+        unset($config['type']);
+
+        if ($this->lockplugins === null) {
+            $this->lockplugins = get_plugin_list_with_class('cachelock', '', 'lib.php');
+        }
+        if (!array_key_exists($type, $this->lockplugins)) {
+            throw new coding_exception('Invalid cache lock type.');
+        }
+        $class = $this->lockplugins[$type];
+        return new $class($name, $config);
+    }
 }
\ No newline at end of file
index 1890172..7f93660 100644 (file)
@@ -151,21 +151,16 @@ class cache_helper {
     }
 
     /**
-     * Returns the cache store to be used for locking or false if there is not one.
-     * @return cache_store|boolean
+     * Returns a cache_lock instance suitable for use with the store.
+     *
+     * @param cache_store $store
+     * @return cache_lock_interface
      */
-    public static function get_cachestore_for_locking() {
-        $factory = cache_factory::instance();
-        $definition = $factory->create_definition('core', 'locking');
+    public static function get_cachelock_for_store(cache_store $store) {
         $instance = cache_config::instance();
-        $stores = $instance->get_stores_for_definition($definition);
-        foreach ($stores as $name => $details) {
-            if ($details['useforlocking']) {
-                $instances = self::initialise_cachestore_instances(array($name => $details), $definition);
-                return reset($instances);
-            }
-        }
-        return false;
+        $lockconf = $instance->get_lock_for_store($store->my_name());
+        $factory = cache_factory::instance();
+        return $factory->create_lock_instance($lockconf);
     }
 
     /**
@@ -387,6 +382,10 @@ class cache_helper {
 
     /**
      * Purge all of the cache stores of all of their data.
+     *
+     * Think twice before calling this method. It will purge **ALL** caches regardless of whether they have been used recently or
+     * anything. This will involve full setup of the cache + the purge operation. On a site using caching heavily this WILL be
+     * painful.
      */
     public static function purge_all() {
         $config = cache_config::instance();
index 44b5d6b..1f96fa0 100644 (file)
@@ -214,7 +214,7 @@ interface cache_loader_with_locking {
      * @return bool True if this code has the lock, false if there is a lock but this code doesn't have it,
      *      null if there is no lock.
      */
-    public function has_lock($key);
+    public function check_lock_state($key);
 
     /**
      * Releases the lock for the given key.
@@ -347,6 +347,12 @@ interface cache_store {
      */
     public function __construct($name, array $configuration = array());
 
+    /**
+     * Returns the name of this store instance.
+     * @return string
+     */
+    public function my_name();
+
     /**
      * Initialises a new instance of the cache store given the definition the instance is to be used for.
      *
@@ -459,29 +465,31 @@ interface cache_is_lockable {
      * Acquires a lock on the given key for the given identifier.
      *
      * @param string $key The key we are locking.
-     * @param string $identifier The identifier so we can check if we have the lock or if it is someone else.
+     * @param string $ownerid The identifier so we can check if we have the lock or if it is someone else.
+     *      The use of this property is entirely optional and implementations can act as they like upon it.
      * @return bool True if the lock could be acquired, false otherwise.
      */
-    public function acquire_lock($key, $identifier);
+    public function acquire_lock($key, $ownerid);
 
     /**
      * Test if there is already a lock for the given key and if there is whether it belongs to the calling code.
      *
      * @param string $key The key we are locking.
-     * @param string $identifier The identifier so we can check if we have the lock or if it is someone else.
+     * @param string $ownerid The identifier so we can check if we have the lock or if it is someone else.
      * @return bool True if this code has the lock, false if there is a lock but this code doesn't have it, null if there
      *      is no lock.
      */
-    public function has_lock($key, $identifier);
+    public function check_lock_state($key, $ownerid);
 
     /**
      * Releases the lock on the given key.
      *
      * @param string $key The key we are locking.
-     * @param string $identifier The identifier so we can check if we have the lock or if it is someone else.
+     * @param string $ownerid The identifier so we can check if we have the lock or if it is someone else.
+     *      The use of this property is entirely optional and implementations can act as they like upon it.
      * @return bool True if the lock has been released, false if there was a problem releasing the lock.
      */
-    public function release_lock($key, $identifier);
+    public function release_lock($key, $ownerid);
 }
 
 /**
@@ -623,3 +631,62 @@ interface cacheable_object {
      */
     public static function wake_from_cache($data);
 }
+
+/**
+ * Cache lock interface
+ *
+ * This interface needs to be inherited by all cache lock plugins.
+ */
+interface cache_lock_interface {
+    /**
+     * Constructs an instance of the cache lock given its name and its configuration data
+     *
+     * @param string $name The unique name of the lock instance
+     * @param array $configuration
+     */
+    public function __construct($name, array $configuration = array());
+
+    /**
+     * Acquires a lock on a given key.
+     *
+     * @param string $key The key to acquire a lock for.
+     * @param string $ownerid An unique identifier for the owner of this lock. It is entirely optional for the cache lock plugin
+     *      to use this. Each implementation can decide for themselves.
+     * @param bool $block If set to true the application will wait until a lock can be acquired
+     * @return bool True if the lock can be acquired false otherwise.
+     */
+    public function lock($key, $ownerid, $block = false);
+
+    /**
+     * Releases the lock held on a certain key.
+     *
+     * @param string $key The key to release the lock for.
+     * @param string $ownerid An unique identifier for the owner of this lock. It is entirely optional for the cache lock plugin
+     *      to use this. Each implementation can decide for themselves.
+     * @param bool $forceunlock If set to true the lock will be removed if it exists regardless of whether or not we own it.
+     */
+    public function unlock($key, $ownerid, $forceunlock = false);
+
+    /**
+     * Checks the state of the given key.
+     *
+     * Returns true if the key is locked and belongs to the ownerid.
+     * Returns false if the key is locked but does not belong to the ownerid.
+     * Returns null if there is no lock
+     *
+     * @param string $key The key we are checking for.
+     * @param string $ownerid The identifier so we can check if we have the lock or if it is someone else.
+     * @return bool True if this code has the lock, false if there is a lock but this code doesn't have it, null if there
+     *      is no lock.
+     */
+    public function check_state($key, $ownerid);
+
+    /**
+     * Cleans up any left over locks.
+     *
+     * This function MUST clean up any locks that have been acquired and not released during processing.
+     * Although the situation of acquiring a lock and not releasing it should be insanely rare we need to deal with it.
+     * Things such as unfortunate timeouts etc could cause this situation.
+     */
+    public function __destruct();
+}
\ No newline at end of file
index 299b789..205872b 100644 (file)
@@ -941,9 +941,9 @@ class cache_application extends cache implements cache_loader_with_locking {
 
     /**
      * Gets set to a cache_store to use for locking if the caches primary store doesn't support locking natively.
-     * @var cache_store
+     * @var cache_lock_interface
      */
-    protected $lockstore;
+    protected $cachelockinstance;
 
     /**
      * Overrides the cache construct method.
@@ -1038,8 +1038,8 @@ class cache_application extends cache implements cache_loader_with_locking {
         if ($this->nativelocking) {
             return $this->get_store()->acquire_lock($key, $this->get_identifier());
         } else {
-            $this->ensure_lock_store_available();
-            return $this->lockstore->acquire_lock($key, $this->get_identifier());
+            $this->ensure_cachelock_available();
+            return $this->cachelockinstance->lock($key, $this->get_identifier());
         }
     }
 
@@ -1050,13 +1050,13 @@ class cache_application extends cache implements cache_loader_with_locking {
      * @return bool|null Returns true if there is a lock and this cache has it, null if no one has a lock on that key, false if
      *      someone else has the lock.
      */
-    public function has_lock($key) {
+    public function check_lock_state($key) {
         $key = $this->parse_key($key);
         if ($this->nativelocking) {
-            return $this->get_store()->has_lock($key, $this->get_identifier());
+            return $this->get_store()->check_lock_state($key, $this->get_identifier());
         } else {
-            $this->ensure_lock_store_available();
-            return $this->lockstore->has_lock($key, $this->get_identifier());
+            $this->ensure_cachelock_available();
+            return $this->cachelockinstance->check_state($key, $this->get_identifier());
         }
     }
 
@@ -1071,8 +1071,8 @@ class cache_application extends cache implements cache_loader_with_locking {
         if ($this->nativelocking) {
             return $this->get_store()->release_lock($key, $this->get_identifier());
         } else {
-            $this->ensure_lock_store_available();
-            return $this->lockstore->release_lock($key, $this->get_identifier());
+            $this->ensure_cachelock_available();
+            return $this->cachelockinstance->unlock($key, $this->get_identifier());
         }
     }
 
@@ -1081,9 +1081,9 @@ class cache_application extends cache implements cache_loader_with_locking {
      *
      * This should only happen if the cache store doesn't natively support it.
      */
-    protected function ensure_lock_store_available() {
-        if ($this->lockstore === null) {
-            $this->lockstore = cache_helper::get_cachestore_for_locking();
+    protected function ensure_cachelock_available() {
+        if ($this->cachelockinstance === null) {
+            $this->cachelockinstance = cache_helper::get_cachelock_for_store($this->get_store());
         }
     }
 
@@ -1168,7 +1168,7 @@ class cache_application extends cache implements cache_loader_with_locking {
      * @throws moodle_exception
      */
     public function get($key, $strictness = IGNORE_MISSING) {
-        if ($this->requirelockingread && $this->has_lock($key) === false) {
+        if ($this->requirelockingread && $this->check_lock_state($key) === false) {
             // Read locking required and someone else has the read lock.
             return false;
         }
index 059993d..0f48b68 100644 (file)
@@ -31,7 +31,6 @@
 defined('MOODLE_INTERNAL') || die();
 
 // Include the required classes.
-require_once($CFG->dirroot.'/cache/classes/lock.php');
 require_once($CFG->dirroot.'/cache/classes/interfaces.php');
 require_once($CFG->dirroot.'/cache/classes/config.php');
 require_once($CFG->dirroot.'/cache/classes/helper.php');
index 8c4bf59..add82cd 100644 (file)
@@ -74,16 +74,22 @@ class cache_config_writer extends cache_config {
         $configuration['modemappings'] = $this->configmodemappings;
         $configuration['definitions'] = $this->configdefinitions;
         $configuration['definitionmappings'] = $this->configdefinitionmappings;
+        $configuration['locks'] = $this->configlocks;
 
         // Prepare the file content.
         $content = "<?php defined('MOODLE_INTERNAL') || die();\n \$configuration = ".var_export($configuration, true).";";
 
-        if (cache_lock::lock('config', false)) {
+        // We need to create a temporary cache lock instance for use here. Remember we are generating the config file
+        // it doesn't exist and thus we can't use the normal API for this (it'll just try to use config).
+        $factory = cache_factory::instance();
+        $locking = $factory->create_lock_instance(reset($this->configlocks));
+        if ($locking->lock('configwrite', 'config', true)) {
+            // Its safe to use w mode here because we have already acquired the lock.
             $handle = fopen($cachefile, 'w');
             fwrite($handle, $content);
             fflush($handle);
             fclose($handle);
-            cache_lock::unlock('config');
+            $locking->unlock('configwrite', 'config');
         } else {
             throw new cache_exception('ex_configcannotsave', 'cache', '', null, 'Unable to open the cache config file.');
         }
@@ -343,6 +349,13 @@ class cache_config_writer extends cache_config {
                 'sort' => -1
             )
         );
+        $writer->configlocks = array(
+            'default_file_lock' => array(
+                'name' => 'default_file_lock',
+                'type' => 'cachelock_file',
+                'dir' => 'filelocks'
+            )
+        );
         $writer->config_save();
     }
 
diff --git a/cache/locks/file/lang/en/cachelock_file.php b/cache/locks/file/lang/en/cachelock_file.php
new file mode 100644 (file)
index 0000000..b57188c
--- /dev/null
@@ -0,0 +1,26 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Strings for the cache file locking plugin
+ *
+ * @package    cachelock_file
+ * @category   cache
+ * @copyright  2012 Sam Hemelryk
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string['pluginname'] = 'File locking';
\ No newline at end of file
similarity index 59%
rename from cache/classes/lock.php
rename to cache/locks/file/lib.php
index 4c8e4f7..7b25f09 100644 (file)
@@ -15,9 +15,9 @@
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * Cache lock class. Used for locking when required.
+ * File locking for the Cache API
  *
- * @package    core
+ * @package    cachelock_file
  * @category   cache
  * @copyright  2012 Sam Hemelryk
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 defined('MOODLE_INTERNAL') || die();
 
 /**
- * The cache lock class.
+ * File locking plugin
  *
- * This class is used for acquiring and releasing locks.
- * We use this rather than flock because we can be sure this is cross-platform compatible and thread/process safe.
- *
- * This class uses the files for locking. It relies on fopens x mode which is documented as follows:
- *
- *    Create and open for writing only; place the file pointer at the beginning of the file. If the file already exists, the
- *    fopen() call will fail by returning FALSE and generating an error of level E_WARNING.
- *    http://www.php.net/manual/en/function.fopen.php
- *
- * Through this we can attempt to call fopen using a lock file name. If the fopen call succeeds we can be sure we have created the
- * file and thus ascertained the lock, otherwise fopen fails and we can look at what to do next.
- *
- * All interaction with this class is handled through its two public static methods, lock and unlock.
- * Internally an instance is generated and used for locking and unlocking. It records the locks used during this session and on
- * destruction cleans up any left over locks.
- * Of course the clean up is just a safe-guard. Really no one should EVER leave a lock and rely on the clean up.
- *
- * Because this lock system uses files for locking really its probably not ideal, but as I could not think of a better cross
- * platform thread safe system it is what we have ended up with.
- *
- * This system also allows us to lock a file before it is created because it doesn't rely on flock.
- *
- * @package    core
- * @category   cache
  * @copyright  2012 Sam Hemelryk
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class cache_lock {
-
-    /**
-     * Acquire a lock.
-     *
-     * If the lock can be acquired:
-     *      This function will return true.
-     *
-     * If the lock cannot be acquired the result of this method is determined by the block param:
-     *      $block = true (default)
-     *          The function will block any further execution unti the lock can be acquired.
-     *          This involves the function attempting to acquire the lock and the sleeping for a period of time. This process
-     *          will be repeated until the lock is required or until a limit is hit (100 by default) in which case a cache
-     *          exception will be thrown.
-     *      $block = false
-     *          The function will return false immediately.
-     *
-     * If a max life has been specified and the lock can not be acquired then the lock file will be checked against this time.
-     * In the case that the file exceeds that max time it will be forcefully deleted.
-     * 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
-     * we can be as sure as possible that the executing code has completed.
-     *
-     * @param string $key The key that we want to lock
-     * @param bool $block True if we want the program block further execution until the lock has been acquired.
-     * @param int $maxlife A maximum life for the block file if there should be one. Read the note in the function description
-     *      before using this param.
-     * @return bool
-     * @throws cache_exception If block is set to true and more than 100 attempts have been made to acquire a lock.
-     */
-    public static function lock($key, $block = true, $maxlife = null) {
-        $key = md5($key);
-        $instance = self::instance();
-        return $instance->_lock($key, $block, $maxlife);
-    }
+class cachelock_file implements cache_lock_interface {
 
     /**
-     * Releases a lock that has been acquired.
-     *
-     * This function can only be used to release locks you have acquired. If you didn't acquire the lock you can't release it.
-     *
-     * @param string $key
-     * @return bool
+     * The name of the cache lock instance
+     * @var string
      */
-    public static function unlock($key) {
-        $key = md5($key);
-        $instance = self::instance();
-        return $instance->_unlock($key);
-    }
+    protected $name;
 
     /**
-     * Resets the cache lock class, reinitialising it.
+     * The absolute directory in which lock files will be created and looked for.
+     * @var string
      */
-    public static function reset() {
-        self::instance(true);
-    }
+    protected $cachedir;
 
     /**
-     * Returns an instance of the cache lock class.
-     *
-     * @staticvar bool $instance
-     * @param bool $forceregeneration
-     * @return cache_lock
+     * The maximum life in seconds for a lock file. By default null for none.
+     * @var int|null
      */
-    protected static function instance($forceregeneration = false) {
-        static $instance = false;
-        if (!$instance || $forceregeneration) {
-            $instance = new cache_lock();
-        }
-        return $instance;
-    }
+    protected $maxlife = null;
 
     /**
-     * The directory in which lock files will be created
-     * @var string
+     * The number of attempts to acquire a lock when blocking is required before throwing an exception.
+     * @var int
      */
-    protected $cachedir;
+    protected $blockattempts = 100;
 
     /**
-     * An array of lock files currently held by this cache lock instance.
-     * @var array
+     * An array containing the locks that have been acquired but not released so far.
+     * @var array Array of key => lock file path
      */
     protected $locks = array();
 
     /**
-     * Constructs this cache lock instance.
-     */
-    protected function __construct() {
-        $this->cachedir = make_cache_directory('cachelock');
-    }
-
-    /**
-     * Cleans up the instance what it is no longer needed.
+     * Initialises the cache lock instance.
+     *
+     * @param string $name The name of the cache lock
+     * @param array $configuration
      */
-    public function __destruct() {
-        foreach ($this->locks as $lockfile) {
-            // Naught, naughty developers.
-            @unlink($lockfile);
+    public function __construct($name, array $configuration = array()) {
+        $this->name = $name;
+        if (!array_key_exists('dir', $configuration)) {
+            $this->cachedir = make_cache_directory(md5($name));
+        } else {
+            $dir = $configuration['dir'];
+            if (strpos($dir, '/') !== false && strpos($dir, '.') !== 0) {
+                // This looks like an absolute path.
+                if (file_exists($dir) && is_dir($dir) && is_writable($dir)) {
+                    $this->cachedir = $dir;
+                }
+            }
+            if (empty($this->cachedir)) {
+                $dir = preg_replace('#[^a-zA-Z0-9_]#', '_', $dir);
+                $this->cachedir = make_cache_directory($dir);
+            }
+        }
+        if (array_key_exists('maxlife', $configuration) && is_number($configuration['maxlife'])) {
+            $maxlife = (int)$configuration['maxlife'];
+            // Minimum lock time is 60 seconds.
+            $this->maxlife = max($maxlife, 60);
+        }
+        if (array_key_exists('blockattempts', $configuration) && is_number($configuration['blockattempts'])) {
+            $this->blockattempts = (int)$configuration['blockattempts'];
         }
     }
 
     /**
-     * Acquires a lock, of dies trying (jokes).
+     * Acquire a lock.
      *
-     * Read {@link cache_lock::lock()} for full details.
+     * If the lock can be acquired:
+     *      This function will return true.
      *
-     * @param string $key
-     * @param bool $block
-     * @param int|null $maxlife
+     * If the lock cannot be acquired the result of this method is determined by the block param:
+     *      $block = true (default)
+     *          The function will block any further execution unti the lock can be acquired.
+     *          This involves the function attempting to acquire the lock and the sleeping for a period of time. This process
+     *          will be repeated until the lock is required or until a limit is hit (100 by default) in which case a cache
+     *          exception will be thrown.
+     *      $block = false
+     *          The function will return false immediately.
+     *
+     * If a max life has been specified and the lock can not be acquired then the lock file will be checked against this time.
+     * In the case that the file exceeds that max time it will be forcefully deleted.
+     * 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
+     * we can be as sure as possible that the executing code has completed.
+     *
+     * @param string $key The key that we want to lock
+     * @param string $ownerid A unique identifier for the owner of this lock. Not used by default.
+     * @param bool $block True if we want the program block further execution until the lock has been acquired.
      * @return bool
-     * @throws cache_exception
+     * @throws cache_exception If block is set to true and more than 100 attempts have been made to acquire a lock.
      */
-    protected function _lock($key, $block = true, $maxlife = null) {
+    public function lock($key, $ownerid, $block = false) {
         // Get the name of the lock file we want to use.
         $lockfile = $this->get_lock_file($key);
 
@@ -179,11 +135,11 @@ class cache_lock {
         // Check if we could create the file or not.
         if ($result === false) {
             // Lock exists already.
-            if ($maxlife !== null) {
+            if ($this->maxlife !== null && !array_key_exists($key, $this->locks)) {
                 $mtime = filemtime($lockfile);
-                if ($mtime < time() - $maxlife) {
-                    $this->_unlock($key, true);
-                    $result = $this->_lock($key, false);
+                if ($mtime < time() - $this->maxlife) {
+                    $this->unlock($key, true);
+                    $result = $this->lock($key, false);
                     if ($result) {
                         return true;
                     }
@@ -192,8 +148,8 @@ class cache_lock {
             if ($block) {
                 // OK we are blocking. We had better sleep and then retry to lock.
                 $iterations = 0;
-                $maxiterations = 100;
-                while (($result = $this->_lock($key, false)) === false) {
+                $maxiterations = $this->blockattempts;
+                while (($result = $this->lock($key, false)) === false) {
                     // usleep causes the application to cleep to x microseconds.
                     // Before anyone asks there are 1'000'000 microseconds to a second.
                     usleep(rand(1000, 50000)); // Sleep between 1 and 50 milliseconds
@@ -220,10 +176,11 @@ class cache_lock {
      * For more details see {@link cache_lock::unlock()}
      *
      * @param string $key
+     * @param string $ownerid A unique identifier for the owner of this lock. Not used by default.
      * @param bool $forceunlock If set to true the lock will be removed if it exists regardless of whether or not we own it.
      * @return bool
      */
-    protected function _unlock($key, $forceunlock = false) {
+    public function unlock($key, $ownerid, $forceunlock = false) {
         if (array_key_exists($key, $this->locks)) {
             @unlink($this->locks[$key]);
             unset($this->locks[$key]);
@@ -239,6 +196,25 @@ class cache_lock {
         return false;
     }
 
+    /**
+     * Checks if the given key is locked.
+     *
+     * @param string $key
+     * @param string $ownerid
+     */
+    public function check_state($key, $ownerid) {
+        if (key_exists($key, $this->locks)) {
+            // The key is locked and we own it.
+            return true;
+        }
+        $lockfile = $this->get_lock_file($key);
+        if (file_exists($lockfile)) {
+            // The key is locked and we don't own it.
+            return false;
+        }
+        return null;
+    }
+
     /**
      * Gets the name to use for a lock file.
      *
@@ -248,4 +224,14 @@ class cache_lock {
     protected function get_lock_file($key) {
         return $this->cachedir.'/'. $key .'.lock';
     }
+
+    /**
+     * Cleans up the instance what it is no longer needed.
+     */
+    public function __destruct() {
+        foreach ($this->locks as $lockfile) {
+            // Naught, naughty developers.
+            @unlink($lockfile);
+        }
+    }
 }
\ No newline at end of file
index c124948..f07d9ed 100644 (file)
@@ -37,7 +37,7 @@
  * @copyright  2012 Sam Hemelryk
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class cachestore_file implements cache_store, cache_is_lockable, cache_is_key_aware {
+class cachestore_file implements cache_store, cache_is_key_aware {
 
     /**
      * The name of the store.
@@ -81,12 +81,6 @@ class cachestore_file implements cache_store, cache_is_lockable, cache_is_key_aw
      */
     protected $isready = false;
 
-    /**
-     * An array containing the locks this store instance owns presently.
-     * @var array
-     */
-    protected $locks = array();
-
     /**
      * The cache definition this instance has been initialised with.
      * @var cache_definition
@@ -447,81 +441,6 @@ class cachestore_file implements cache_store, cache_is_lockable, cache_is_key_aw
         }
         return false;
     }
-
-    /**
-     * Acquires a lock for the key with the given identifier.
-     *
-     * @param string $key The key to acquire a lock for.
-     * @param string $identifier The identifier who will own the lock.
-     * @return bool True if the lock could be acquired, false otherwise.
-     */
-    public function acquire_lock($key, $identifier) {
-        if (array_key_exists($key, $this->locks) && $this->locks[$key] == $identifier) {
-            // We already have the lock, return true.
-            return true;
-        }
-        $result = cache_lock::lock($key, false);
-        if ($result) {
-            $this->locks[$key] = $identifier;
-        }
-        return $result;
-    }
-    
-    /**
-     * Releases the lock provided it belongs to the identifier.
-     *
-     * @param string $key The key to the lock is for.
-     * @param string $identifier The identifier of the caller.
-     * @return bool True if the lock has been released, false if there was a problem releasing the lock.
-     */
-    public function release_lock($key, $identifier) {
-        if (array_key_exists($key, $this->locks) && $this->locks[$key] == $identifier) {
-            $outcome = cache_lock::unlock($key);
-            return $outcome;
-        }
-        return false;
-    }
-    
-    /**
-     * Returns true if the given key has a lock and it belongs to the identifier.
-     *
-     * @param string $key The key to the lock is for.
-     * @param string $identifier The identifier of the caller.
-     * @return bool True if this code has the lock, false if there is a lock but this code doesn't have it, null if there is no lock.
-     */
-    public function has_lock($key, $identifier) {
-        return (array_key_exists($key, $this->locks) && $this->locks[$key] == $identifier);
-    }
-
-    /**
-     * Returns the path to the lock file.
-     *
-     * @param string $key
-     * @return string The absolute path to use for a lock file for this key.
-     */
-    protected function get_lock_file($key) {
-        return $this->path.'/lock-'.$key.'.lock';
-    }
-
-    /**
-     * Cleans up any left over lock files.
-     *
-     * There shouldn't be any left over lock files but clean them up just in case.
-     */
-    public function __destruct() {
-        $errors = false;
-        foreach ($this->locks as $file) {
-            try {
-                @unlink($file);
-            } catch (Exception $e) {
-                // We just want to ensure we unlink everything possible.
-                $errors = true;
-            }
-        }
-        if ($errors) {
-            error_log('ERROR ERROR ERROR!!! Unable to release all file cache store locks!');
-        }
-    }
     
     /**
      * Purges the cache deleting all items within it.
@@ -596,8 +515,7 @@ class cachestore_file implements cache_store, cache_is_lockable, cache_is_key_aw
      *
      * There are several things going on in this function to try to ensure what we don't end up with partial writes etc.
      *   1. Files for writing are opened with the mode xb, the file must be created and can not already exist.
-     *   2. We use cache_mutex to ensure we acquire a lock.
-     *   3. Renaming, data is written to a temporary file, where it can be verified using md5 and is then renamed.
+     *   2. Renaming, data is written to a temporary file, where it can be verified using md5 and is then renamed.
      *
      * @param string $file Absolute file path
      * @param string $content The content to write.
@@ -614,11 +532,6 @@ class cachestore_file implements cache_store, cache_is_lockable, cache_is_key_aw
             }
         }
 
-        // Lock the temp file before we write.
-        if (!cache_lock::lock($tempfile, false)) {
-            return false;
-        }
-
         // Open the file with mode=x. This acts to create and open the file for writing only.
         // If the file already exists this will return false.
         // We also force binary.
@@ -627,15 +540,11 @@ class cachestore_file implements cache_store, cache_is_lockable, cache_is_key_aw
             // File already exists... lock already exists, return false.
             return false;
         }
-        // We have the lock. Write our content.
         fwrite($handle, $content);
         fflush($handle);
         // Close the handle, we're done.
         fclose($handle);
 
-        // Unlock the temp file.
-        cache_lock::unlock($tempfile);
-
         if (md5_file($tempfile) !== md5($content)) {
             // The md5 of the content of the file must match the md5 of the content given to be written.
             @unlink($tempfile);
@@ -650,4 +559,12 @@ class cachestore_file implements cache_store, cache_is_lockable, cache_is_key_aw
         }
         return $result;
     }
+
+    /**
+     * Returns the name of this instance.
+     * @return string
+     */
+    public function my_name() {
+        return $this->name;
+    }
 }
\ No newline at end of file
index c21d64e..d00eb63 100644 (file)
@@ -362,4 +362,12 @@ class cachestore_memcache implements cache_store {
 
         return $store;
     }
+
+    /**
+     * Returns the name of this instance.
+     * @return string
+     */
+    public function my_name() {
+        return $this->name;
+    }
 }
\ No newline at end of file
index 7707755..0ec1300 100644 (file)
@@ -442,4 +442,12 @@ class cachestore_memcached implements cache_store {
 
         return $store;
     }
+
+    /**
+     * Returns the name of this instance.
+     * @return string
+     */
+    public function my_name() {
+        return $this->name;
+    }
 }
\ No newline at end of file
index 7a6fca2..18f17d5 100644 (file)
@@ -473,4 +473,12 @@ class cachestore_mongodb implements cache_store {
 
         return $store;
     }
+
+    /**
+     * Returns the name of this instance.
+     * @return string
+     */
+    public function my_name() {
+        return $this->name;
+    }
 }
\ No newline at end of file
index dfe205e..f73009b 100644 (file)
@@ -362,6 +362,14 @@ class cachestore_session extends session_data_store implements cache_store, cach
         $cache->initialise($definition);
         return $cache;
     }
+
+    /**
+     * Returns the name of this instance.
+     * @return string
+     */
+    public function my_name() {
+        return $this->name;
+    }
 }
 
 /**
index 2b25283..11ba2a6 100644 (file)
@@ -362,6 +362,14 @@ class cachestore_static extends static_data_store implements cache_store, cache_
         $cache->initialise($definition);
         return $cache;;
     }
+
+    /**
+     * Returns the name of this instance.
+     * @return string
+     */
+    public function my_name() {
+        return $this->name;
+    }
 }
 
 /**
index 810767d..9035230 100644 (file)
@@ -324,8 +324,8 @@ class cache_phpunit_tests extends advanced_testcase {
         $this->assertTrue($cache1->acquire_lock('testkey'));
         $this->assertFalse($cache2->acquire_lock('testkey'));
 
-        $this->assertTrue($cache1->has_lock('testkey'));
-        $this->assertFalse($cache2->has_lock('testkey'));
+        $this->assertTrue($cache1->check_lock_state('testkey'));
+        $this->assertFalse($cache2->check_lock_state('testkey'));
 
         $this->assertTrue($cache1->release_lock('testkey'));
         $this->assertFalse($cache2->release_lock('testkey'));
index 41398c2..7ffc742 100644 (file)
@@ -35,6 +35,7 @@ $string['editstore'] = 'Edit store';
 $string['editstoresuccess'] = 'Succesfully edited the cache store.';
 $string['editdefinitionmappings'] = '{$a} definition store mappings';
 $string['ex_configcannotsave'] = 'Unable to save the cache config to file.';
+$string['ex_nodefaultlock'] = 'Unable to find a default lock instance.';
 $string['ex_unabletolock'] = 'Unable to acquire a lock for caching.';
 $string['gethit'] = 'Get - Hit';
 $string['getmiss'] = 'Get - Miss';
index 3686a13..ff9ecad 100644 (file)
@@ -8011,7 +8011,8 @@ function get_plugin_types($fullpaths=true) {
                       'qformat'       => 'question/format',
                       'plagiarism'    => 'plagiarism',
                       'tool'          => $CFG->admin.'/tool',
-                      'cachestore'         => 'cache/stores',
+                      'cachestore'    => 'cache/stores',
+                      'cachelock'     => 'cache/locks',
                       'theme'         => 'theme',  // this is a bit hacky, themes may be in $CFG->themedir too
         );
 
index 5d23173..a4b3d9f 100644 (file)
@@ -534,8 +534,11 @@ class phpunit_util {
         make_cache_directory('');
         make_cache_directory('htmlpurifier');
         // Reset the cache API so that it recreates it's required directories as well.
-        cache_lock::reset();
         cache_factory::reset();
+        // Purge all data from the caches. This is required for consistency.
+        // Any file caches that happened to be within the data root will have already been clearer (because we just deleted cache)
+        // and now we will purge any other caches as well.
+        cache_helper::purge_all();
     }
 
     /**