From 8139ad13a5a3bf0eaecf84d94b92fc0188b86262 Mon Sep 17 00:00:00 2001 From: Sam Hemelryk Date: Mon, 10 Sep 2012 15:26:33 +1200 Subject: [PATCH] MDL-25290 cache: Implemented Cache API aka MUC --- cache/README.md | 207 ++++++ cache/admin.php | 183 +++++ cache/classes/config.php | 397 ++++++++++ cache/classes/definition.php | 715 ++++++++++++++++++ cache/classes/dummystore.php | 269 +++++++ cache/classes/factory.php | 307 ++++++++ cache/classes/helper.php | 432 +++++++++++ cache/classes/interfaces.php | 625 ++++++++++++++++ cache/classes/loaders.php | 1355 ++++++++++++++++++++++++++++++++++ cache/classes/lock.php | 250 +++++++ cache/forms.php | 200 +++++ cache/lib.php | 141 ++++ cache/locallib.php | 853 +++++++++++++++++++++ cache/renderer.php | 285 +++++++ cache/testperformance.php | 202 +++++ cache/tests/cache_test.php | 529 +++++++++++++ cache/tests/fixtures/lib.php | 146 ++++ 17 files changed, 7096 insertions(+) create mode 100644 cache/README.md create mode 100644 cache/admin.php create mode 100644 cache/classes/config.php create mode 100644 cache/classes/definition.php create mode 100644 cache/classes/dummystore.php create mode 100644 cache/classes/factory.php create mode 100644 cache/classes/helper.php create mode 100644 cache/classes/interfaces.php create mode 100644 cache/classes/loaders.php create mode 100644 cache/classes/lock.php create mode 100644 cache/forms.php create mode 100644 cache/lib.php create mode 100644 cache/locallib.php create mode 100644 cache/renderer.php create mode 100644 cache/testperformance.php create mode 100644 cache/tests/cache_test.php create mode 100644 cache/tests/fixtures/lib.php diff --git a/cache/README.md b/cache/README.md new file mode 100644 index 00000000000..8317cb91de4 --- /dev/null +++ b/cache/README.md @@ -0,0 +1,207 @@ +MUC development code +==================== + +Congratulations you've found the MUC development code. +This code is still very much in development and as such is not (and is know to not) function correctly or completely at the moment. +Of course that will all be well and truly sorted out WELL before this gets integrated. + +Sample code snippets +-------------------- + +A definition: + + $definitions = array( + 'core_string' => array( // Required, unique + 'mode' => cache_store::MODE_APPLICATION, // Required + 'component' => 'core', // Required + 'area' => 'string', // Required + 'requireidentifiers' => array( // Optional + 'lang', + 'component' + ), + 'requiredataguarantee' => false, // Optional + 'requiremultipleidentifiers' => false, // Optional + 'overrideclass' => null, // Optional + 'overrideclassfile' => null, // Optional + 'datasource' => null, // Optional + 'datasourcefile' => null, // Optional + 'persistent' => false, // Optional + 'ttl' => 0, // Optional + 'mappingsonly' => false // Optional + 'invalidationevents' => array( // Optional + 'contextmarkeddirty' + ), + ) + ); + +Getting a something from a cache using the definition: + + $cache = cache::make('core', 'string'); + if (!$component = $cache->get('component')) { + // get returns false if its not there and can't be loaded. + $component = generate_data(); + $cache->set($component); + } + +The same thing but from using params: + + $cache = cache::make_from_params(cache_store::MODE_APPLICATION, 'core', 'string'); + if (!$component = $cache->get('component')) { + // get returns false if its not there and can't be loaded. + $component = generate_data(); + $cache->set($component); + } + +If a data source had been specified in the definition the following would be all that was needed. + + $cache = cache::make('core', 'string'); + $component = $cache->get('component'); + +The bits that make up the cache API +----------------------------------- + +There are several parts that _**will**_ make up this solution: + +### Loader +The loader is central to the whole thing. +It is used by the end developer to get an object that handles caching. +90% of end developers will not need to know or use anything else about the cache API. +In order to get a loader you must use one of two static methods, make, or make_with_params. +To the end developer interacting with the loader is simple and is dictated by the cache_loader interface. +Internally there is lots of magic going on. The important parts to know about are: +* There are two ways to get with a loader, the first with a definition (discussed below) the second with params. When params are used they are turned into an adhoc definition with default params. +* A loader get passed three things when being constructed, a definition, a store, and another loader or datasource if there is either. +* If a loader is the third arg then requests will be chained to provide redundancy. +* If a data source is provided then requests for an item that is not cached will be passed to the data source and that will be expected to load the data. If it loads data that data is stored in each store on its way back to the user. +* There are three core loaders. One for each application, session, and request. +* A custom loader can be used. It will be provided by the definition (thus cannot be used with adhoc definitions) and must override the appropriate core loader +* The loader handles ttl for stores that don't natively support ttl. +* The application loader handles locking for stores that don't natively support locking. + +### Store +The store is the bridge between the cache API and a cache solution. +Cache store plugins exist within moodle/cache/store. +The administrator of a site can configure multiple instances of each plugin, the configuration gets initialised as a store for the loader when required in code (during construction of the loader). +The following points highlight things you should know about stores. +* A cache_store interface is used to define the requirements of a store plugin. +* The store plugin can inherit the cache_is_lockable interface to handle its own locking. +* The store plugin can inherit the cache_is_key_aware interface to handle is own has checks. +* Store plugins inform the cache API about the things they support. Features can be required by a definition. +** Data guarantee - Data is guaranteed to exist in the cache once it is set there. It is never cleaned up to free space or because it has not been recently used. +** Multiple identifiers - Rather than a single string key, the parts that make up the key are passed as an array. +** Native TTL support - When required the store supports native ttl and doesn't require the cache API to manage ttl of things given to the store. + +### Definition +_Definitions were not a part of the previous proposal._ +Definitions are cache definitions. They will be located within a new file for each component/plugin at **db/caches.php**. +They can be used to set all of the requirements of a cache instance and are used to ensure that a cache can only be interacted with in the same way no matter where it is being used. +It also ensure that caches are easy to use, the config is stored in the definition and the developer using the cache does not need to know anything about it. +When getting a loader you can either provide a definition name, or a set or params. +* If you provide a definition name then the matching definition is found and used to construct a loader for you. +* If you provide params then an adhoc definition is created. It will have defaults and will not have any special requirements or options set. + +Definitions are designed to be used in situations where things are more than basic. + +The following settings are required for a definition: +* name - Identifies the definition and must be unique. +* mode - Application, session, request. +* component - The component associated the definition is associated with. +* area - Describes the stuff being cached. + +The following optional settings can also be defined: +* requireidentifiers - Any identifiers the definition requires. Must be provided when creating the loader. +* requiredataguarantee - If set to true then only stores that support data guarantee will be used. +* requiremultipleidentifiers - If set to true then only stores that support multiple identifiers will be used. +* overrideclass - If provided this class will be used for the loader. It must extend on of the core loader classes (based upon mode). +* overrideclassfile - Included if required when using the overrideclass param. +* datasource - If provided this class will be used as a data source for the definition. It must implement the cache_data_source interface. +* datasourcefile - Included if required when using the datasource param. +* persistent - If set to true the loader will be stored when first created and provided to subsequent requests. More on this later. +* ttl - Can be used to set a ttl value for data being set for this cache. +* mappingsonly - This definition can only be used if there is a store mapping for it. More on this later. +* invalidationevents - An array of events that should trigger this cache to invalidate. + +The persist option. +As noted the persist option causes the loader generated for this definition to be stored when first created. Subsequent requests for this definition will be given the original loader instance. +Data passed to or retrieved from the loader and its chained loaders gets cached by the instance. +This option should be used when you know you will require the loader several times and perhaps in different areas of code. +Because it caches key=>value data it avoids the need to re-fetch things from stores after the first request. Its good for performance, bad for memory. +It should be used sparingly. + +The mappingsonly option. +The administrator of a site can create mappings between stores and definitions. Allowing them to designate stores for specific definitions (caches). +Setting this option to true means that the definition can only be used if a mapping has been made for it. +Normally if no mappings exist then the default store for the definition mode is used. + +### Data source +Data sources allow cache _misses_ (requests for a key that doesn't exist) to be handled and loaded internally. +The loader gets used as the last resort if provided and means that code using the cache doesn't need to handle the situation that information isn't cached. +They can be specified in a cache definition and must implement the cache_data_source interface. + +### How it all chains together. +Consider the following if you can: + +Basic request for information (no frills): + + => Code calls get + => Loader handles get, passes the request to its store + <= Memcache doesn't have the data. sorry. + <= Loader returns the result. + |= Code couldn't get the data from the cache. It must generate it and then ask the loader to cache it. + +Advanced initial request for information not already cached (has chained stores and data source): + + => Code calls get + => Loader handles get, passes the request to its store + => Memcache handles request, doesn't have it passes it to the chained store + => File (default store) doesn't have it requests it from the loader + => Data source - makes required db calls, processes information + ...database calls... + ...processing and moulding... + <= Data source returns the information + <= File caches the information on its way back through + <= Memcache caches the information on its way back through + <= Loader returns the data to the user. + |= Code the code now has the data. + +Subsequent request for information: + + => Code calls get + => Loader handles get, passes the request to its store + <= Store returns the data + <= Loader returns the data + |= Code has the data + +Other internal magic you should be aware of +------------------------------------------- +The following should fill you in on a bit more of the behind the scenes stuff for the cache API. + +### Helper class +There is a helper class called cache_helper which is abstract with static methods. +This class handles much of the internal generation and initialisation requirements. +In normal use this class will not be needed outside of the API (mostly internal use only) + +### Configuration +There are two configuration classes cache_config and cache_config_writer. +The reader class is used for every request, the writer is only used when modifying the configuration. +Because the cache API is designed to cache database configuration and meta data it must be able to operate prior to database configuration being loaded. +To get around this we store the configuration information in a file in the dataroot. +The configuration file contains information on the configured store instances, definitions collected from definition files, and mappings. +That information is stored and loaded in the same way we work with the lang string files. +This means that we use the cache API as soon as it has been included. + +### Invalidation +Cache information can be invalidated in two ways. +1. pass a definition name and the keys to be invalidated (or none to invalidate the whole cache). +2. pass an event and the keys to be invalidated. + +The first method is designed to be used when you have a single known definition you want to invalidate entries from within. +The second method is a lot more intensive for the system. There are defined invalidation events that definitions can "subscribe" to (through the definitions invalidationevents option). +When you invalidate by event the cache API finds all of the definitions that subscribe to the event, it then loads the stores for each of those definitions and purges the keys from each store. +This is obviously a recursive and therefor intense process. + +TODO's and things still to think about +-------------------------------------- + + 1. Definitions don't really need/use the component/area identifiers presently. They may be useful in the future... they may not be. + We should consider whether we leave them, or remove them. \ No newline at end of file diff --git a/cache/admin.php b/cache/admin.php new file mode 100644 index 00000000000..96d086d7189 --- /dev/null +++ b/cache/admin.php @@ -0,0 +1,183 @@ +. + +/** + * The administration and management interface for the cache setup and configuration. + * + * This file is part of Moodle's cache API, affectionately called MUC. + * + * @package core + * @category cache + * @copyright 2012 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once('../config.php'); +require_once($CFG->dirroot.'/lib/adminlib.php'); +require_once($CFG->dirroot.'/cache/locallib.php'); +require_once($CFG->dirroot.'/cache/forms.php'); + +$action = optional_param('action', null, PARAM_ALPHA); + +admin_externalpage_setup('cacheconfig'); +$context = context_system::instance(); + +$stores = cache_administration_helper::get_store_summaries(); +$plugins = cache_administration_helper::get_plugin_summaries(); +$definitions = cache_administration_helper::get_definition_summaries(); +$defaultmodestores = cache_administration_helper::get_default_mode_stores(); + +$title = new lang_string('cacheadmin', 'cache'); +$mform = null; +$notification = null; +$notifysuccess = true; + +if (!empty($action) && confirm_sesskey()) { + switch ($action) { + case 'rescandefinitions' : + cache_config_writer::update_definitions(); + redirect($PAGE->url); + break; + case 'addstore' : + $plugin = required_param('plugin', PARAM_PLUGIN); + $mform = cache_administration_helper::get_add_store_form($plugin); + $title = get_string('addstore', 'cache', $plugins[$plugin]['name']); + if ($mform->is_cancelled()) { + rediect($PAGE->url); + } else if ($data = $mform->get_data()) { + $config = cache_administration_helper::get_store_configuration_from_data($data); + $writer = cache_config_writer::instance(); + $writer->add_plugin_instance($data->name, $data->plugin, $config); + redirect($PAGE->url, get_string('addstoresuccess', 'cache', $plugins[$plugin]['name']), 5); + } + break; + case 'editstore' : + $plugin = required_param('plugin', PARAM_PLUGIN); + $store = required_param('store', PARAM_TEXT); + $mform = cache_administration_helper::get_edit_store_form($plugin, $store); + $title = get_string('addstore', 'cache', $plugins[$plugin]['name']); + if ($mform->is_cancelled()) { + rediect($PAGE->url); + } else if ($data = $mform->get_data()) { + $config = cache_administration_helper::get_store_configuration_from_data($data); + $writer = cache_config_writer::instance(); + $writer->edit_plugin_instance($data->name, $data->plugin, $config); + redirect($PAGE->url, get_string('editstoresuccess', 'cache', $plugins[$plugin]['name']), 5); + } + break; + case 'deletestore' : + $store = required_param('store', PARAM_TEXT); + $confirm = optional_param('confirm', false, PARAM_BOOL); + + if (!array_key_exists($store, $stores)) { + $notifysuccess = false; + $notification = get_string('invalidstore'); + } else if ($stores[$store]['mappings'] > 0) { + $notifysuccess = false; + $notification = get_string('deletestorehasmappings'); + } + + if ($notifysuccess) { + if (!$confirm) { + $title = get_string('confirmstoredeletion', 'cache'); + $url = new moodle_url($PAGE->url, array('store' => $store, 'confirm' => 1, 'action' => $action, 'sesskey' => sesskey())); + $button = new single_button($url, get_string('deletestore', 'cache')); + + $PAGE->set_title($title); + $PAGE->set_heading($SITE->fullname); + echo $OUTPUT->header(); + echo $OUTPUT->heading($title); + echo $OUTPUT->confirm(get_string('deletestoreconfirmation', 'cache', $stores[$store]['name']), $button, $PAGE->url); + echo $OUTPUT->footer(); + exit; + } else { + $writer = cache_config_writer::instance(); + $writer->delete_store($store); + redirect($PAGE->url, get_string('deletestoresuccess', 'cache'), 5); + } + } + break; + case 'editdefinitionmapping' : + $definition = required_param('definition', PARAM_TEXT); + $title = get_string('editdefinitionmappings', 'cache', $definition); + $mform = new cache_definition_mappings_form($PAGE->url, array('definition' => $definition)); + if ($mform->is_cancelled()) { + redirect($PAGE->url); + } else if ($data = $mform->get_data()) { + $writer = cache_config_writer::instance(); + $mappings = array(); + foreach ($data->mappings as $mapping) { + if (!empty($mapping)) { + $mappings[] = $mapping; + } + } + $writer->set_definition_mappings($definition, $mappings); + redirect($PAGE->url); + } + break; + case 'editmodemappings': // Edit default mode mappings + $mform = new cache_mode_mappings_form(null, $stores); + $mform->set_data(array( + 'mode_'.cache_store::MODE_APPLICATION => key($defaultmodestores[cache_store::MODE_APPLICATION]), + 'mode_'.cache_store::MODE_SESSION => key($defaultmodestores[cache_store::MODE_SESSION]), + 'mode_'.cache_store::MODE_REQUEST => key($defaultmodestores[cache_store::MODE_REQUEST]), + )); + if ($mform->is_cancelled()) { + redirect($PAGE->url); + } else if ($data = $mform->get_data()) { + $mappings = array( + cache_store::MODE_APPLICATION => array($data->{'mode_'.cache_store::MODE_APPLICATION}), + cache_store::MODE_SESSION => array($data->{'mode_'.cache_store::MODE_SESSION}), + cache_store::MODE_REQUEST => array($data->{'mode_'.cache_store::MODE_REQUEST}), + ); + $writer = cache_config_writer::instance(); + $writer->set_mode_mappings($mappings); + redirect($PAGE->url); + } + break; + + case 'purge': // Purge a store cache + // TODO + break; + } +} + +$PAGE->set_title($title); +$PAGE->set_heading($SITE->fullname); +$renderer = $PAGE->get_renderer('core_cache'); + +echo $renderer->header(); +echo $renderer->heading($title); + +if (!is_null($notification)) { + echo $renderer->notification($notification, ($notifysuccess)?'notifysuccess' : 'notifyproblem'); +} + +if ($mform instanceof moodleform) { + $mform->display(); +} else { + echo $renderer->plugin_summaries($plugins); + echo $renderer->store_summariers($stores, $plugins); + echo $renderer->definition_summaries($definitions, cache_administration_helper::get_definition_actions($context)); + + $applicationstore = join(', ', $defaultmodestores[cache_store::MODE_APPLICATION]); + $sessionstore = join(', ', $defaultmodestores[cache_store::MODE_SESSION]); + $requeststore = join(', ', $defaultmodestores[cache_store::MODE_REQUEST]); + $editurl = new moodle_url('/cache/admin.php', array('action' => 'editmodemappings', 'sesskey' => sesskey())); + echo $renderer->mode_mappings($applicationstore, $sessionstore, $requeststore, $editurl); +} + +echo $renderer->footer(); diff --git a/cache/classes/config.php b/cache/classes/config.php new file mode 100644 index 00000000000..b9fd7f74b65 --- /dev/null +++ b/cache/classes/config.php @@ -0,0 +1,397 @@ +. + +/** + * Cache configuration reader + * + * This file is part of Moodle's cache API, affectionately called MUC. + * It contains the components that are requried in order to use caching. + * + * @package core + * @category cache + * @copyright 2012 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Cache configuration reader. + * + * This class is used to interact with the cache's configuration. + * The configuration is stored in the Moodle data directory. + * + * @package core + * @category cache + * @copyright 2012 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class cache_config { + + /** + * The configured stores + * @var array + */ + protected $configstores = array(); + + /** + * The configured mode mappings + * @var array + */ + protected $configmodemappings = array(); + + /** + * The configured definitions as picked up from cache.php files + * @var array + */ + protected $configdefinitions = array(); + + /** + * The definition mappings that have been configured. + * @var array + */ + protected $configdefinitionmappings = array(); + + /** + * Please use cache_config::instance to get an instance of the cache config that is ready to be used. + */ + public function __construct() { + //$this->config_load(); + } + + /** + * Gets an instance of the cache_configuration class. + * + * @return cache_config + */ + public static function instance() { + $factory = cache_factory::instance(); + return $factory->create_config_instance(); + } + + /** + * Checks if the configuration file exists. + * + * @return bool True if it exists + */ + public static function config_file_exists() { + // Allow for late static binding + return file_exists(self::get_config_file_path()); + } + + /** + * Returns the expected path to the configuration file. + * + * @return string The absolute path + */ + protected static function get_config_file_path() { + global $CFG; + return $CFG->dataroot.'/muc/config.php'; + } + + /** + * Loads the configuration file and parses its contents into the expected structure. + * + * @return boolean + */ + public function load() { + global $CFG; + + $configuration = $this->include_configuration(); + + $this->configstores = array(); + $this->configdefinitions = array(); + $this->configmodemappings = array(); + $this->configdefinitionmappings = array(); + + // Filter the stores + $availableplugins = cache_helper::early_get_cache_plugins(); + foreach ($configuration['stores'] as $store) { + if (!is_array($store) || !array_key_exists('name', $store) || !array_key_exists('plugin', $store)) { + // Not a valid instance configuration + debugging('Invalid cache store in config. Missing name or plugin.', DEBUG_DEVELOPER); + continue; + } + $plugin = $store['plugin']; + $class = 'cache_store_'.$plugin; + if (!array_key_exists($plugin, $availableplugins) && (!class_exists($class) || !is_subclass_of($class, 'cache_store'))) { + // Not a valid plugin, or has been uninstalled, just skip it an carry on. + debugging('Invalid cache store in config. Not an available plugin.', DEBUG_DEVELOPER); + continue; + } + $file = $CFG->dirroot.'/cache/stores/'.$plugin.'/lib.php'; + if (!class_exists($class) && file_exists($file)) { + require_once($file); + } + if (!class_exists($class)) { + continue; + } + if (!array_key_exists('cache_store', class_implements($class))) { + continue; + } + if (!array_key_exists('configuration', $store) || !is_array($store['configuration'])) { + $store['configuration'] = array(); + } + if (!empty($store['useforlocking'])) { + // The site has a specified cache for locking. + unset($this->configstores['default_locking']); + } + $store['class'] = $class; + $store['default'] = !empty($store['default']); + $this->configstores[$store['name']] = $store; + } + + // Filter the definitions + foreach ($configuration['definitions'] as $id => $conf) { + if (!is_array($conf)) { + // Something is very wrong here. + continue; + } + if (!array_key_exists('mode', $conf) || !array_key_exists('component', $conf) || !array_key_exists('area', $conf)) { + // Not a valid definition configuration + continue; + } + if (array_key_exists($id, $this->configdefinitions)) { + debugging('Duplicate cache definition detected. This should never happen.', DEBUG_DEVELOPER); + continue; + } + $conf['mode'] = (int)$conf['mode']; + if ($conf['mode'] < cache_store::MODE_APPLICATION || $conf['mode'] > cache_store::MODE_REQUEST) { + // Invalid cache mode used for the definition + continue; + } + $this->configdefinitions[$id] = $conf; + } + + // Filter the mode mappings + foreach ($configuration['modemappings'] as $mapping) { + if (!is_array($mapping) || !array_key_exists('mode', $mapping) || !array_key_exists('store', $mapping)) { + // Not a valid mapping configuration + debugging('A cache mode mapping entry is invalid.', DEBUG_DEVELOPER); + continue; + } + if (!array_key_exists($mapping['store'], $this->configstores)) { + // Mapped array instance doesn't exist + debugging('A cache mode mapping exists for a mode or store that does not exist.', DEBUG_DEVELOPER); + continue; + } + $mapping['mode'] = (int)$mapping['mode']; + if ($mapping['mode'] < 0 || $mapping['mode'] > 4) { + // Invalid cache type used for the mapping + continue; + } + if (!array_key_exists('sort', $mapping)) { + $mapping['sort'] = 0; + } + $this->configmodemappings[] = $mapping; + } + + // Filter the definition mappings + foreach ($configuration['definitionmappings'] as $mapping) { + if (!is_array($mapping) || !array_key_exists('definition', $mapping) || !array_key_exists('store', $mapping)) { + // Not a valid mapping configuration + continue; + } + if (!array_key_exists($mapping['store'], $this->configstores)) { + // Mapped array instance doesn't exist + continue; + } + if (!array_key_exists($mapping['definition'], $this->configdefinitions)) { + // Mapped array instance doesn't exist + continue; + } + if (!array_key_exists('sort', $mapping)) { + $mapping['sort'] = 0; + } + $this->configdefinitionmappings[] = $mapping; + } + + usort($this->configmodemappings, array($this, 'sort_mappings')); + usort($this->configdefinitionmappings, array($this, 'sort_mappings')); + + return true; + } + + /** + * Includes the configuration file and makes sure it contains the expected bits. + * + * You need to ensure that the config file exists before this is called. + * + * @return array + * @throws cache_exception + */ + protected function include_configuration() { + $configuration = array(); + $cachefile = self::get_config_file_path(); + + if (!file_exists($cachefile)) { + throw new cache_exception('Default cache configuration could not be found. It should have already been created by now.'); + } + include($cachefile); + if (!is_array($configuration)) { + throw new cache_exception('Invalid cache configuration file'); + } + if (!array_key_exists('stores', $configuration) || !is_array($configuration['stores'])) { + $configuration['stores'] = array(); + } + if (!array_key_exists('modemappings', $configuration) || !is_array($configuration['modemappings'])) { + $configuration['modemappings'] = array(); + } + if (!array_key_exists('definitions', $configuration) || !is_array($configuration['definitions'])) { + $configuration['definitions'] = array(); + } + if (!array_key_exists('definitionmappings', $configuration) || !is_array($configuration['definitionmappings'])) { + $configuration['definitionmappings'] = array(); + } + + return $configuration; + } + + /** + * Used to sort cache config arrays based upon a sort key. + * + * Highest number at the top. + * + * @param array $a + * @param array $b + * @return int + */ + protected function sort_mappings(array $a, array $b) { + if ($a['sort'] == $b['sort']) { + return 0; + } + return ($a['sort'] < $b['sort']) ? 1 : -1 ; + } + + /** + * Gets a definition from the config given its name. + * + * @param string $id + * @return bool + */ + public function get_definition_by_id($id) { + if (array_key_exists($id, $this->configdefinitions)) { + return $this->configdefinitions[$id]; + } + return false; + } + + /** + * Returns all the known definitions. + * + * @return array + */ + public function get_definitions() { + return $this->configdefinitions; + } + + /** + * Returns all of the stores that are suitable for the given mode and requirements. + * + * @param int $mode One of cache_store::MODE_* + * @param int $requirements The requirements of the cache as a binary flag + * @return array An array of suitable stores. + */ + public function get_stores($mode, $requirements = 0) { + $stores = array(); + foreach ($this->configstores as $name => $store) { + // If the mode is supported and all of the requirements are provided features. + if (($store['modes'] & $mode) && ($store['features'] & $requirements) === $requirements) { + $stores[$name] = $store; + } + } + return $stores; + } + + /** + * Gets all of the stores that are to be used for the given definition. + * + * @param cache_definition $definition + * @return array + */ + public function get_stores_for_definition(cache_definition $definition) { + // Check if MUC has been disabled. + if (defined('NO_CACHE_STORES') && NO_CACHE_STORES !== false) { + // Yip its been disabled. + // To facilitate this we are going to always return an empty array of stores to use. + // This will force all cache instances to use the cache_store_dummy. + // MUC will still be used essentially so that code using it will still continue to function but because no cache stores + // are being used interaction with MUC will be purely based around a static var. + return array(); + } + + $availablestores = $this->get_stores($definition->get_mode(), $definition->get_requirements_bin()); + $stores = array(); + $id = $definition->get_id(); + + // Now get any mappings and give them priority. + foreach ($this->configdefinitionmappings as $mapping) { + if ($mapping['definition'] !== $id) { + continue; + } + $storename = $mapping['store']; + if (!array_key_exists($storename, $availablestores)) { + continue; + } + if (array_key_exists($storename, $stores)) { + $store = $stores[$storename]; + unset($stores[$storename]); + $stores[$storename] = $store; + } else { + $stores[$storename] = $availablestores[$storename]; + } + } + + if (empty($stores) && !$definition->is_for_mappings_only()) { + $mode = $definition->get_mode(); + // Load the default stores. + foreach ($this->configmodemappings as $mapping) { + if ($mapping['mode'] === $mode && array_key_exists($mapping['store'], $availablestores)) { + $store = $availablestores[$mapping['store']]; + if (empty($store['mappingsonly'])) { + $stores[$mapping['store']] = $store; + } + } + } + } + + return $stores; + } + + /** + * Returns all of the configured stores + * @return array + */ + public function get_all_stores() { + return $this->configstores; + } + + /** + * Returns all of the configured mode mappings + * @return array + */ + public function get_mode_mappings() { + return $this->configmodemappings; + } + + /** + * Returns all of the known definition mappings. + * @return array + */ + public function get_definition_mappings() { + return $this->configdefinitionmappings; + } +} \ No newline at end of file diff --git a/cache/classes/definition.php b/cache/classes/definition.php new file mode 100644 index 00000000000..36373c713d9 --- /dev/null +++ b/cache/classes/definition.php @@ -0,0 +1,715 @@ +. + +/** + * Cache definition class + * + * This file is part of Moodle's cache API, affectionately called MUC. + * It contains the components that are requried in order to use caching. + * + * @package core + * @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 definition class. + * + * Cache definitions need to be defined in db/caches.php files. + * They can be constructed with the following options. + * + * Required settings: + * + mode + * [int] Sets the mode for the definition. Must be one of cache_store::MODE_* + * + * Optional settings: + * + requireidentifiers + * [array] An array of identifiers that must be provided to the cache when it is created. + * + requiredataguarantee + * [bool] If set to true then only stores that can guarantee data will remain available once set will be used. + * + requiremultipleidentifiers + * [bool] If set to true then only stores that support multiple identifiers will be used. + * + requirelockingread + * [bool] If set to true then a lock will be gained before reading from the cache store. It is recommended not to use + * this setting unless 100% absolutely positively required. Remember 99.9% of caches will NOT need this setting. + * This setting will only be used for application caches presently. + * + requirelockingwrite + * [bool] If set to true then a lock will be gained before writing to the cache store. As above this is not recommended + * unless truly needed. Please think about the order of your code and deal with race conditions there first. + * This setting will only be used for application caches presently. + * + maxsize + * [int] If set this will be used as the maximum number of entries within the cache store for this definition. + * Its important to note that cache stores don't actually have to acknowledge this setting or maintain it as a hard limit. + * + overrideclass + * [string] A class to use as the loader for this cache. This is an advanced setting and will allow the developer of the + * definition to take 100% control of the caching solution. + * Any class used here must inherit the cache_loader interface and must extend default cache loader for the mode they are + * using. + * + overrideclassfile + * [string] Suplements the above setting indicated the file containing the class to be used. This file is included when + * required. + * + datasource + * [string] A class to use as the data loader for this definition. + * Any class used here must inherit the cache_data_loader interface. + * + datasourcefile + * [string] Suplements the above setting indicated the file containing the class to be used. This file is included when + * required. + * + persistent + * [bool] This setting does two important things. First it tells the cache API to only instantiate the cache structure for + * this definition once, further requests will be given the original instance. + * Second the cache loader will keep an array of the items set and retrieved to the cache during the request. + * This has several advantages including better performance without needing to start passing the cache instance between + * function calls, the downside is that the cache instance + the items used stay within memory. + * Consider using this setting when you know that there are going to be many calls to the cache for the same information + * or when you are converting existing code to the cache and need to access the cache within functions but don't want + * to add it as an argument to the function. + * + persistentmaxsize + * [int] This supplements the above setting by limiting the number of items in the caches persistent array of items. + * Tweaking this setting lower will allow you to minimise the memory implications above while hopefully still managing to + * offset calls to the cache store. + * + ttl + * [int] A time to live for the data (in seconds). It is strongly recommended that you don't make use of this and + * instead try to create an event driven invalidation system. + * Not all cache stores will support this natively and there are undesired performance impacts if the cache store does not. + * + mappingsonly + * [bool] If set to true only the mapped cache store(s) will be used and the default mode store will not. This is a super + * advanced setting and should not be used unless absolutely required. It allows you to avoid the default stores for one + * reason or another. + * + invalidationevents + * [array] An array of events that should cause this cache to invalidate some or all of the items within it. + * + * For examples take a look at lib/db/caches.php + * + * @package core + * @category cache + * @copyright 2012 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class cache_definition { + + /** + * The identifier for the definition + * @var string + */ + protected $id; + + /** + * The mode for the defintion. One of cache_store::MODE_* + * @var int + */ + protected $mode; + + /** + * The component this definition is associated with. + * @var string + */ + protected $component; + + /** + * The area this definition is associated with. + * @var string + */ + protected $area; + + /** + * An array of identifiers that must be provided when the definition is used to create a cache. + * @var array + */ + protected $requireidentifiers = array(); + + /** + * If set to true then only stores that guarantee data may be used with this definition. + * @var bool + */ + protected $requiredataguarantee = false; + + /** + * If set to true then only stores that support multple identifiers may be used with this definition. + * @var bool + */ + protected $requiremultipleidentifiers = false; + + /** + * If set to true then we know that this definition requires the locking functionality. + * This gets set during construction based upon the settings requirelockingread and requirelockingwrite. + * @var bool + */ + protected $requirelocking = false; + + /** + * Set to true if this definition requires read locking. + * @var bool + */ + protected $requirelockingread = false; + + /** + * Gets set to true if this definition requires write locking. + * @var bool + */ + protected $requirelockingwrite = false; + + /** + * Sets the maximum number of items that can exist in the cache. + * Please note this isn't a hard limit, and doesn't need to be enforced by the caches. They can choose to do so optionally. + * @var int + */ + protected $maxsize = null; + + /** + * The class to use as the cache loader for this definition. + * @var string + */ + protected $overrideclass = null; + + /** + * The file in which the override class exists. This will be included if required. + * @var string Absolute path + */ + protected $overrideclassfile = null; + + /** + * The data source class to use with this definition. + * @var string + */ + protected $datasource = null; + + /** + * The file in which the data source class exists. This will be included if required. + * @var string + */ + protected $datasourcefile = null; + + /** + * The data source class aggregate to use. This is a super advanced setting. + * @var string + */ + protected $datasourceaggregate = null; + + /** + * Set to true if the definitions cache should be persistent + * @var bool + */ + protected $persistent = false; + + /** + * The persistent item array max size. + * @var int + */ + protected $persistentmaxsize = false; + + /** + * The TTL for data in this cache. Please don't use this, instead use event driven invalidation. + * @var int + */ + protected $ttl = 0; + + /** + * Set to true if this cache should only use mapped cache stores and not the default mode cache store. + * @var bool + */ + protected $mappingsonly = false; + + /** + * An array of events that should cause this cache to invalidate. + * @var array + */ + protected $invalidationevents = array(); + + /** + * An array of identifiers provided to this cache when it was initialised. + * @var array + */ + protected $identifiers = array(); + + /** + * Key prefix for use with single key cache stores + * @var string + */ + protected $keyprefixsingle = null; + + /** + * Key prefix to use with cache stores that support multi keys. + * @var array + */ + protected $keyprefixmulti = null; + + /** + * A hash identifier of this definition. + * @var string + */ + protected $definitionhash = null; + + /** + * Creates a cache definition given a definition from the cache configuration or from a caches.php file. + * + * @param string $id + * @param array $definition + * @param string $datasourceaggregate + * @return cache_definition + * @throws coding_exception + */ + public static function load($id, array $definition, $datasourceaggregate = null) { + if (!array_key_exists('mode', $definition)) { + throw new coding_exception('You must provide a mode when creating a cache definition'); + } + if (!array_key_exists('component', $definition)) { + throw new coding_exception('You must provide a mode when creating a cache definition'); + } + if (!array_key_exists('area', $definition)) { + throw new coding_exception('You must provide a mode when creating a cache definition'); + } + $mode = (int)$definition['mode']; + $component = (string)$definition['component']; + $area = (string)$definition['area']; + + // Set the defaults + $requireidentifiers = array(); + $requiredataguarantee = false; + $requiremultipleidentifiers = false; + $requirelockingread = false; + $requirelockingwrite = false; + $maxsize = null; + $overrideclass = null; + $overrideclassfile = null; + $datasource = null; + $datasourcefile = null; + $persistent = false; + $persistentmaxsize = false; + $ttl = 0; + $mappingsonly = false; + $invalidationevents = array(); + + if (array_key_exists('requireidentifiers', $definition)) { + $requireidentifiers = (array)$definition['requireidentifiers']; + } + if (array_key_exists('requiredataguarantee', $definition)) { + $requiredataguarantee = (bool)$definition['requiredataguarantee']; + } + if (array_key_exists('requiremultipleidentifiers', $definition)) { + $requiremultipleidentifiers = (bool)$definition['requiremultipleidentifiers']; + } + + if (array_key_exists('requirelockingread', $definition)) { + $requirelockingread = (bool)$definition['requirelockingread']; + } + if (array_key_exists('requirelockingwrite', $definition)) { + $requirelockingwrite = (bool)$definition['requirelockingwrite']; + } + $requirelocking = $requirelockingwrite || $requirelockingread; + + if (array_key_exists('maxsize', $definition)) { + $maxsize = (int)$definition['maxsize']; + } + + if (array_key_exists('overrideclass', $definition)) { + $overrideclass = $definition['overrideclass']; + } + if (array_key_exists('overrideclassfile', $definition)) { + $overrideclassfile = $definition['overrideclassfile']; + } + + if (array_key_exists('datasource', $definition)) { + $datasource = $definition['datasource']; + } + if (array_key_exists('datasourcefile', $definition)) { + $datasourcefile = $definition['datasourcefile']; + } + + if (array_key_exists('persistent', $definition)) { + $persistent = (bool)$definition['persistent']; + } + if (array_key_exists('persistentmaxsize', $definition)) { + $persistentmaxsize = (int)$definition['persistentmaxsize']; + } + if (array_key_exists('ttl', $definition)) { + $ttl = (int)$definition['ttl']; + } + if (array_key_exists('mappingsonly', $definition)) { + $mappingsonly = (bool)$definition['mappingsonly']; + } + if (array_key_exists('invalidationevents', $definition)) { + $invalidationevents = (array)$definition['invalidationevents']; + } + + if (!is_null($overrideclass)) { + if (!is_null($overrideclassfile)) { + if (!file_exists($overrideclassfile)) { + throw new coding_exception('The override class file does not exist.'); + } + require_once($overrideclassfile); + } + if (!class_exists($overrideclass)) { + throw new coding_exception('The override class does not exist.'); + } + + // Make sure that the provided class extends the default class for the mode. + if (get_parent_class($overrideclass) !== cache_helper::get_class_for_mode($mode)) { + throw new coding_exception('The override class does not immediately extend the relevant cache class.'); + } + } + + if (!is_null($datasource)) { + if (!is_null($datasourcefile)) { + if (!file_exists($datasourcefile)) { + throw new coding_exception('The override class file does not exist.'); + } + require_once($datasourcefile); + } + if (!class_exists($datasource)) { + throw new coding_exception('The override class does not exist.'); + } + if (!array_key_exists('cache_data_source', class_implements($datasource))) { + throw new coding_exception('Cache data source classes must implement the cache_data_source interface'); + } + } + + $cachedefinition = new cache_definition(); + $cachedefinition->id = $id; + $cachedefinition->mode = $mode; + $cachedefinition->component = $component; + $cachedefinition->area = $area; + $cachedefinition->requireidentifiers = $requireidentifiers; + $cachedefinition->requiredataguarantee = $requiredataguarantee; + $cachedefinition->requiremultipleidentifiers = $requiremultipleidentifiers; + $cachedefinition->requirelocking = $requirelocking; + $cachedefinition->requirelockingread = $requirelockingread; + $cachedefinition->requirelockingwrite = $requirelockingwrite; + $cachedefinition->maxsize = $maxsize; + $cachedefinition->overrideclass = $overrideclass; + $cachedefinition->overrideclassfile = $overrideclassfile; + $cachedefinition->datasource = $datasource; + $cachedefinition->datasourcefile = $datasourcefile; + $cachedefinition->datasourceaggregate = $datasourceaggregate; + $cachedefinition->persistent = $persistent; + $cachedefinition->persistentmaxsize = $persistentmaxsize; + $cachedefinition->ttl = $ttl; + $cachedefinition->mappingsonly = $mappingsonly; + $cachedefinition->invalidationevents = $invalidationevents; + + return $cachedefinition; + } + + /** + * Creates an ah-hoc cache definition given the required params. + * + * Please note that when using an adhoc definition you cannot set any of the optional params. + * This is because we cannot guarantee consistent access and we don't want to mislead people into thinking that. + * + * @param int $mode One of cache_store::MODE_* + * @param string $component The component this definition relates to. + * @param string $area The area this definition relates to. + * @param string $overrideclass The class to use as the loader. + * @param bool $persistent If this cache should be persistent. + * @return cache_application|cache_session|cache_request + */ + public static function load_adhoc($mode, $component, $area, $overrideclass = null, $persistent = false) { + $id = 'adhoc/'.$component.'_'.$area; + $definition = array( + 'mode' => $mode, + 'component' => $component, + 'area' => $area, + 'persistent' => $persistent + ); + if (!is_null($overrideclass)) { + $definition['overrideclass'] = $overrideclass; + } + return self::load($id, $definition, null); + } + + /** + * Returns the cache loader class that should be used for this definition. + * @return string + */ + public function get_cache_class() { + if (!is_null($this->overrideclass)) { + return $this->overrideclass; + } + return cache_helper::get_class_for_mode($this->mode); + } + + /** + * Returns the id of this definition. + * @return string + */ + public function get_id() { + return $this->id; + } + + /** + * Returns the name for this definition + * @return string + */ + public function get_name() { + $identifier = 'cachedef_'.clean_param($this->area, PARAM_STRINGID); + $component = $this->component; + if ($component === 'core') { + $component = 'cache'; + } + return new lang_string($identifier, $component); + } + + /** + * Returns the mode of this definition + * @return int One more cache_store::MODE_ + */ + public function get_mode() { + return $this->mode; + } + + /** + * Returns the area this definition is associated with. + * @return string + */ + public function get_area() { + return $this->area; + } + + /** + * Returns the component this definition is associated with. + * @return string + */ + public function get_component() { + return $this->component; + } + + /** + * Returns the identifiers that are being used for this definition. + * @return array + */ + public function get_identifiers() { + return $this->identifiers; + } + + /** + * Returns the ttl in seconds for this definition if there is one, or null if not. + * @return int|null + */ + public function get_ttl() { + return $this->ttl; + } + + /** + * Returns the maximum number of items allowed in this cache. + * @return int + */ + public function get_maxsize() { + return $this->maxsize; + } + + /** + * Returns true if this definition should only be used with mappings. + * @return bool + */ + public function is_for_mappings_only() { + return $this->mappingsonly; + } + + /** + * Returns true if this definition requires a data guarantee from the cache stores being used. + * @return bool + */ + public function require_data_guarantee() { + return $this->requiredataguarantee; + } + + /** + * Returns true if this definition requires that the cache stores support multiple identifiers + * @return bool + */ + public function require_multiple_identifiers() { + return $this->requiremultipleidentifiers; + } + + /** + * Returns true if this definition requires locking functionality. Either read or write locking. + * @return bool + */ + public function require_locking() { + return $this->requirelocking; + } + + /** + * Returns true if this definition requires read locking. + * @return bool + */ + public function require_locking_read() { + return $this->requirelockingread; + } + + /** + * Returns true if this definition requires write locking. + * @return bool + */ + public function require_locking_write() { + return $this->requirelockingwrite; + } + + /** + * Returns true if this definition has an associated data source. + * @return bool + */ + public function has_data_source() { + return !is_null($this->datasource); + } + + /** + * Returns an instance of the data source class used for this definition. + * + * @return cache_data_source + * @throws coding_exception + */ + public function get_data_source() { + if (!$this->has_data_source()) { + throw new coding_exception('This cache does not use a datasource.'); + } + return forward_static_call(array($this->datasource, 'get_instance_for_cache'), $this); + } + + /** + * Sets the identifiers for this definition, or updates them if they have already been set. + * + * @param array $identifiers + * @throws coding_exception + */ + public function set_identifiers(array $identifiers = array()) { + foreach ($this->requireidentifiers as $identifier) { + if (!array_key_exists($identifier, $identifiers)) { + throw new coding_exception('Identifier required for cache has not been provided: '.$identifier); + } + } + foreach ($identifiers as $name => $value) { + $this->identifiers[$name] = (string)$value; + } + // Reset the key prefix's they need updating now. + $this->keyprefixsingle = null; + $this->keyprefixmulti = null; + } + + /** + * Returns the requirements of this definition as a binary flag. + * @return int + */ + public function get_requirements_bin() { + $requires = 0; + if ($this->require_data_guarantee()) { + $requires += cache_store::SUPPORTS_DATA_GUARANTEE; + } + if ($this->require_multiple_identifiers()) { + $requires += cache_store::SUPPORTS_MULTIPLE_IDENTIFIERS; + } + return $requires; + } + + /** + * Returns true if this definitions cache should be made persistent. + * @return bool + */ + public function should_be_persistent() { + return $this->persistent; + } + + /** + * Returns the max size for the persistent item array in the cache. + * @return int + */ + public function get_persistent_max_size() { + return $this->persistentmaxsize; + } + + /** + * Generates a hash of this definition and returns it. + * @return string + */ + public function generate_definition_hash() { + if ($this->definitionhash === null) { + $this->definitionhash = md5("{$this->mode} {$this->component} {$this->area}"); + } + return $this->definitionhash; + } + + /** + * Generates a single key prefix for this definition + * + * @return string + */ + public function generate_single_key_prefix() { + if ($this->keyprefixsingle === null) { + $this->keyprefixsingle = $this->mode.'/'.$this->mode; + $identifiers = $this->get_identifiers(); + if ($identifiers) { + foreach ($identifiers as $key => $value) { + $this->keyprefixsingle .= '/'.$key.'='.$value; + } + } + $this->keyprefixsingle = md5($this->keyprefixsingle); + } + return $this->keyprefixsingle; + } + + /** + * Generates a multi key prefix for this definition + * + * @return array + */ + public function generate_multi_key_parts() { + if ($this->keyprefixmulti === null) { + $this->keyprefixmulti = array( + 'mode' => $this->mode, + 'component' => $this->component, + 'area' => $this->area, + ); + if (!empty($this->identifiers)) { + $identifiers = array(); + foreach ($this->identifiers as $key => $value) { + $identifiers[] = htmlentities($key).'='.htmlentities($value); + } + $this->keyprefixmulti['identifiers'] = join('&', $identifiers); + } + } + return $this->keyprefixmulti; + } + + /** + * Check if this definition should invalidate on the given event. + * + * @param string $event + * @return bool True if the definition should invalidate on the event. False otherwise. + */ + public function invalidates_on_event($event) { + return (in_array($event, $this->invalidationevents)); + } + + /** + * Check if the definition has any invalidation events. + * + * @return bool True if it does, false otherwise + */ + public function has_invalidation_events() { + return !empty($this->invalidationevents); + } + + /** + * Returns all of the invalidation events for this definition. + * + * @return array + */ + public function get_invalidation_events() { + return $this->invalidationevents; + } +} \ No newline at end of file diff --git a/cache/classes/dummystore.php b/cache/classes/dummystore.php new file mode 100644 index 00000000000..d49dbe10354 --- /dev/null +++ b/cache/classes/dummystore.php @@ -0,0 +1,269 @@ +. + +/** + * Cache dummy store. + * + * This dummy store is used when a load has no other stores that it can make use of. + * This shouldn't happen in normal operation... I think. + * + * This file is part of Moodle's cache API, affectionately called MUC. + * It contains the components that are requried in order to use caching. + * + * @package core + * @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 dummy store. + * + * @copyright 2012 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class cache_store_dummy implements cache_store { + + /** + * The name of this store. + * @var string + */ + protected $name; + + /** + * Gets set to true if this store is going to persist data. + * This happens when the definition doesn't require it as the loader will not be persisting information and something has to. + * @var bool + */ + protected $persist = false; + + /** + * The persistent store array + * @var array + */ + protected $store = array(); + + /** + * Constructs a dummy store instance. + * @param string $name + * @param array $configuration + */ + public function __construct($name = 'Dummy store', array $configuration = array()) { + $this->name = $name; + } + + /** + * Returns true if this store plugin is usable. + * @return bool + */ + public static function are_requirements_met() { + return true; + } + + /** + * Returns true if the user can add an instance. + * @return bool + */ + public static function can_add_instance() { + return false; + } + + /** + * Returns the supported features. + * @param array $configuration + * @return int + */ + public static function get_supported_features(array $configuration = array()) { + return self::SUPPORTS_NATIVE_TTL; + } + + /** + * Returns the supported mode. + * @param array $configuration + * @return int + */ + public static function get_supported_modes(array $configuration = array()) { + return self::MODE_APPLICATION + self::MODE_REQUEST + self::MODE_SESSION; + } + + /** + * Initialises the store instance for a definition. + * @param cache_definition $definition + */ + public function initialise(cache_definition $definition) { + // If the definition isn't persistent then we need to be persistent here. + $this->persist = !$definition->should_be_persistent(); + } + + /** + * Returns true if this has been initialised. + * @return bool + */ + public function is_initialised() { + return (!empty($this->definition)); + } + + /** + * Returns true if this is ready. + * @return bool + */ + public function is_ready() { + return true; + } + + /** + * Returns true the given mode is supported. + * @param int $mode + * @return bool + */ + public static function is_supported_mode($mode) { + return true; + } + + /** + * Returns true if this store supports data guarantee. + * @return bool + */ + public function supports_data_guarantee() { + return false; + } + + /** + * Returns true if this store supports multiple identifiers. + * @return bool + */ + public function supports_multiple_indentifiers() { + return false; + } + + /** + * Returns true if this store supports a native ttl. + * @return bool + */ + public function supports_native_ttl() { + return true; + } + + /** + * Returns the data for the given key + * @param string $key + * @return string|false + */ + public function get($key) { + if ($this->persist && array_key_exists($key, $this->store)) { + return $this->store[$key]; + } + return false; + } + + /** + * Gets' the values for many keys + * @param array $keys + * @return bool + */ + public function get_many($keys) { + $return = array(); + foreach ($keys as $key) { + if ($this->persist && array_key_exists($key, $this->store)) { + $return[$key] = $this->store[$key]; + } else { + $return[$key] = false; + } + } + return $return; + } + + /** + * Sets an item in the cache + * @param string $key + * @param mixed $data + * @return bool + */ + public function set($key, $data) { + if ($this->persist) { + $this->store[$key] = $data; + } + return true; + } + + /** + * Sets many items in the cache + * @param array $keyvaluearray + * @return int + */ + public function set_many(array $keyvaluearray) { + if ($this->persist) { + foreach ($keyvaluearray as $pair) { + $this->store[$pair['key']] = $pair['value']; + } + return count($keyvaluearray); + } + return 0; + } + + /** + * Deletes an item from the cache + * @param string $key + * @return bool + */ + public function delete($key) { + unset($this->store[$key]); + return true; + } + /** + * Deletes many items from the cache + * @param array $keys + * @return bool + */ + public function delete_many(array $keys) { + if ($this->persist) { + foreach ($keys as $key) { + unset($this->store[$key]); + } + } + return count($keys); + } + + /** + * Deletes all of the items from the cache. + * @return bool + */ + public function purge() { + $this->store = array(); + return true; + } + + /** + * Performs any necessary clean up when the store instance is being deleted. + */ + public function cleanup() { + $this->purge(); + } + + /** + * Generates an instance of the cache store that can be used for testing. + * + * @param cache_definition $definition + * @return false + */ + public static function initialise_test_instance(cache_definition $definition) { + $cache = new cache_store_dummy('Dummy store test'); + $cache->initialise($definition); + return $cache;; + } +} \ No newline at end of file diff --git a/cache/classes/factory.php b/cache/classes/factory.php new file mode 100644 index 00000000000..def9f999d1a --- /dev/null +++ b/cache/classes/factory.php @@ -0,0 +1,307 @@ +. + +/** + * This file contains the cache factory class. + * + * This file is part of Moodle's cache API, affectionately called MUC. + * It contains the components that are requried in order to use caching. + * + * @package core + * @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 factory class. + * + * This factory class is important because it stores instances of objects used by the cache API and returns them upon requests. + * This allows us to both reuse objects saving on overhead, and gives us an easy place to "reset" the cache API in situations that + * we need such as unit testing. + * + * @copyright 2012 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class cache_factory { + + /** + * An instance of the cache_factory class created upon the first request. + * @var cache_factory + */ + protected static $instance; + + /** + * An array containing caches created for definitions + * @var array + */ + protected $cachesfromdefinitions = array(); + + /** + * Array of caches created by parameters, ad-hoc definitions will have been used. + * @var array + */ + protected $cachesfromparams = array(); + + /** + * An array of instantiated stores. + * @var array + */ + protected $stores = array(); + + /** + * An array of configuration instances + * @var array + */ + protected $configs = array(); + + /** + * An array of initialised definitions + * @var array + */ + protected $definitions = array(); + + /** + * Returns an instance of the cache_factor method. + * + * @param bool $forcereload If set to true a new cache_factory instance will be created and used. + * @return cache_factory + */ + public static function instance($forcereload = false) { + if ($forcereload || self::$instance === null) { + self::$instance = new cache_factory(); + } + return self::$instance; + } + + /** + * Protected constructor, please use the static instance method. + */ + protected function __construct() { + // Nothing to do here. + } + + /** + * Resets the arrays containing instantiated caches, stores, and config instances. + */ + public static function reset() { + $factory = self::instance(); + $factory->cachesfromdefinitions = array(); + $factory->cachesfromparams = array(); + $factory->stores = array(); + $factory->configs = array(); + $factory->definitions = array(); + } + + /** + * Creates a cache object given the parameters for a definition. + * + * If a cache has already been created for the given definition then that cache instance will be returned. + * + * @param string $component + * @param string $area + * @param array $identifiers + * @param string $aggregate + * @return cache_application|cache_session|cache_request + */ + public function create_cache_from_definition($component, $area, array $identifiers = array(), $aggregate = null) { + $definitionname = $component.'/'.$area; + if (array_key_exists($definitionname, $this->cachesfromdefinitions)) { + $cache = $this->cachesfromdefinitions[$definitionname]; + $cache->set_identifiers($identifiers); + return $cache; + } + $definition = $this->create_definition($component, $area, $aggregate); + $definition->set_identifiers($identifiers); + $cache = $this->create_cache($definition, $identifiers); + if ($definition->should_be_persistent()) { + $this->cachesfromdefinitions[$definitionname] = $cache; + } + return $cache; + } + + /** + * Creates an ad-hoc cache from the given param. + * + * If a cache has already been created using the same params then that cache instance will be returned. + * + * @param int $mode + * @param string $component + * @param string $area + * @param array $identifiers + * @param bool $persistent + * @return cache_application|cache_session|cache_request + */ + public function create_cache_from_params($mode, $component, $area, array $identifiers = array(), $persistent = false) { + $key = "{$mode}_{$component}_{$area}"; + if (array_key_exists($key, $this->cachesfromparams)) { + return $this->cachesfromparams[$key]; + } + // Get the class. Note this is a late static binding so we need to use get_called_class. + $definition = cache_definition::load_adhoc($mode, $component, $area, null, $persistent); + $definition->set_identifiers($identifiers); + $cache = $this->create_cache($definition, $identifiers); + if ($definition->should_be_persistent()) { + $cache->persist = true; + $cache->persistcache = array(); + $this->cachesfromparams[$key] = $cache; + } + return $cache; + } + + /** + * Common protected method to create a cache instance given a definition. + * + * This is used by the static make methods. + * + * @param cache_definition $definition + * @return cache_application|cache_session|cache_store + * @throws coding_exception + */ + public function create_cache(cache_definition $definition) { + $class = $definition->get_cache_class(); + $stores = cache_helper::get_cache_stores($definition); + if (count($stores) === 0) { + // Hmm no stores, better provide a dummy store to mimick functionality. The dev will be none the wiser. + $stores[] = $this->create_dummy_store($definition); + } + $loader = null; + if ($definition->has_data_source()) { + $loader = $definition->get_data_source(); + } + while (($store = array_pop($stores)) !== null) { + $loader = new $class($definition, $store, $loader); + } + return $loader; + } + + /** + * Creates a store instance given its name and configuration. + * + * If the store has already been instantiated then the original objetc will be returned. (reused) + * + * @param string $name The name of the store (must be unique remember) + * @param array $details + * @param cache_definition $definition The definition to instantiate it for. + * @return boolean + */ + public function create_store_from_config($name, array $details, cache_definition $definition) { + if (!array_key_exists($name, $this->stores)) { + // name, plugin, configuration, class + $class = $details['class']; + $store = new $class($details['name'], $details['configuration']); + $this->stores[$name] = $store; + } + $store = $this->stores[$name]; + if (!$store->is_ready() || !$store->is_supported_mode($definition->get_mode())) { + return false; + } + $store = clone($this->stores[$name]); + $store->initialise($definition); + return $store; + } + + /** + * Creates a cache config instance with the ability to write if required. + * + * @param bool $writer If set to true an instance that can update the configuration will be returned. + * @return cache_config|cache_config_writer + */ + public function create_config_instance($writer = false) { + global $CFG; + + // Check if we need to create a config file with defaults. + $needtocreate = !cache_config::config_file_exists(); + + // The class to use. + $class = 'cache_config'; + if ($writer || $needtocreate) { + require_once($CFG->dirroot.'/cache/locallib.php'); + $class .= '_writer'; + } + + // Check if this is a PHPUnit test and redirect to the phpunit config classes if it is. + if (defined('PHPUNIT_TEST') && PHPUNIT_TEST) { + require_once($CFG->dirroot.'/cache/locallib.php'); + require_once($CFG->dirroot.'/cache/tests/fixtures/lib.php'); + // We have just a single class for PHP unit tests. We don't care enough about its + // performance to do otherwise and having a single method allows us to inject things into it + // while testing. + $class = 'cache_config_phpunittest'; + } + + if ($needtocreate) { + // Create the default configuration. + $class::create_default_configuration(); + } + + if (!array_key_exists($class, $this->configs)) { + // Create a new instance and call it to load it. + $this->configs[$class] = new $class; + $this->configs[$class]->load(); + } + + // Return the instance. + return $this->configs[$class]; + } + + /** + * Creates a definition instance or returns the existing one if it has already been created. + * @param string $component + * @param string $area + * @param string $aggregate + * @return cache_definition + */ + public function create_definition($component, $area, $aggregate = null) { + $id = $component.'/'.$area; + if ($aggregate) { + $id .= '::'.$aggregate; + } + if (!array_key_exists($id, $this->definitions)) { + $instance = $this->create_config_instance(); + $definition = $instance->get_definition_by_id($id); + if (!$definition) { + $this->reset(); + $instance = $this->create_config_instance(true); + $instance->update_definitions(); + $definition = $instance->get_definition_by_id($id); + if (!$definition) { + throw new coding_exception('The requested cache definition does not exist.'. $id, $id); + } else { + debugging('Cache definitions reparsed causing cache reset in order to locate definition. You should bump the version number to ensure definitions are reprocessed.', DEBUG_DEVELOPER); + } + } + $this->definitions[$id] = cache_definition::load($id, $definition, $aggregate); + } + return $this->definitions[$id]; + } + + /** + * Creates a dummy store object for use when a loader has no potential stores to use. + * + * @param cache_definition $definition + * @return cache_store_dummy + */ + protected function create_dummy_store(cache_definition $definition) { + global $CFG; + require_once($CFG->dirroot.'/cache/classes/dummystore.php'); + $store = new cache_store_dummy(); + $store->initialise($definition); + return $store; + } +} \ No newline at end of file diff --git a/cache/classes/helper.php b/cache/classes/helper.php new file mode 100644 index 00000000000..b118e8e6078 --- /dev/null +++ b/cache/classes/helper.php @@ -0,0 +1,432 @@ +. + +/** + * Cache helper class + * + * This file is part of Moodle's cache API, affectionately called MUC. + * It contains the components that are requried in order to use caching. + * + * @package core + * @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 helper class. + * + * The cache helper class provides common functionality to the cache API and is useful to developers within to interact with + * the cache API in a general way. + * + * @package core + * @category cache + * @copyright 2012 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class cache_helper { + + /** + * Statistics gathered by the cache API during its operation will be used here. + * @static + * @var array + */ + protected static $stats = array(); + + /** + * The instance of the cache helper. + * @var cache_helper + */ + protected static $instance; + + /** + * Returns true if the cache API can be initialised before Moodle has finished initialising itself. + * + * This check is essential when trying to cache the likes of configuration information. It checks to make sure that the cache + * configuration file has been created which allows use to set up caching when ever is required. + * + * @return bool + */ + public static function ready_for_early_init() { + return cache_config::config_file_exists(); + } + + /** + * Returns an instance of the cache_helper. + * + * This is designed for internal use only and acts as a static store. + * @staticvar null $instance + * @return cache_helper + */ + protected static function instance() { + if (is_null(self::$instance)) { + self::$instance = new cache_helper(); + } + return self::$instance; + } + + /** + * Constructs an instance of the cache_helper class. Again for internal use only. + */ + protected function __construct() { + // Nothing to do here, just making sure you can't get an instance of this. + } + + /** + * Used as a data store for initialised definitions. + * @var array + */ + protected $definitions = array(); + + /** + * Used as a data store for initialised cache stores + * We use this because we want to avoid establishing multiple instances of a single store. + * @var array + */ + protected $stores = array(); + + /** + * Returns the class for use as a cache loader for the given mode. + * + * @param int $mode One of cache_store::MODE_ + * @return string + * @throws coding_exception + */ + public static function get_class_for_mode($mode) { + switch ($mode) { + case cache_store::MODE_APPLICATION : + return 'cache_application'; + case cache_store::MODE_REQUEST : + return 'cache_request'; + case cache_store::MODE_SESSION : + return 'cache_session'; + } + throw new coding_exception('Unknown cache mode passed. Must be one of cache_store::MODE_*'); + } + + /** + * Returns the cache stores to be used with the given definition. + * @param cache_definition $definition + * @return array + */ + public static function get_cache_stores(cache_definition $definition) { + $instance = cache_config::instance(); + $stores = $instance->get_stores_for_definition($definition); + $stores = self::initialise_cache_store_instances($stores, $definition); + return $stores; + } + + /** + * Internal function for initialising an array of stores against a given cache definition. + * + * @param array $stores + * @param cache_definition $definition + * @return array + */ + protected static function initialise_cache_store_instances(array $stores, cache_definition $definition) { + $return = array(); + $factory = cache_factory::instance(); + foreach ($stores as $name => $details) { + $store = $factory->create_store_from_config($name, $details, $definition); + if ($store !== false) { + $return[] = $store; + } + } + return $return; + } + + /** + * Returns the cache store to be used for locking or false if there is not one. + * @return cache_store|boolean + */ + public static function get_cache_store_for_locking() { + $factory = cache_factory::instance(); + $definition = $factory->create_definition('core', 'locking'); + $instance = cache_config::instance(); + $stores = $instance->get_stores_for_definition($definition); + foreach ($stores as $name => $details) { + if ($details['useforlocking']) { + $instances = self::initialise_cache_store_instances(array($name => $details), $definition); + return reset($instances); + } + } + return false; + } + + /** + * Returns an array of plugins without using core methods. + * + * This function explicitly does NOT use core functions as it will in some circumstances be called before Moodle has + * finished initialising. This happens when loading configuration for instance. + * + * @return string + */ + public static function early_get_cache_plugins() { + global $CFG; + $result = array(); + $ignored = array('CVS', '_vti_cnf', 'simpletest', 'db', 'yui', 'tests'); + $fulldir = $CFG->dirroot.'/cache/stores'; + $items = new DirectoryIterator($fulldir); + foreach ($items as $item) { + if ($item->isDot() or !$item->isDir()) { + continue; + } + $pluginname = $item->getFilename(); + if (in_array($pluginname, $ignored)) { + continue; + } + $pluginname = clean_param($pluginname, PARAM_PLUGIN); + if (empty($pluginname)) { + // better ignore plugins with problematic names here + continue; + } + $result[$pluginname] = $fulldir.'/'.$pluginname; + unset($item); + } + unset($items); + return $result; + } + + /** + * Invalidates a given set of keys from a given definition. + * + * @todo Invalidating by definition should also add to the event cache so that sessions can be invalidated (when required). + * + * @param string $component + * @param string $area + * @param array $identifiers + * @param array $keys + * @return boolean + */ + public static function invalidate_by_definition($component, $area, array $identifiers = array(), $keys = array()) { + $cache = cache::make($component, $area, $identifiers); + if (is_array($keys)) { + $cache->delete_many($keys); + } else if (is_scalar($keys)) { + $cache->delete($keys); + } else { + throw new coding_exception('cache_helper::invalidate_by_definition only accepts $keys as array, or scalar.'); + } + return true; + } + + /** + * Invalidates a given set of keys by means of an event. + * + * @todo add support for identifiers to be supplied and utilised. + * + * @param string $event + * @param array $keys + */ + public static function invalidate_by_event($event, array $keys) { + $instance = cache_config::instance(); + $invalidationeventset = false; + $factory = cache_factory::instance(); + foreach ($instance->get_definitions() as $name => $definitionarr) { + $definition = cache_definition::load($name, $definitionarr); + if ($definition->invalidates_on_event($event)) { + // OK at this point we know that the definition has information to invalidate on the event. + // There are two routes, either its an application cache in which case we can invalidate it now. + // or it is a session cache in which case we need to set something to the "Event invalidation" definition. + // No need to deal with request caches, we don't want to change data half way through a request. + if ($definition->get_mode() === cache_store::MODE_APPLICATION) { + $cache = $factory->create_cache($definition); + $cache->delete_many($keys); + } + + // We need to flag the event in the "Event invalidation" cache if it hasn't already happened. + if ($invalidationeventset === false) { + // Get the event invalidation cache. + $cache = cache::make('core', 'eventinvalidation'); + // Get any existing invalidated keys for this cache. + $data = $cache->get($event); + if ($data === false) { + // There are none. + $data = array(); + } + // Add our keys to them with the current cache timestamp. + foreach ($keys as $key) { + $data[$key] = cache::now(); + } + // Set that data back to the cache. + $cache->set($event, $data); + // This only needs to occur once. + $invalidationeventset = true; + } + } + } + } + + /** + * Purges the cache for a specific definition. + * + * @param string $component + * @param string $area + * @param array $identifiers + * @return bool + */ + public static function purge_by_definition($component, $area, array $identifiers = array()) { + // Create the cache + $cache = cache::make($component, $area, $identifiers); + // Purge baby, purge. + $cache->purge(); + return true; + } + + /** + * Purges a cache of all information on a given event. + * + * @param string $event + */ + public static function purge_by_event($event) { + $instance = cache_config::instance(); + $invalidationeventset = false; + $factory = cache_factory::instance(); + foreach ($instance->get_definitions() as $name => $definitionarr) { + $definition = cache_definition::load($name, $definitionarr); + if ($definition->invalidates_on_event($event)) { + // Purge the cache. + $cache = $factory->create_cache($definition); + $cache->purge(); + + // We need to flag the event in the "Event invalidation" cache if it hasn't already happened. + if ($invalidationeventset === false) { + // Get the event invalidation cache. + $cache = cache::make('core', 'eventinvalidation'); + // Create a key to invalidate all + $data = array( + 'purged' => cache::now() + ); + // Set that data back to the cache. + $cache->set($event, $data); + // This only needs to occur once. + $invalidationeventset = true; + } + } + } + } + + /** + * Ensure that the stats array is ready to collect information for the given store and definition. + * @param string $store + * @param string $definition + */ + protected static function ensure_ready_for_stats($store, $definition) { + if (!array_key_exists($definition, self::$stats)) { + self::$stats[$definition] = array( + $store => array( + 'hits' => 0, + 'misses' => 0, + 'sets' => 0, + ) + ); + } else if (!array_key_exists($store, self::$stats[$definition])) { + self::$stats[$definition][$store] = array( + 'hits' => 0, + 'misses' => 0, + 'sets' => 0, + ); + } + } + + /** + * Record a cache hit in the stats for the given store and definition. + * + * @param string $store + * @param string $definition + */ + public static function record_cache_hit($store, $definition) { + self::ensure_ready_for_stats($store, $definition); + self::$stats[$definition][$store]['hits']++; + } + + /** + * Record a cache miss in the stats for the given store and definition. + * + * @param string $store + * @param string $definition + */ + public static function record_cache_miss($store, $definition) { + self::ensure_ready_for_stats($store, $definition); + self::$stats[$definition][$store]['misses']++; + } + + /** + * Record a cache set in the stats for the given store and definition. + * + * @param string $store + * @param string $definition + */ + public static function record_cache_set($store, $definition) { + self::ensure_ready_for_stats($store, $definition); + self::$stats[$definition][$store]['sets']++; + } + + /** + * Return the stats collected so far. + * @return array + */ + public static function get_stats() { + return self::$stats; + } + + /** + * Purge all of the cache stores of all of their data. + */ + public static function purge_all() { + $config = cache_config::instance(); + $stores = $config->get_all_stores(); + $definition = cache_definition::load_adhoc(cache_store::MODE_REQUEST, 'core', 'cache_purge'); + foreach ($stores as $store) { + $class = $store['class']; + $instance = new $class($store['name'], $store['configuration']); + if (!$instance->is_ready()) { + continue; + } + $instance->initialise($definition); + $instance->purge(); + } + } + + /** + * Returns the translated name of the definition. + * + * @param cache_definition $definition + * @return lang_string + */ + public static function get_definition_name($definition) { + if ($definition instanceof cache_definition) { + return $definition->get_name(); + } + $identifier = 'cachedef_'.clean_param($definition['area'], PARAM_STRINGID); + $component = $definition['component']; + if ($component === 'core') { + $component = 'cache'; + } + return new lang_string($identifier, $component); + } + + /** + * Hashes a descriptive key to make it shorter and stil unique. + * @param string $key + * @return string + */ + public static function hash_key($key) { + return crc32($key); + } +} \ No newline at end of file diff --git a/cache/classes/interfaces.php b/cache/classes/interfaces.php new file mode 100644 index 00000000000..44b5d6b00ec --- /dev/null +++ b/cache/classes/interfaces.php @@ -0,0 +1,625 @@ +. + +/** + * Cache API interfaces + * + * This file is part of Moodle's cache API, affectionately called MUC. + * It contains the components that are requried in order to use caching. + * + * @package core + * @category cache + * @copyright 2012 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Cache Loader. + * + * This cache loader interface provides the required structure for classes that wish to be interacted with as a + * means of accessing and interacting with a cache. + * + * Can be implemented by any class wishing to be a cache loader. + */ +interface cache_loader { + + /** + * Retrieves the value for the given key from the cache. + * + * @param string|int $key The key for the data being requested. + * @param int $strictness One of IGNORE_MISSING or MUST_EXIST. + * @return mixed The data retrieved from the cache, or false if the key did not exist within the cache. + * If MUST_EXIST was used then an exception will be thrown if the key does not exist within the cache. + */ + public function get($key, $strictness = IGNORE_MISSING); + + /** + * Retrieves an array of values for an array of keys. + * + * Using this function comes with potential performance implications. + * Not all cache stores will support get_many/set_many operations and in order to replicate this functionality will call + * the equivalent singular method for each item provided. + * This should not deter you from using this function as there is a performance benefit in situations where the cache + * store does support it, but you should be aware of this fact. + * + * @param array $keys The keys of the data being requested. + * @param int $strictness One of IGNORE_MISSING or MUST_EXIST. + * @return array An array of key value pairs for the items that could be retrieved from the cache. + * If MUST_EXIST was used and not all keys existed within the cache then an exception will be thrown. + * Otherwise any key that did not exist will have a data value of false within the results. + */ + public function get_many(array $keys, $strictness = IGNORE_MISSING); + + /** + * Sends a key => value pair to the cache. + * + * + * // This code will add four entries to the cache, one for each url. + * $cache->set('main', 'http://moodle.org'); + * $cache->set('docs', 'http://docs.moodle.org'); + * $cache->set('tracker', 'http://tracker.moodle.org'); + * $cache->set('qa', 'http://qa.moodle.net'); + * + * + * @param string|int $key The key for the data being requested. + * @param mixed $data The data to set against the key. + * @return bool True on success, false otherwise. + */ + public function set($key, $data); + + /** + * Sends several key => value pairs to the cache. + * + * Using this function comes with potential performance implications. + * Not all cache stores will support get_many/set_many operations and in order to replicate this functionality will call + * the equivalent singular method for each item provided. + * This should not deter you from using this function as there is a performance benefit in situations where the cache store + * does support it, but you should be aware of this fact. + * + * + * // This code will add four entries to the cache, one for each url. + * $cache->set_many(array( + * 'main' => 'http://moodle.org', + * 'docs' => 'http://docs.moodle.org', + * 'tracker' => 'http://tracker.moodle.org', + * 'qa' => ''http://qa.moodle.net' + * )); + * + * + * @param array $keyvaluearray An array of key => value pairs to send to the cache. + * @return int The number of items successfully set. It is up to the developer to check this matches the number of items. + * ... if they care that is. + */ + public function set_many(array $keyvaluearray); + + /** + * Test is a cache has a key. + * + * The use of the has methods is strongly discouraged. In a high load environment the cache may well change between the + * test and any subsequent action (get, set, delete etc). + * Instead it is recommended to write your code in such a way they it performs the following steps: + *
    + *
  1. Attempt to retrieve the information.
  2. + *
  3. Generate the information.
  4. + *
  5. Attempt to set the information
  6. + *
+ * + * Its also worth mentioning that not all stores support key tests. + * For stores that don't support key tests this functionality is mimicked by using the equivalent get method. + * Just one more reason you should not use these methods unless you have a very good reason to do so. + * + * @param string|int $key + * @return bool True if the cache has the requested key, false otherwise. + */ + public function has($key); + + /** + * Test if a cache has at least one of the given keys. + * + * It is strongly recommended to avoid the use of this function if not absolutely required. + * In a high load environment the cache may well change between the test and any subsequent action (get, set, delete etc). + * + * Its also worth mentioning that not all stores support key tests. + * For stores that don't support key tests this functionality is mimicked by using the equivalent get method. + * Just one more reason you should not use these methods unless you have a very good reason to do so. + * + * @param array $keys + * @return bool True if the cache has at least one of the given keys + */ + public function has_any(array $keys); + + /** + * Test is a cache has all of the given keys. + * + * It is strongly recommended to avoid the use of this function if not absolutely required. + * In a high load environment the cache may well change between the test and any subsequent action (get, set, delete etc). + * + * Its also worth mentioning that not all stores support key tests. + * For stores that don't support key tests this functionality is mimicked by using the equivalent get method. + * Just one more reason you should not use these methods unless you have a very good reason to do so. + * + * @param array $keys + * @return bool True if the cache has all of the given keys, false otherwise. + */ + public function has_all(array $keys); + + /** + * Delete the given key from the cache. + * + * @param string|int $key The key to delete. + * @param bool $recurse When set to true the key will also be deleted from all stacked cache loaders and their stores. + * This happens by default and ensure that all the caches are consistent. It is NOT recommended to change this. + * @return bool True of success, false otherwise. + */ + public function delete($key, $recurse = true); + + /** + * Delete all of the given keys from the cache. + * + * @param array $keys The key to delete. + * @param bool $recurse When set to true the key will also be deleted from all stacked cache loaders and their stores. + * This happens by default and ensure that all the caches are consistent. It is NOT recommended to change this. + * @return int The number of items successfully deleted. + */ + public function delete_many(array $keys, $recurse = true); +} + +/** + * Cache Loader supporting locking. + * + * This interface should be given to classes already implementing cache_loader that also wish to support locking. + * It outlines the required structure for utilising locking functionality when using a cache. + * + * Can be implemented by any class already implementing the cache_loader interface. + */ +interface cache_loader_with_locking { + + /** + * Acquires a lock for the given key. + * + * Please note that this happens automatically if the cache definition requires locking. + * it is still made a public method so that adhoc caches can use it if they choose. + * However this doesn't guarantee consistent access. It will become the reponsiblity of the calling code to ensure locks + * are acquired, checked, and released. + * + * @param string|int $key + * @return bool True if the lock could be acquired, false otherwise. + */ + public function acquire_lock($key); + + /** + * Checks if the cache loader owns the lock for the given key. + * + * Please note that this happens automatically if the cache definition requires locking. + * it is still made a public method so that adhoc caches can use it if they choose. + * However this doesn't guarantee consistent access. It will become the reponsiblity of the calling code to ensure locks + * are acquired, checked, and released. + * + * @param string|int $key + * @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); + + /** + * Releases the lock for the given key. + * + * Please note that this happens automatically if the cache definition requires locking. + * it is still made a public method so that adhoc caches can use it if they choose. + * However this doesn't guarantee consistent access. It will become the reponsiblity of the calling code to ensure locks + * are acquired, checked, and released. + * + * @param string|int $key + * @return bool True if the lock has been released, false if there was a problem releasing the lock. + */ + public function release_lock($key); +} + +/** + * Cache store. + * + * This interface outlines the requirements for a cache store plugin. + * It must be implemented by all such plugins and provides a reference to interacting with cache stores. + * + * Must be implemented by all cache store plugins. + * + * @package core + * @category cache + * @copyright 2012 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +interface cache_store { + + /**#@+ + * Constants for features a cache store can support + */ + /** + * Supports multi-part keys + */ + const SUPPORTS_MULTIPLE_IDENTIFIERS = 1; + /** + * Ensures data remains in the cache once set. + */ + const SUPPORTS_DATA_GUARANTEE = 2; + /** + * Supports a native ttl system. + */ + const SUPPORTS_NATIVE_TTL = 4; + /**#@-*/ + + /**#@+ + * Constants for the modes of a cache store + */ + /** + * Application caches. These are shared caches. + */ + const MODE_APPLICATION = 1; + /** + * Session caches. Just access to the PHP session. + */ + const MODE_SESSION = 2; + /** + * Request caches. Static caches really. + */ + const MODE_REQUEST = 4; + /**#@-*/ + + /** + * Static method to check if the store requirements are met. + * + * @return bool True if the stores software/hardware requirements have been met and it can be used. False otherwise. + */ + public static function are_requirements_met(); + + /** + * Static method to check if a store is usable with the given mode. + * + * @param int $mode One of cache_store::MODE_* + */ + public static function is_supported_mode($mode); + + /** + * Returns the supported features as a binary flag. + * + * @param array $configuration The configuration of a store to consider specifically. + * @return int The supported features. + */ + public static function get_supported_features(array $configuration = array()); + + /** + * Returns the supported modes as a binary flag. + * + * @param array $configuration The configuration of a store to consider specifically. + * @return int The supported modes. + */ + public static function get_supported_modes(array $configuration = array()); + + /** + * Returns true if this cache store instance supports multiple identifiers. + * + * @return bool + */ + public function supports_multiple_indentifiers(); + + /** + * Returns true if this cache store instance promotes data guarantee. + * + * @return bool + */ + public function supports_data_guarantee(); + + /** + * Returns true if this cache store instance supports ttl natively. + * + * @return bool + */ + public function supports_native_ttl(); + + /** + * Used to control the ability to add an instance of this store through the admin interfaces. + * + * @return bool True if the user can add an instance, false otherwise. + */ + public static function can_add_instance(); + + /** + * Constructs an instance of the cache store. + * + * This method should not create connections or perform and processing, it should be used + * + * @param string $name The name of the cache store + * @param array $configuration The configuration for this store instance. + */ + public function __construct($name, array $configuration = array()); + + /** + * Initialises a new instance of the cache store given the definition the instance is to be used for. + * + * This function should prepare any given connections etc. + * + * @param cache_definition $definition + */ + public function initialise(cache_definition $definition); + + /** + * Returns true if this cache store instance has been initialised. + * @return bool + */ + public function is_initialised(); + + /** + * Returns true if this cache store instance is ready to use. + * @return bool + */ + public function is_ready(); + + /** + * Retrieves an item from the cache store given its key. + * + * @param string $key The key to retrieve + * @return mixed The data that was associated with the key, or false if the key did not exist. + */ + public function get($key); + + /** + * Retrieves several items from the cache store in a single transaction. + * + * If not all of the items are available in the cache then the data value for those that are missing will be set to false. + * + * @param array $keys The array of keys to retrieve + * @return array An array of items from the cache. There will be an item for each key, those that were not in the store will + * be set to false. + */ + public function get_many($keys); + + /** + * Sets an item in the cache given its key and data value. + * + * @param string $key The key to use. + * @param mixed $data The data to set. + * @return bool True if the operation was a success false otherwise. + */ + public function set($key, $data); + + /** + * Sets many items in the cache in a single transaction. + * + * @param array $keyvaluearray An array of key value pairs. Each item in the array will be an associative array with two + * keys, 'key' and 'value'. + * @return int The number of items successfully set. It is up to the developer to check this matches the number of items + * sent ... if they care that is. + */ + public function set_many(array $keyvaluearray); + + /** + * Deletes an item from the cache store. + * + * @param string $key The key to delete. + * @return bool Returns true if the operation was a success, false otherwise. + */ + public function delete($key); + + /** + * Deletes several keys from the cache in a single action. + * + * @param array $keys The keys to delete + * @return int The number of items successfully deleted. + */ + public function delete_many(array $keys); + + /** + * Purges the cache deleting all items within it. + * + * @return boolean True on success. False otherwise. + */ + public function purge(); + + /** + * Performs any necessary clean up when the store instance is being deleted. + */ + public function cleanup(); + + /** + * Generates an instance of the cache store that can be used for testing. + * + * Returns an instance of the cache store, or false if one cannot be created. + * + * @param cache_definition $definition + * @return cache_store|false + */ + public static function initialise_test_instance(cache_definition $definition); +} + +/** + * Cache store feature: locking + * + * This is a feature that cache stores can implement if they wish to support locking themselves rather + * than having the cache loader handle it for them. + * + * Can be implemented by classes already implementing cache_store. + */ +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. + * @return bool True if the lock could be acquired, false otherwise. + */ + public function acquire_lock($key, $identifier); + + /** + * 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. + * @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); + + /** + * 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. + * @return bool True if the lock has been released, false if there was a problem releasing the lock. + */ + public function release_lock($key, $identifier); +} + +/** + * Cache store feature: key awareness. + * + * This is a feature that cache stores and cache loaders can both choose to implement. + * If a cache store implements this then it will be made responsible for tests for items within the cache. + * If the cache store being used doesn't implement this then it will be the responsibility of the cache loader to use the + * equivalent get methods to mimick the functionality of these tests. + * + * Cache stores should only override these methods if they natively support such features or if they have a better performing + * means of performing these tests than the handling that would otherwise take place in the cache_loader. + * + * Can be implemented by classes already implementing cache_store. + */ +interface cache_is_key_aware { + + /** + * Test is a cache has a key. + * + * The use of the has methods is strongly discouraged. In a high load environment the cache may well change between the + * test and any subsequent action (get, set, delete etc). + * Instead it is recommended to write your code in such a way they it performs the following steps: + *
    + *
  1. Attempt to retrieve the information.
  2. + *
  3. Generate the information.
  4. + *
  5. Attempt to set the information
  6. + *
+ * + * Its also worth mentioning that not all stores support key tests. + * For stores that don't support key tests this functionality is mimicked by using the equivalent get method. + * Just one more reason you should not use these methods unless you have a very good reason to do so. + * + * @param string|int $key + * @return bool True if the cache has the requested key, false otherwise. + */ + public function has($key); + + /** + * Test if a cache has at least one of the given keys. + * + * It is strongly recommended to avoid the use of this function if not absolutely required. + * In a high load environment the cache may well change between the test and any subsequent action (get, set, delete etc). + * + * Its also worth mentioning that not all stores support key tests. + * For stores that don't support key tests this functionality is mimicked by using the equivalent get method. + * Just one more reason you should not use these methods unless you have a very good reason to do so. + * + * @param array $keys + * @return bool True if the cache has at least one of the given keys + */ + public function has_any(array $keys); + + /** + * Test is a cache has all of the given keys. + * + * It is strongly recommended to avoid the use of this function if not absolutely required. + * In a high load environment the cache may well change between the test and any subsequent action (get, set, delete etc). + * + * Its also worth mentioning that not all stores support key tests. + * For stores that don't support key tests this functionality is mimicked by using the equivalent get method. + * Just one more reason you should not use these methods unless you have a very good reason to do so. + * + * @param array $keys + * @return bool True if the cache has all of the given keys, false otherwise. + */ + public function has_all(array $keys); +} + +/** + * Cache Data Source. + * + * The cache data source interface can be implemented by any class within Moodle. + * If implemented then the class can be reference in a cache definition and will be used to load information that cannot be + * retrieved from the cache. As part of its retrieval that information will also be loaded into the cache. + * + * This allows developers to created a complete cache solution that can be used through code ensuring consistent cache + * interaction and loading. Allowing them in turn to centralise code and help keeps things more easily maintainable. + * + * Can be implemented by any class. + * + * @package core + * @category cache + * @copyright 2012 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +interface cache_data_source { + + /** + * Returns an instance of the data source class that the cache can use for loading data using the other methods + * specified by this interface. + * + * @param cache_definition $definition + * @return object + */ + public static function get_instance_for_cache(cache_definition $definition); + + /** + * Loads the data for the key provided ready formatted for caching. + * + * @param string|int $key The key to load. + * @return mixed What ever data should be returned, or false if it can't be loaded. + */ + public function load_for_cache($key); + + /** + * Loads several keys for the cache. + * + * @param array $keys An array of keys each of which will be string|int. + * @return array An array of matching data items. + */ + public function load_many_for_cache(array $keys); +} + +/** + * Cacheable object. + * + * This interface can be implemented by any class that is going to be passed into a cache and allows it to take control of the + * structure and the information about to be cached, as well as how to deal with it when it is retrieved from a cache. + * Think of it like serialisation and the __sleep and __wakeup methods. + * This is used because cache stores are responsible for how they interact with data and what they do when storing it. This + * interface ensures there is always a guaranteed action. + */ +interface cacheable_object { + + /** + * Prepares the object for caching. Works like the __sleep method. + * + * @return mixed The data to cache, can be anything except a class that implements the cacheable_object... that would + * be dumb. + */ + public function prepare_to_cache(); + + /** + * Takes the data provided by prepare_to_cache and reinitialises an instance of the associated from it. + * + * @param mixed $data + * @return object The instance for the given data. + */ + public static function wake_from_cache($data); +} diff --git a/cache/classes/loaders.php b/cache/classes/loaders.php new file mode 100644 index 00000000000..935ce63b14b --- /dev/null +++ b/cache/classes/loaders.php @@ -0,0 +1,1355 @@ +. + +/** + * Cache loaders + * + * This file is part of Moodle's cache API, affectionately called MUC. + * It contains the components that are requried in order to use caching. + * + * @package core + * @category cache + * @copyright 2012 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * The main cache class. + * + * This class if the first class that any end developer will interact with. + * In order to create an instance of a cache that they can work with they must call one of the static make methods belonging + * to this class. + * + * @package core + * @category cache + * @copyright 2012 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class cache implements cache_loader, cache_is_key_aware { + + /** + * We need a timestamp to use within the cache API. + * This stamp needs to be used for all ttl and time based operations to ensure that we don't end up with + * timing issues. + * @var int + */ + protected static $now; + + /** + * The definition used when loading this cache if there was one. + * @var cache_definition + */ + private $definition = false; + + /** + * The cache store that this loader will make use of. + * @var cache_store + */ + private $store; + + /** + * The next cache loader in the chain if there is one. + * If a cache request misses for the store belonging to this loader then the loader + * stored here will be checked next. + * If there is a loader here then $datasource must be false. + * @var cache_loader|false + */ + private $loader = false; + + /** + * The data source to use if we need to load data (because if doesn't exist in the cache store). + * If there is a data source here then $loader above must be false. + * @var cache_data_source|false + */ + private $datasource = false; + + /** + * Used to quickly check if the store supports key awareness. + * This is set when the cache is initialised and is used to speed up processing. + * @var bool + */ + private $supportskeyawareness = null; + + /** + * Used to quickly check if the store supports ttl natively. + * This is set when the cache is initialised and is used to speed up processing. + * @var bool + */ + private $supportsnativettl = null; + + /** + * Gets set to true if the cache is going to be using the build in static "persist" cache. + * The persist cache statically caches items used during the lifetime of the request. This greatly speeds up interaction + * with the cache in areas where it will be repetitively hit for the same information such as with strings. + * There are several other variables to control how this persist cache works. + * @var bool + */ + private $persist = false; + + /** + * The persist cache itself. + * Items will be stored in this cache as they were provided. This ensure there is no unnecessary processing taking place. + * @var array + */ + private $persistcache = array(); + + /** + * The number of items in the persist cache. Avoids count calls like you wouldn't believe. + * @var int + */ + private $persistcount = 0; + + /** + * An array containing just the keys being used in the persist cache. + * This seems redundant perhaps but is used when managing the size of the persist cache. + * @var array + */ + private $persistkeys = array(); + + /** + * The maximum size of the persist cache. If set to false there is no max size. + * Caches that make use of the persist cache should seriously consider setting this to something reasonably small, but + * still large enough to offset repetitive calls. + * @var int|false + */ + private $persistmaxsize = false; + + /** + * Gets set to true during initialisation if the definition is making use of a ttl. + * Used to speed up processing. + * @var bool + */ + private $hasattl = false; + + /** + * Gets set to the class name of the store during initialisation. This is used several times in the cache class internally + * and having it here helps speed up processing. + * @var strubg + */ + private $storetype = 'unknown'; + + /** + * Gets set to true if we want to collect performance information about the cache API. + * @var bool + */ + protected $perfdebug = false; + + /** + * Determines if this loader is a sub loader, not the top of the chain. + * @var bool + */ + protected $subloader = false; + + /** + * Creates a new cache instance for a pre-defined definition. + * + * @param string $component The component for the definition + * @param string $area The area for the definition + * @param array $identifiers Any additional identifiers that should be provided to the definition. + * @param string $aggregate Super advanced feature. More docs later. + * @return cache_application|cache_session|cache_store + */ + public static function make($component, $area, array $identifiers = array(), $aggregate = null) { + $factory = cache_factory::instance(); + return $factory->create_cache_from_definition($component, $area, $identifiers, $aggregate); + } + + /** + * Creates a new cache instance based upon the given params. + * + * @param int $mode One of cache_store::MODE_* + * @param string $component The component this cache relates to. + * @param string $area The area this cache relates to. + * @param array $identifiers Any additional identifiers that should be provided to the definition. + * @param bool $persistent If set to true the cache will persist construction requests. + * @return cache_application|cache_session|cache_store + */ + public static function make_from_params($mode, $component, $area, array $identifiers = array(), $persistent = false) { + $factory = cache_factory::instance(); + return $factory->create_cache_from_params($mode, $component, $area, $identifiers, $persistent); + } + + /** + * Constructs a new cache instance. + * + * You should not call this method from your code, instead you should use the cache::make methods. + * + * This method is public so that the cache_factory is able to instantiate cache instances. + * Ideally we would make this method protected and expose its construction to the factory method internally somehow. + * The factory class is responsible for this in order to centralise the storage of instances once created. This way if needed + * we can force a reset of the cache API (used during unit testing). + * + * @param cache_definition $definition The definition for the cache instance. + * @param cache_store $store The store that cache should use. + * @param cache_loader|cache_data_source $loader The next loader in the chain or the data source if there is one and there + * are no other cache_loaders in the chain. + */ + public function __construct(cache_definition $definition, cache_store $store, $loader = null) { + global $CFG; + $this->definition = $definition; + $this->store = $store; + $this->storetype = get_class($store); + $this->perfdebug = !empty($CFG->perfdebug); + if ($loader instanceof cache_loader) { + $this->loader = $loader; + // Mark the loader as a sub (chained) loader. + $this->loader->set_is_sub_loader(true); + } else if ($loader instanceof cache_data_source) { + $this->datasource = $loader; + } + $this->definition->generate_definition_hash(); + $this->persist = $this->definition->should_be_persistent(); + if ($this->persist) { + $this->persistmaxsize = $this->definition->get_persistent_max_size(); + } + $this->hasattl = ($this->definition->get_ttl() > 0); + } + + /** + * Used to inform the loader of its state as a sub loader, or as the top of the chain. + * + * This is important as it ensures that we do not have more than one loader keeping persistent data. + * Subloaders need to be "pure" loaders in the sense that they are used to store and retrieve information from stores or the + * next loader/data source in the chain. + * Nothing fancy, nothing flash. + * + * @param bool $setting + */ + protected function set_is_sub_loader($setting = true) { + if ($setting) { + $this->subloader = true; + // Subloaders should not keep persistent data. + $this->persist = false; + $this->persistmaxsize = false; + } else { + $this->subloader = true; + $this->persist = $this->definition->should_be_persistent(); + if ($this->persist) { + $this->persistmaxsize = $this->definition->get_persistent_max_size(); + } + } + } + + /** + * Alters the identifiers that have been provided to the definition. + * + * This is an advanced method and should not be used unless really needed. + * It allows the developer to slightly alter the definition without having to re-establish the cache. + * It will cause more processing as the definition will need to clear and reprepare some of its properties. + * + * @param array $identifiers + */ + public function set_identifiers(array $identifiers) { + $this->definition->set_identifiers($identifiers); + } + + /** + * Retrieves the value for the given key from the cache. + * + * @param string|int $key The key for the data being requested. + * It can be any structure although using a scalar string or int is recommended in the interests of performance. + * In advanced cases an array may be useful such as in situations requiring the multi-key functionality. + * @param int $strictness One of IGNORE_MISSING | MUST_EXIST + * @return mixed|false The data from the cache or false if the key did not exist within the cache. + * @throws moodle_exception + */ + public function get($key, $strictness = IGNORE_MISSING) { + // 1. Parse the key. + $parsedkey = $this->parse_key($key); + // 2. Get it from the persist cache if we can (only when persist is enabled and it has already been requested/set). + $result = $this->get_from_persist_cache($parsedkey); + if ($result !== false) { + if ($this->perfdebug) { + cache_helper::record_cache_hit('** static persist **', $this->definition->get_id()); + } + return $result; + } else if ($this->perfdebug) { + cache_helper::record_cache_miss('** static persist **', $this->definition->get_id()); + } + // 3. Get it from the store. Obviously wasn't in the persist cache. + $result = $this->store->get($parsedkey); + if ($result !== false) { + if ($result instanceof cache_ttl_wrapper) { + if ($result->has_expired()) { + $this->store->delete($parsedkey); + $result = false; + } else { + $result = $result->data; + } + } + if ($result instanceof cache_cached_object) { + $result = $result->restore_object(); + } + if ($this->is_using_persist_cache()) { + $this->set_in_persist_cache($parsedkey, $result); + } + } + // 4. Load if from the loader/datasource if we don't already have it. + $setaftervalidation = false; + if ($result === false) { + if ($this->perfdebug) { + cache_helper::record_cache_miss($this->storetype, $this->definition->get_id()); + } + if ($this->loader !== false) { + $result = $this->loader->get($parsedkey); + } else if ($this->datasource !== false) { + $result = $this->datasource->load_for_cache($key); + } + $setaftervalidation = ($result !== false); + } else if ($this->perfdebug) { + cache_helper::record_cache_hit($this->storetype, $this->definition->get_id()); + } + // 5. Validate strictness + if ($strictness === MUST_EXIST && $result === false) { + throw new moodle_exception('Requested key did not exist in any cache stores and could not be loaded.'); + } + // 6. Set it to the store if we got it from the loader/datasource. + if ($setaftervalidation) { + $this->set($key, $result); + } + return $result; + } + + /** + * Retrieves an array of values for an array of keys. + * + * Using this function comes with potential performance implications. + * Not all cache stores will support get_many/set_many operations and in order to replicate this functionality will call + * the equivalent singular method for each item provided. + * This should not deter you from using this function as there is a performance benefit in situations where the cache store + * does support it, but you should be aware of this fact. + * + * @param array $keys The keys of the data being requested. + * Each key can be any structure although using a scalar string or int is recommended in the interests of performance. + * In advanced cases an array may be useful such as in situations requiring the multi-key functionality. + * @param int $strictness One of IGNORE_MISSING or MUST_EXIST. + * @return array An array of key value pairs for the items that could be retrieved from the cache. + * If MUST_EXIST was used and not all keys existed within the cache then an exception will be thrown. + * Otherwise any key that did not exist will have a data value of false within the results. + * @throws moodle_exception + */ + public function get_many(array $keys, $strictness = IGNORE_MISSING) { + + $parsedkeys = array(); + $resultpersist = array(); + $resultstore = array(); + $keystofind = array(); + + // First up check the persist cache for each key + $isusingpersist = $this->is_using_persist_cache(); + foreach ($keys as $key) { + $pkey = $this->parse_key($key); + $parsedkeys[$pkey] = $key; + $keystofind[$pkey] = $key; + if ($isusingpersist) { + $value = $this->get_from_persist_cache($pkey); + if ($value !== false) { + $resultpersist[$pkey] = $value; + unset($keystofind[$pkey]); + } + } + } + + // Next assuming we didn't find all of the keys in the persist cache try loading them from the store. + if (count($keystofind)) { + $resultstore = $this->store->get_many(array_keys($keystofind)); + // Process each item in the result to "unwrap" it. + foreach ($resultstore as $key => $value) { + if ($value instanceof cache_ttl_wrapper) { + if ($value->has_expired()) { + $value = false; + } else { + $value = $value->data; + } + } + if ($value instanceof cache_cached_object) { + $value = $value->restore_object(); + } + $resultstore[$key] = $value; + } + } + + // Merge the result from the persis cache with the results from the store load. + $result = $resultpersist + $resultstore; + unset($resultpersist); + unset($resultstore); + + // Next we need to find any missing values and load them from the loader/datasource next in the chain. + $usingloader = ($this->loader !== false); + $usingsource = (!$usingloader && ($this->datasource !== false)); + if ($usingloader || $usingsource) { + $missingkeys = array(); + foreach ($result as $key => $value) { + if ($value === false) { + $missingkeys[] = ($usingloader) ? $key : $parsedkeys[$key]; + } + } + if (!empty($missingkeys)) { + if ($usingloader) { + $resultmissing = $this->loader->get_many($missingkeys); + } else { + $resultmissing = $this->datasource->load_many_for_cache($missingkeys); + } + foreach ($resultmissing as $key => $value) { + $result[$key] = $value; + if ($value !== false) { + $this->set($parsedkeys[$key], $value); + } + } + unset($resultmissing); + } + unset($missingkeys); + } + + // Create an array with the original keys and the found values. This will be what we return. + $fullresult = array(); + foreach ($result as $key => $value) { + $fullresult[$parsedkeys[$key]] = $value; + } + unset($result); + + // Final step is to check strictness + if ($strictness === MUST_EXIST) { + foreach ($keys as $key) { + if (!array_key_exists($key, $fullresult)) { + throw new moodle_exception('Not all the requested keys existed within the cache stores.'); + } + } + } + + // Return the result. Phew! + return $fullresult; + } + + /** + * Sends a key => value pair to the cache. + * + * + * // This code will add four entries to the cache, one for each url. + * $cache->set('main', 'http://moodle.org'); + * $cache->set('docs', 'http://docs.moodle.org'); + * $cache->set('tracker', 'http://tracker.moodle.org'); + * $cache->set('qa', 'http://qa.moodle.net'); + * + * + * @param string|int $key The key for the data being requested. + * It can be any structure although using a scalar string or int is recommended in the interests of performance. + * In advanced cases an array may be useful such as in situations requiring the multi-key functionality. + * @param mixed $data The data to set against the key. + * @return bool True on success, false otherwise. + */ + public function set($key, $data) { + if ($this->perfdebug) { + cache_helper::record_cache_set($this->storetype, $this->definition->get_id()); + } + if (is_object($data) && $data instanceof cacheable_object) { + $data = new cache_cached_object($data); + } + if ($this->has_a_ttl() && !$this->store_supports_native_ttl()) { + $data = new cache_ttl_wrapper($data, $this->definition->get_ttl()); + } + $parsedkey = $this->parse_key($key); + if ($this->is_using_persist_cache()) { + $this->set_in_persist_cache($parsedkey, $data); + } + return $this->store->set($parsedkey, $data); + } + + /** + * Sends several key => value pairs to the cache. + * + * Using this function comes with potential performance implications. + * Not all cache stores will support get_many/set_many operations and in order to replicate this functionality will call + * the equivalent singular method for each item provided. + * This should not deter you from using this function as there is a performance benefit in situations where the cache store + * does support it, but you should be aware of this fact. + * + * + * // This code will add four entries to the cache, one for each url. + * $cache->set_many(array( + * 'main' => 'http://moodle.org', + * 'docs' => 'http://docs.moodle.org', + * 'tracker' => 'http://tracker.moodle.org', + * 'qa' => ''http://qa.moodle.net' + * )); + * + * + * @param array $keyvaluearray An array of key => value pairs to send to the cache. + * @return int The number of items successfully set. It is up to the developer to check this matches the number of items. + * ... if they care that is. + */ + public function set_many(array $keyvaluearray) { + $data = array(); + $simulatettl = $this->has_a_ttl() && !$this->store_supports_native_ttl(); + $usepersistcache = $this->is_using_persist_cache(); + foreach ($keyvaluearray as $key => $value) { + if (is_object($value) && $value instanceof cacheable_object) { + $value = new cache_cached_object($value); + } + if ($simulatettl) { + $value = new cache_ttl_wrapper($value, $this->definition->get_ttl()); + } + $data[$key] = array( + 'key' => $this->parse_key($key), + 'value' => $value + ); + if ($usepersistcache) { + $this->set_in_persist_cache($data[$key]['key'], $value); + } + } + if ($this->perfdebug) { + cache_helper::record_cache_set($this->storetype, $this->definition->get_id()); + } + return $this->store->set_many($data); + } + + /** + * Test is a cache has a key. + * + * The use of the has methods is strongly discouraged. In a high load environment the cache may well change between the + * test and any subsequent action (get, set, delete etc). + * Instead it is recommended to write your code in such a way they it performs the following steps: + *
    + *
  1. Attempt to retrieve the information.
  2. + *
  3. Generate the information.
  4. + *
  5. Attempt to set the information
  6. + *
+ * + * Its also worth mentioning that not all stores support key tests. + * For stores that don't support key tests this functionality is mimicked by using the equivalent get method. + * Just one more reason you should not use these methods unless you have a very good reason to do so. + * + * @param string|int $key + * @param bool $tryloadifpossible If set to true, the cache doesn't contain the key, and there is another cache loader or + * data source then the code will try load the key value from the next item in the chain. + * @return bool True if the cache has the requested key, false otherwise. + */ + public function has($key, $tryloadifpossible = false) { + $parsedkey = $this->parse_key($key); + if ($this->is_in_persist_cache($parsedkey)) { + return true; + } + if (($this->has_a_ttl() && !$this->store_supports_native_ttl()) || !$this->store_supports_key_awareness()) { + if ($this->store_supports_key_awareness() && !$this->store->has($parsedkey)) { + return false; + } + $data = $this->store->get($parsedkey); + if (!$this->store_supports_native_ttl()) { + $has = ($data instanceof cache_ttl_wrapper && !$data->has_expired()); + } else { + $has = ($data !== false); + } + } else { + $has = $this->store->has($parsedkey); + } + if (!$has && $tryloadifpossible) { + if ($this->loader !== false) { + $result = $this->loader->get($parsedkey); + } else if ($this->datasource !== null) { + $result = $this->datasource->load_for_cache($key); + } + $has = ($result !== null); + if ($has) { + $this->set($key, $result); + } + } + return $has; + } + + /** + * Test is a cache has all of the given keys. + * + * It is strongly recommended to avoid the use of this function if not absolutely required. + * In a high load environment the cache may well change between the test and any subsequent action (get, set, delete etc). + * + * Its also worth mentioning that not all stores support key tests. + * For stores that don't support key tests this functionality is mimicked by using the equivalent get method. + * Just one more reason you should not use these methods unless you have a very good reason to do so. + * + * @param array $keys + * @return bool True if the cache has all of the given keys, false otherwise. + */ + public function has_all(array $keys) { + if (($this->has_a_ttl() && !$this->store_supports_native_ttl()) || !$this->store_supports_key_awareness()) { + foreach ($keys as $key) { + if (!$this->has($key)) { + return false; + } + } + return true; + } + $parsedkeys = array_map(array($this, 'parse_key'), $keys); + return $this->store->has_all($parsedkeys); + } + + /** + * Test if a cache has at least one of the given keys. + * + * It is strongly recommended to avoid the use of this function if not absolutely required. + * In a high load environment the cache may well change between the test and any subsequent action (get, set, delete etc). + * + * Its also worth mentioning that not all stores support key tests. + * For stores that don't support key tests this functionality is mimicked by using the equivalent get method. + * Just one more reason you should not use these methods unless you have a very good reason to do so. + * + * @param array $keys + * @return bool True if the cache has at least one of the given keys + */ + public function has_any(array $keys) { + if (($this->has_a_ttl() && !$this->store_supports_native_ttl()) || !$this->store_supports_key_awareness()) { + foreach ($keys as $key) { + if ($this->has($key)) { + return true; + } + } + return false; + } + + if ($this->is_using_persist_cache()) { + $parsedkeys = array(); + foreach ($keys as $id => $key) { + $parsedkey = $this->parse_key($key); + if ($this->is_in_persist_cache($parsedkey)) { + return true; + } + $parsedkeys[] = $parsedkey; + } + } else { + $parsedkeys = array_map(array($this, 'parse_key'), $keys); + } + return $this->store->has_any($parsedkeys); + } + + /** + * Delete the given key from the cache. + * + * @param string|int $key The key to delete. + * @param bool $recurse When set to true the key will also be deleted from all stacked cache loaders and their stores. + * This happens by default and ensure that all the caches are consistent. It is NOT recommended to change this. + * @return bool True of success, false otherwise. + */ + public function delete($key, $recurse = true) { + $parsedkey = $this->parse_key($key); + $this->delete_from_persist_cache($parsedkey); + if ($recurse && !empty($this->loader)) { + // Delete from the bottom of the stack first + $this->loader->delete($key, $recurse); + } + return $this->store->delete($parsedkey); + } + + /** + * Delete all of the given keys from the cache. + * + * @param array $keys The key to delete. + * @param bool $recurse When set to true the key will also be deleted from all stacked cache loaders and their stores. + * This happens by default and ensure that all the caches are consistent. It is NOT recommended to change this. + * @return int The number of items successfully deleted. + */ + public function delete_many(array $keys, $recurse = true) { + $parsedkeys = array_map(array($this, 'parse_key'), $keys); + if ($this->is_using_persist_cache()) { + foreach ($parsedkeys as $parsedkey) { + $this->delete_from_persist_cache($parsedkey); + } + } + if ($recurse && !empty($this->loader)) { + // Delete from the bottom of the stack first + $this->loader->delete_many($keys, $recurse); + } + return $this->store->delete_many($parsedkeys); + } + + /** + * Purges the cache store, and loader if there is one. + * + * @return bool True on success, false otherwise + */ + public function purge() { + // 1. Purge the persist cache + $this->persistcache = array(); + // 2. Purge the store + $this->store->purge(); + // 3. Optionally pruge any stacked loaders + if ($this->loader) { + $this->loader->purge(); + } + return true; + } + + /** + * Parses the key turning it into a string (or array is required) suitable to be passed to the cache store. + * + * @param string|int $key As passed to get|set|delete etc. + * @return string|array String unless the store supports multi-identifiers in which case an array if returned. + */ + protected function parse_key($key) { + if ($this->store->supports_multiple_indentifiers()) { + $result = $this->definition->generate_multi_key_parts(); + $result['key'] = $key; + return $result; + } + return cache_helper::hash_key($this->definition->generate_single_key_prefix().'-'.$key); + } + + /** + * Returns true if the cache is making use of a ttl. + * @return bool + */ + protected function has_a_ttl() { + return $this->hasattl; + } + + /** + * Returns true if the cache store supports native ttl. + * @return bool + */ + protected function store_supports_native_ttl() { + if ($this->supportsnativettl === null) { + $this->supportsnativettl = ($this->store->supports_native_ttl()); + } + return $this->supportsnativettl; + } + + /** + * Returns the cache definition. + * + * @return cache_definition + */ + protected function get_definition() { + return $this->definition; + } + + /** + * Returns the cache store + * + * @return cache_store + */ + protected function get_store() { + return $this->store; + } + + /** + * Returns true if the store supports key awareness. + * + * @return bool + */ + protected function store_supports_key_awareness() { + if ($this->supportskeyawareness === null) { + $this->supportskeyawareness = ($this->store instanceof cache_is_key_aware); + } + return $this->supportskeyawareness; + } + + /** + * Returns true if the store natively supports locking. + * + * @return bool + */ + protected function store_supports_native_locking() { + if ($this->nativelocking === null) { + $this->nativelocking = ($this->store instanceof cache_is_lockable); + } + return $this->nativelocking; + } + + /** + * Returns true if this cache is making use of the persist cache. + * + * @return bool + */ + protected function is_using_persist_cache() { + return $this->persist; + } + + /** + * Returns true if the requested key exists within the persist cache. + * + * @param string $key The parsed key + * @return bool + */ + protected function is_in_persist_cache($key) { + if (is_array($key)) { + $key = $key['key']; + } + // This could be written as a single line, however it has been split because the ttl check is faster than the instanceof + // and has_expired calls. + if (!$this->persist || !array_key_exists($key, $this->persistcache)) { + return false; + } + if ($this->has_a_ttl() && $this->store_supports_native_ttl()) { + return !($this->persistcache[$key] instanceof cache_ttl_wrapper && $this->persistcache[$key]->has_expired()); + } + return true; + } + + /** + * Returns the item from the persist cache if it exists there. + * + * @param string $key The parsed key + * @return mixed|false The data from the persist cache or false if it wasn't there. + */ + protected function get_from_persist_cache($key) { + if (is_array($key)) { + $key = $key['key']; + } + if (!$this->persist || !array_key_exists($key, $this->persistcache)) { + return false; + } + $data = $this->persistcache[$key]; + if (!$this->has_a_ttl() || !$data instanceof cache_ttl_wrapper) { + if ($data instanceof cache_cached_object) { + $data = $data->restore_object(); + } + return $data; + } + if ($data->has_expired()) { + $this->delete_from_persist_cache($key); + return false; + } else { + if ($data instanceof cache_cached_object) { + $data = $data->restore_object(); + } + return $data->data; + } + } + + /** + * Sets a key value pair into the persist cache. + * + * @param string $key The parsed key + * @param mixed $data + * @return bool + */ + protected function set_in_persist_cache($key, $data) { + if (is_array($key)) { + $key = $key['key']; + } + $this->persistcache[$key] = $data; + if ($this->persistmaxsize !== false) { + $this->persistcount++; + if ($this->persistcount > $this->persistmaxsize) { + $dropkey = array_shift($this->persistkeys); + unset($this->persistcache[$dropkey]); + $this->persistcount--; + } + } + return true; + } + + /** + * Deletes an item from the persist cache. + * + * @param string|int $key As given to get|set|delete + * @return bool True on success, false otherwise. + */ + protected function delete_from_persist_cache($key) { + unset($this->persistcache[$key]); + if ($this->persistmaxsize !== false) { + $dropkey = array_search($key, $this->persistkeys); + if ($dropkey) { + unset($this->persistkeys[$dropkey]); + $this->persistcount--; + } + } + return true; + } + + /** + * Returns the timestamp from the first request for the time from the cache API. + * + * This stamp needs to be used for all ttl and time based operations to ensure that we don't end up with + * timing issues. + * + * @return int + */ + public static function now() { + if (self::$now === null) { + self::$now = time(); + } + return self::$now; + } +} + +/** + * An application cache. + * + * This class is used for application caches returned by the cache::make methods. + * On top of the standard functionality it also allows locking to be required and or manually operated. + * + * This cache class should never be interacted with directly. Instead you should always use the cache::make methods. + * It is technically possible to call those methods through this class however there is no guarantee that you will get an + * instance of this class back again. + * + * @internal don't use me directly. + * + * @package core + * @category cache + * @copyright 2012 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class cache_application extends cache implements cache_loader_with_locking { + + /** + * Lock identifier. + * This is used to ensure the lock belongs to the cache instance + definition + user. + * @var string + */ + protected $lockidentifier; + + /** + * Gets set to true if the cache's primary store natively supports locking. + * If it does then we use that, otherwise we need to instantiate a second store to use for locking. + * @var cache_store + */ + protected $nativelocking = null; + + /** + * Gets set to true if the cache is going to be using locking. + * This isn't a requirement, it doesn't need to use locking (most won't) and this bool is used to quickly check things. + * If required then locking will be forced for the get|set|delete operation. + * @var bool + */ + protected $requirelocking = false; + + /** + * Gets set to true if the cache must use read locking (get|has). + * @var bool + */ + protected $requirelockingread = false; + + /** + * Gets set to true if the cache must use write locking (set|delete) + * @var bool + */ + protected $requirelockingwrite = false; + + /** + * Gets set to a cache_store to use for locking if the caches primary store doesn't support locking natively. + * @var cache_store + */ + protected $lockstore; + + /** + * Overrides the cache construct method. + * + * You should not call this method from your code, instead you should use the cache::make methods. + * + * @param cache_definition $definition + * @param cache_store $store + * @param cache_loader|cache_data_source $loader + */ + public function __construct(cache_definition $definition, cache_store $store, $loader = null) { + parent::__construct($definition, $store, $loader); + $this->nativelocking = $this->store_supports_native_locking(); + if ($definition->require_locking()) { + $this->requirelocking = true; + $this->requirelockingread = $definition->require_locking_read(); + $this->requirelockingwrite = $definition->require_locking_write(); + } + + if ($definition->has_invalidation_events()) { + $lastinvalidation = $this->get('lastinvalidation'); + if ($lastinvalidation === false) { + // This is a new session, there won't be anything to invalidate. Set the time of the last invalidation and + // move on. + $this->set('lastinvalidation', cache::now()); + return; + } else if ($lastinvalidation == cache::now()) { + // We've already invalidated during this request. + return; + } + + // Get the event invalidation cache. + $cache = cache::make('core', 'eventinvalidation'); + $events = $cache->get_many($definition->get_invalidation_events()); + $todelete = array(); + // Iterate the returned data for the events. + foreach ($events as $event => $keys) { + // Look at each key and check the timestamp + foreach ($keys as $key => $timestamp) { + // If the timestamp of the event is more than or equal to the last invalidation (happened between the last + // invalidation and now)then we need to invaliate the key. + if ($timestamp >= $lastinvalidation) { + $todelete[] = $key; + } + } + } + if (!empty($todelete)) { + $todelete = array_unique($todelete); + $this->delete_many($todelete); + } + // Set the time of the last invalidation + $this->set('lastinvalidation', cache::now()); + } + } + + /** + * Returns the identifier to use + * + * @staticvar int $instances Counts the number of instances. Used as part of the lock identifier. + * @return string + */ + public function get_identifier() { + static $instances = 0; + if ($this->lockidentifier === null) { + $this->lockidentifier = md5($this->get_definition()->generate_definition_hash().sesskey().$instances++.'cache_application'); + } + return $this->lockidentifier; + } + + /** + * Fixes the instance up after a clone. + */ + public function __clone() { + // Force a new idenfitier + $this->lockidentifier = null; + } + + /** + * Acquires a lock on the given key. + * + * This is done automatically if the definition requires it. + * It is recommended to use a definition if you want to have locking although it is possible to do locking without having + * it required by the definition. + * The problem with such an approach is that you cannot ensure that code will consistently use locking. You will need to + * rely on the integrators review skills. + * + * @param string|int $key The key as given to get|set|delete + * @return bool Returns true if the lock could be acquired, false otherwise. + */ + public function acquire_lock($key) { + $key = $this->parse_key($key); + 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()); + } + } + + /** + * Checks if this cache has a lock on the given key. + * + * @param string|int $key The key as given to get|set|delete + * @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) { + $key = $this->parse_key($key); + if ($this->nativelocking) { + return $this->get_store()->has_lock($key, $this->get_identifier()); + } else { + $this->ensure_lock_store_available(); + return $this->lockstore->has_lock($key, $this->get_identifier()); + } + } + + /** + * Releases the lock this cache has on the given key + * + * @param string|int $key + * @return bool True if the operation succeeded, false otherwise. + */ + public function release_lock($key) { + $key = $this->parse_key($key); + 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()); + } + } + + /** + * Ensure that the dedicated lock store is ready to go. + * + * 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_cache_store_for_locking(); + } + } + + /** + * Sends a key => value pair to the cache. + * + * + * // This code will add four entries to the cache, one for each url. + * $cache->set('main', 'http://moodle.org'); + * $cache->set('docs', 'http://docs.moodle.org'); + * $cache->set('tracker', 'http://tracker.moodle.org'); + * $cache->set('qa', 'http://qa.moodle.net'); + * + * + * @param string|int $key The key for the data being requested. + * @param mixed $data The data to set against the key. + * @return bool True on success, false otherwise. + */ + public function set($key, $data) { + if ($this->requirelockingwrite && !$this->acquire_lock($key)) { + return false; + } + $result = parent::set($key, $data); + if ($this->requirelockingwrite && !$this->release_lock($key)) { + debugging('Failed to release cache lock on set operation... this should not happen.', DEBUG_DEVELOPER); + } + return $result; + } + + /** + * Sends several key => value pairs to the cache. + * + * Using this function comes with potential performance implications. + * Not all cache stores will support get_many/set_many operations and in order to replicate this functionality will call + * the equivalent singular method for each item provided. + * This should not deter you from using this function as there is a performance benefit in situations where the cache store + * does support it, but you should be aware of this fact. + * + * + * // This code will add four entries to the cache, one for each url. + * $cache->set_many(array( + * 'main' => 'http://moodle.org', + * 'docs' => 'http://docs.moodle.org', + * 'tracker' => 'http://tracker.moodle.org', + * 'qa' => ''http://qa.moodle.net' + * )); + * + * + * @param array $keyvaluearray An array of key => value pairs to send to the cache. + * @return int The number of items successfully set. It is up to the developer to check this matches the number of items. + * ... if they care that is. + */ + public function set_many(array $keyvaluearray) { + if ($this->requirelockingwrite) { + $locks = array(); + foreach ($keyvaluearray as $id => $pair) { + $key = $pair['key']; + if ($this->acquire_lock($key)) { + $locks[] = $key; + } else { + unset($keyvaluearray[$id]); + } + } + } + $result = parent::set_many($keyvaluearray); + if ($this->requirelockingwrite) { + foreach ($locks as $key) { + if ($this->release_lock($key)) { + debugging('Failed to release cache lock on set_many operation... this should not happen.', DEBUG_DEVELOPER); + } + } + } + return $result; + } + + /** + * Retrieves the value for the given key from the cache. + * + * @param string|int $key The key for the data being requested. + * @param int $strictness One of IGNORE_MISSING | MUST_EXIST + * @return mixed|false The data from the cache or false if the key did not exist within the cache. + * @throws moodle_exception + */ + public function get($key, $strictness = IGNORE_MISSING) { + if ($this->requirelockingread && $this->has_lock($key) === false) { + // Read locking required and someone else has the read lock. + return false; + } + return parent::get($key, $strictness); + } + + /** + * Retrieves an array of values for an array of keys. + * + * Using this function comes with potential performance implications. + * Not all cache stores will support get_many/set_many operations and in order to replicate this functionality will call + * the equivalent singular method for each item provided. + * This should not deter you from using this function as there is a performance benefit in situations where the cache store + * does support it, but you should be aware of this fact. + * + * @param array $keys The keys of the data being requested. + * @param int $strictness One of IGNORE_MISSING or MUST_EXIST. + * @return array An array of key value pairs for the items that could be retrieved from the cache. + * If MUST_EXIST was used and not all keys existed within the cache then an exception will be thrown. + * Otherwise any key that did not exist will have a data value of false within the results. + * @throws moodle_exception + */ + public function get_many(array $keys, $strictness = IGNORE_MISSING) { + if ($this->requirelockingread) { + foreach ($keys as $id => $key) { + $lock =$this->acquire_lock($key); + if (!$lock) { + if ($strictness === MUST_EXIST) { + throw new coding_exception('Could not acquire read locks for all of the items being requested.'); + } else { + // Can't return this as we couldn't get a read lock. + unset($keys[$id]); + } + } + + } + } + return parent::get_many($keys, $strictness); + } + + /** + * Delete the given key from the cache. + * + * @param string|int $key The key to delete. + * @param bool $recurse When set to true the key will also be deleted from all stacked cache loaders and their stores. + * This happens by default and ensure that all the caches are consistent. It is NOT recommended to change this. + * @return bool True of success, false otherwise. + */ + public function delete($key, $recurse = true) { + if ($this->requirelockingwrite && !$this->acquire_lock($key)) { + return false; + } + $result = parent::delete($key, $recurse); + if ($this->requirelockingwrite && !$this->release_lock($key)) { + debugging('Failed to release cache lock on delete operation... this should not happen.', DEBUG_DEVELOPER); + } + return $result; + } + + /** + * Delete all of the given keys from the cache. + * + * @param array $keys The key to delete. + * @param bool $recurse When set to true the key will also be deleted from all stacked cache loaders and their stores. + * This happens by default and ensure that all the caches are consistent. It is NOT recommended to change this. + * @return int The number of items successfully deleted. + */ + public function delete_many(array $keys, $recurse = true) { + if ($this->requirelockingwrite) { + $locks = array(); + foreach ($keys as $id => $key) { + if ($this->acquire_lock($key)) { + $locks[] = $key; + } else { + unset($keys[$id]); + } + } + } + $result = parent::delete_many($keys, $recurse); + if ($this->requirelockingwrite) { + foreach ($locks as $key) { + if ($this->release_lock($key)) { + debugging('Failed to release cache lock on delete_many operation... this should not happen.', DEBUG_DEVELOPER); + } + } + } + return $result; + } +} + +/** + * A session cache. + * + * This class is used for session caches returned by the cache::make methods. + * + * This cache class should never be interacted with directly. Instead you should always use the cache::make methods. + * It is technically possible to call those methods through this class however there is no guarantee that you will get an + * instance of this class back again. + * + * @todo we should support locking in the session as well. Should be pretty simple to set up. + * + * @internal don't use me directly. + * + * @package core + * @category cache + * @copyright 2012 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class cache_session extends cache { + /** + * Override the cache::construct method. + * + * This function gets overriden so that we can process any invalidation events if need be. + * If the definition doesn't have any invalidation events then this occurs exactly as it would for the cache class. + * Otherwise we look at the last invalidation time and then check the invalidation data for events that have occured + * between then now. + * + * You should not call this method from your code, instead you should use the cache::make methods. + * + * @param cache_definition $definition + * @param cache_store $store + * @param cache_loader|cache_data_source $loader + * @return void + */ + public function __construct(cache_definition $definition, cache_store $store, $loader = null) { + parent::__construct($definition, $store, $loader); + if ($definition->has_invalidation_events()) { + $lastinvalidation = $this->get('lastsessioninvalidation'); + if ($lastinvalidation === false) { + // This is a new session, there won't be anything to invalidate. Set the time of the last invalidation and + // move on. + $this->set('lastsessioninvalidation', cache::now()); + return; + } else if ($lastinvalidation == cache::now()) { + // We've already invalidated during this request. + return; + } + + // Get the event invalidation cache. + $cache = cache::make('core', 'eventinvalidation'); + $events = $cache->get_many($definition->get_invalidation_events()); + $todelete = array(); + // Iterate the returned data for the events. + foreach ($events as $event => $keys) { + if ($keys === false) { + // No data to be invalidated yet + continue; + } + // Look at each key and check the timestamp + foreach ($keys as $key => $timestamp) { + // If the timestamp of the event is more than or equal to the last invalidation (happened between the last + // invalidation and now)then we need to invaliate the key. + if ($timestamp >= $lastinvalidation) { + $todelete[] = $key; + } + } + } + if (!empty($todelete)) { + $todelete = array_unique($todelete); + $this->delete_many($todelete); + } + // Set the time of the last invalidation + $this->set('lastsessioninvalidation', cache::now()); + } + } +} + +/** + * An request cache. + * + * This class is used for request caches returned by the cache::make methods. + * + * This cache class should never be interacted with directly. Instead you should always use the cache::make methods. + * It is technically possible to call those methods through this class however there is no guarantee that you will get an + * instance of this class back again. + * + * @internal don't use me directly. + * + * @package core + * @category cache + * @copyright 2012 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class cache_request extends cache {} \ No newline at end of file diff --git a/cache/classes/lock.php b/cache/classes/lock.php new file mode 100644 index 00000000000..195f5fa17fa --- /dev/null +++ b/cache/classes/lock.php @@ -0,0 +1,250 @@ +. + +/** + * Cache lock class. Used for locking when required. + * + * @package core + * @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. + * + * 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); + } + + /** + * 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 + */ + public static function unlock($key) { + $key = md5($key); + $instance = self::instance(); + return $instance->_unlock($key); + } + + /** + * Resets the cache lock class, reinitialising it. + */ + public static function reset() { + self::instance(true); + } + + /** + * Returns an instance of the cache lock class. + * + * @staticvar bool $instance + * @return cache_lock + */ + protected static function instance($forceregeneration = false) { + static $instance = false; + if (!$instance || $forceregeneration) { + $instance = new cache_lock(); + } + return $instance; + } + + /** + * The directory in which lock files will be created + * @var string + */ + protected $cachedir; + + /** + * An array of lock files currently held by this cache lock instance. + * @var array + */ + 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. + */ + public function __destruct() { + foreach ($this->locks as $lockfile) { + // Naught, naughty developers. + @unlink($lockfile); + } + } + + /** + * Acquires a lock, of dies trying (jokes). + * + * Read {@see cache_lock::lock()} for full details. + * + * @param string $key + * @param bool $block + * @param int|null $maxlife + * @return bool + * @throws cache_exception + */ + protected function _lock($key, $block = true, $maxlife = null) { + // Get the name of the lock file we want to use. + $lockfile = $this->get_lock_file($key); + + // Attempt to create a handle to the lock file. + // Mode xb is the secret to this whole function. + // x = Creates the file and opens it for writing. If the file already exists fopen returns false and a warning is thrown. + // b = Forces binary mode. + $result = @fopen($lockfile, 'xb'); + + // Check if we could create the file or not. + if ($result === false) { + // Lock exists already. + if ($maxlife !== null) { + $mtime = filemtime($lockfile); + if ($mtime < time() - $maxlife) { + $this->_unlock($key, true); + $result = $this->_lock($key, false); + if ($result) { + return true; + } + } + } + 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) { + // 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 + $iterations++; + if ($iterations > $maxiterations) { + // BOOM! We've exceeded the maximum number of iterations we want to block for. + throw new cache_exception('ex_unabletolock'); + } + } + } + + return false; + } else { + // We have the lock. + fclose($result); + $this->locks[$key] = $lockfile; + return true; + } + } + + /** + * Releases an acquired lock. + * + * For more details see {@see cache_lock::unlock()} + * + * @param string $key + * @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) { + if (array_key_exists($key, $this->locks)) { + @unlink($this->locks[$key]); + unset($this->locks[$key]); + return true; + } else if ($forceunlock) { + $lockfile = $this->get_lock_file($key); + if (file_exists($lockfile)) { + @unlink($lockfile); + } + return true; + } + // You cannot unlock a file you didn't lock. + return false; + } + + /** + * Gets the name to use for a lock file. + * + * @param string $key + * @return string + */ + protected function get_lock_file($key) { + return $this->cachedir.'/'. $key .'.lock'; + } +} \ No newline at end of file diff --git a/cache/forms.php b/cache/forms.php new file mode 100644 index 00000000000..488e9997b12 --- /dev/null +++ b/cache/forms.php @@ -0,0 +1,200 @@ +. + +/** + * Forms used for the administration and managemement of the cache setup. + * + * This file is part of Moodle's cache API, affectionately called MUC. + * + * @package core + * @category cache + * @copyright 2012 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot.'/lib/formslib.php'); + +/** + * Add store instance form. + * + * @package core + * @category cache + * @copyright 2012 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class cache_store_addinstance_form extends moodleform { + + /** + * The definition of the add instance form + */ + protected final function definition() { + $form = $this->_form; + $store = $this->_customdata['store']; + $plugin = $this->_customdata['plugin']; + + $form->addElement('hidden', 'plugin', $plugin); + $form->addElement('hidden', 'editing', !empty($this->_customdata['store'])); + + if (!$store) { + $form->addElement('text', 'name', get_string('storename', 'cache')); + $form->addHelpButton('name', 'storename', 'cache'); + $form->addRule('name', get_string('required'), 'required'); + $form->setType('name', PARAM_TEXT); + } else { + $form->addElement('hidden', 'name', $store); + $form->addElement('static', 'name-value', get_string('storename', 'cache'), $store); + } + + + if (method_exists($this, 'configuration_definition')) { + $form->addElement('header', 'storeconfiguration', get_string('storeconfiguration', 'cache')); + $this->configuration_definition(); + } + + $this->add_action_buttons(); + } + + /** + * Validates the add instance form data + * + * @param array $data + * @param array $files + * @return array + */ + public function validation($data, $files) { + $errors = parent::validation($data, $files); + + if (!array_key_exists('name', $errors)) { + if (!preg_match('#^[a-zA-Z0-9\-_ ]+$#', $data['name'])) { + $errors['name'] = get_string('storenameinvalid', 'cache'); + } else if (empty($this->_customdata['store'])) { + $stores = cache_administration_helper::get_store_summaries(); + if (array_key_exists($data['name'], $stores)) { + $errors['name'] = get_string('storenamealreadyused', 'cache'); + } + } + } + + if (method_exists($this, 'configuration_validation')) { + $errors = $this->configuration_validation($data, $files); + } + + return $errors; + } +} + +/** + * Form to set definition mappings + * + * @package core + * @category cache + * @copyright 2012 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class cache_definition_mappings_form extends moodleform { + + /** + * The definition of the form + */ + protected final function definition() { + $definition = $this->_customdata['definition']; + $form = $this->_form; + + list($component, $area) = explode('/', $definition, 2); + list($currentstores, $storeoptions, $defaults) = cache_administration_helper::get_definition_store_options($component, $area); + + $form->addElement('hidden', 'definition', $definition); + $form->addElement('hidden', 'action', 'editdefinitionmapping'); + + $requiredoptions = max(3, count($currentstores)+1); + $requiredoptions = min($requiredoptions, count($storeoptions)); + + + $options = array('' => get_string('none')); + foreach ($storeoptions as $option => $def) { + $options[$option] = $option; + if ($def['default']) { + $options[$option] .= ' '.get_string('mappingdefault', 'cache'); + } + } + + for ($i = 0 ; $i < $requiredoptions; $i++) { + $title = '...'; + if ($i === 0) { + $title = get_string('mappingprimary', 'cache'); + } else if ($i === $requiredoptions-1) { + $title = get_string('mappingfinal', 'cache'); + } + $form->addElement('select', 'mappings['.$i.']', $title, $options); + } + $i = 0; + foreach ($currentstores as $store => $def) { + $form->setDefault('mappings['.$i.']', $store); + $i++; + } + + if (!empty($defaults)) { + $form->addElement('static', 'defaults', get_string('defaultmappings', 'cache'), html_writer::tag('strong', join(', ', $defaults))); + $form->addHelpButton('defaults', 'defaultmappings', 'cache'); + } + + $this->add_action_buttons(); + } +} + +/** + * Form to set the mappings for a mode. + * + * @package core + * @category cache + * @copyright 2012 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class cache_mode_mappings_form extends moodleform { + /** + * The definition of the form + */ + protected function definition() { + $form = $this->_form; + $stores = $this->_customdata; + + $options = array( + cache_store::MODE_APPLICATION => array(), + cache_store::MODE_SESSION => array(), + cache_store::MODE_REQUEST => array() + ); + foreach ($stores as $storename => $store) { + foreach ($store['modes'] as $mode => $enabled) { + if ($enabled) { + if (empty($store['default'])) { + $options[$mode][$storename] = $store['name']; + } else { + $options[$mode][$storename] = get_string('store_'.$store['name'], 'cache'); + } + } + } + } + + $form->addElement('hidden', 'action', 'editmodemappings'); + foreach ($options as $mode => $optionset) { + $form->addElement('select', 'mode_'.$mode, get_string('mode_'.$mode, 'cache'), $optionset); + } + + $this->add_action_buttons(); + } +} \ No newline at end of file diff --git a/cache/lib.php b/cache/lib.php new file mode 100644 index 00000000000..e92e56afe54 --- /dev/null +++ b/cache/lib.php @@ -0,0 +1,141 @@ +. + +/** + * The core cache API. + * + * Pretty much just includes the mandatory classes and contains the misc classes that arn't worth separating into individual files. + * + * This file is part of Moodle's cache API, affectionately called MUC. + * It contains the components that are requried in order to use caching. + * + * @package core + * @category cache + * @copyright 2012 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +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'); +require_once($CFG->dirroot.'/cache/classes/factory.php'); +require_once($CFG->dirroot.'/cache/classes/loaders.php'); +require_once($CFG->dirroot.'/cache/classes/definition.php'); + +/** + * A cached object wrapper. + * + * This class gets used when the data is an object that has implemented the cacheable_object interface. + * + * @package core + * @category cache + * @copyright 2012 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class cache_cached_object { + + /** + * The class of the cacheable object + * @var string + */ + protected $class; + + /** + * The data returned by the cacheable_object prepare_to_cache method. + * @var mixed + */ + protected $data; + + /** + * Constructs a cached object wrapper. + * @param cacheable_object $obj + */ + public function __construct(cacheable_object $obj) { + $this->class = get_class($obj); + $this->data = $obj->prepare_to_cache(); + } + + /** + * Restores the data as an instance of the cacheable_object class. + * @return object + */ + public function restore_object() { + $class = $this->class; + return $class::wake_from_cache($this->data); + } +} + +/** + * A wrapper class used to handle ttl when the cache store doesn't natively support it. + * + * This class is exactly why you should use event driving invalidation of cache data rather than relying on ttl. + * + * @package core + * @category cache + * @copyright 2012 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class cache_ttl_wrapper { + + /** + * The data being stored. + * @var mixed + */ + public $data; + + /** + * When the cache data expires as a timestamp. + * @var int + */ + public $expires; + + /** + * Constructs a ttl cache wrapper. + * + * @param mixed $data + * @param int $ttl The time to live in seconds. + */ + public function __construct($data, $ttl) { + $this->data = $data; + $this->expires = cache::now() + (int)$ttl; + } + + /** + * Returns true if the data has expired. + * @return int + */ + public function has_expired() { + return ($this->expires < cache::now()); + } +} + +/** + * A cache exception class. Just allows people to catch cache exceptions. + * + * @package core + * @category cache + * @copyright 2012 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class cache_exception extends moodle_exception { + public function __construct($errorcode, $module = 'cache', $link = '', $a = NULL, $debuginfo = null) { + parent::__construct($errorcode, $module, $link, $a, $debuginfo); + } +} \ No newline at end of file diff --git a/cache/locallib.php b/cache/locallib.php new file mode 100644 index 00000000000..e9f134a25da --- /dev/null +++ b/cache/locallib.php @@ -0,0 +1,853 @@ +. + +/** + * The supplementary cache API. + * + * This file is part of Moodle's cache API, affectionately called MUC. + * It contains elements of the API that are not required in order to use caching. + * Things in here are more in line with administration and management of the cache setup and configuration. + * + * @package core + * @category cache + * @copyright 2012 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Cache configuration writer. + * + * This class should only be used when you need to write to the config, all read operations exist within the cache_config. + * + * @package core + * @category cache + * @copyright 2012 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class cache_config_writer extends cache_config { + + /** + * Returns an instance of the configuration writer. + * + * @return cache_config_writer + */ + public static function instance() { + $factory = cache_factory::instance(); + return $factory->create_config_instance(true); + } + + /** + * Saves the current configuration. + */ + protected function config_save() { + global $CFG; + $cachefile = self::get_config_file_path(); + $directory = dirname($cachefile); + if ($directory !== $CFG->dataroot && !file_exists($directory)) { + $result = make_writable_directory($directory, false); + if (!$result) { + throw new cache_exception('ex_configcannotsave', 'cache', '', null, 'Cannot create config directory.'); + } + } + if (!file_exists($directory) || !is_writable($directory)) { + throw new cache_exception('ex_configcannotsave', 'cache', '', null, 'Config directory is not writable.'); + } + + // Prepare a configuration array to store. + $configuration = array(); + $configuration['stores'] = $this->configstores; + $configuration['modemappings'] = $this->configmodemappings; + $configuration['definitions'] = $this->configdefinitions; + $configuration['definitionmappings'] = $this->configdefinitionmappings; + + // Prepare the file content. + $content = "configstores)) { + throw new cache_exception('Duplicate name specificed for cache plugin instance. You must provide a unique name.'); + } + $class = 'cache_store_'.$plugin; + if (!class_exists($class)) { + $plugins = get_plugin_list_with_file('cache', 'lib.php'); + if (!array_key_exists($plugin, $plugins)) { + throw new cache_exception('Invalid plugin name specified. The plugin either does not exist or is not a valid cache plugin.'); + } + $file = $plugins[$plugin]; + if (file_exists($file)) { + require_once($file); + } + if (!class_exists($class)) { + throw new cache_exception('Invalid cache plugin specified. The plugin does not contain the required class.'); + } + } + if (!is_subclass_of($class, 'cache_store')) { + throw new cache_exception('Invalid cache plugin specified. The plugin does not extend the required class.'); + } + if (!$class::are_requirements_met()) { + throw new cache_exception('Unable to add new cache plugin instance. The requested plugin type is not supported.'); + } + $this->configstores[$name] = array( + 'name' => $name, + 'plugin' => $plugin, + 'configuration' => $configuration, + 'features' => $class::get_supported_features($configuration), + 'modes' => $class::get_supported_modes($configuration), + 'mappingsonly' => !empty($configuration['mappingsonly']), + 'useforlocking' => !empty($configuration['useforlocking']) + + ); + $this->config_save(); + return true; + } + + /** + * Sets the mode mappings. + * + * These determine the default caches for the different modes. + * This function also calls save so you should redirect immediately, or at least very shortly after + * calling this method. + * + * @param array $modemappings + * @return bool + * @throws cache_exception + */ + public function set_mode_mappings(array $modemappings) { + $mappings = array( + cache_store::MODE_APPLICATION => array(), + cache_store::MODE_SESSION => array(), + cache_store::MODE_REQUEST => array(), + ); + foreach ($modemappings as $mode => $stores) { + if (!array_key_exists($mode, $mappings)) { + throw new cache_exception('The cache mode for the new mapping does not exist'); + } + $sort = 0; + foreach ($stores as $store) { + if (!array_key_exists($store, $this->configstores)) { + throw new cache_exception('The instance name for the new mapping does not exist'); + } + if (array_key_exists($store, $mappings[$mode])) { + throw new cache_exception('This cache mapping already exists'); + } + $mappings[$mode][] = array( + 'store' => $store, + 'mode' => $mode, + 'sort' => $sort++ + ); + } + } + $this->configmodemappings = array_merge( + $mappings[cache_store::MODE_APPLICATION], + $mappings[cache_store::MODE_SESSION], + $mappings[cache_store::MODE_REQUEST] + ); + + $this->config_save(); + return true; + } + + /** + * Edits a give plugin instance. + * + * The plugin instance if determined by its name, hence you cannot rename plugins. + * This function also calls save so you should redirect immediately, or at least very shortly after + * calling this method. + * + * @param string $name + * @param string $plugin + * @param array $configuration + * @return bool + * @throws cache_exception + */ + public function edit_plugin_instance($name, $plugin, $configuration) { + if (!array_key_exists($name, $this->configstores)) { + throw new cache_exception('The requested instance does not exist.'); + } + $plugins = get_plugin_list_with_file('cache', 'lib.php'); + if (!array_key_exists($plugin, $plugins)) { + throw new cache_exception('Invalid plugin name specified. The plugin either does not exist or is not valid.'); + } + $class = 'cache_store_.'.$plugin; + $file = $plugins[$plugin]; + if (!class_exists($class)) { + if (file_exists($file)) { + require_once($file); + } + if (!class_exists($class)) { + throw new cache_exception('Invalid cache plugin specified. The plugin does not contain the require class.'); + } + } + $this->configstores[$name] = array( + 'name' => $name, + 'plugin' => $plugin, + 'configuration' => $configuration, + 'features' => $class::get_supported_features($configuration), + 'modes' => $class::get_supported_modes($configuration), + 'mappingsonly' => !empty($configuration['mappingsonly']), + 'useforlocking' => !empty($configuration['useforlocking']) + ); + $this->config_save(); + return true; + } + + /** + * Deletes a store instance. + * + * This function also calls save so you should redirect immediately, or at least very shortly after + * calling this method. + * + * @param string $name The name of the instance to delete. + * @return bool + * @throws cache_exception + */ + public function delete_store($name) { + if (!array_key_exists($name, $this->configstores)) { + throw new cache_exception('The requested store does not exist.'); + } + if ($this->configstores[$name]['default']) { + throw new cache_exception('The can not delete the default stores.'); + } + foreach ($this->configmodemappings as $mapping) { + if ($mapping['store'] === $name) { + throw new cache_exception('You cannot delete a cache store that has mode mappings.'); + } + } + foreach ($this->configdefinitionmappings as $mapping) { + if ($mapping['store'] === $name) { + throw new cache_exception('You cannot delete a cache store that has definition mappings.'); + } + } + unset($this->configstores[$name]); + $this->config_save(); + return true; + } + + /** + * Creates the default configuration and saves it. + * + * This function calls config_save, however it is safe to continue using it afterwards as this function should only ever + * be called when there is no configuration file already. + */ + public static function create_default_configuration() { + global $CFG; + + // HACK ALERT. + // We probably need to come up with a better way to create the default stores, or at least ensure 100% that the + // default store plugins are protected from deletion. + require_once($CFG->dirroot.'/cache/stores/file/lib.php'); + require_once($CFG->dirroot.'/cache/stores/session/lib.php'); + require_once($CFG->dirroot.'/cache/stores/static/lib.php'); + + $writer = new self; + $writer->configstores = array( + 'default_locking' => array( + 'name' => 'default_locking', + 'plugin' => 'file', + 'configuration' => array(), + 'features' => cache_store_file::get_supported_features(), + 'modes' => cache_store::MODE_APPLICATION, + 'useforlocking' => true, + 'mappingsonly' => true, + 'default' => true, + //'class' => 'cache_store_file' + ), + 'default_application' => array( + 'name' => 'default_application', + 'plugin' => 'file', + 'configuration' => array(), + 'features' => cache_store_file::get_supported_features(), + 'modes' => cache_store::MODE_APPLICATION, + 'default' => true, + //'class' => 'cache_store_file' + ), + 'default_session' => array( + 'name' => 'default_session', + 'plugin' => 'session', + 'configuration' => array(), + 'features' => cache_store_session::get_supported_features(), + 'modes' => cache_store::MODE_SESSION, + 'default' => true, + //'class' => 'cache_store_session' + ), + 'default_request' => array( + 'name' => 'default_request', + 'plugin' => 'static', + 'configuration' => array(), + 'features' => cache_store_static::get_supported_features(), + 'modes' => cache_store::MODE_REQUEST, + 'default' => true, + //'class' => 'cache_store_static' + ) + ); + $writer->configdefinitions = self::locate_definitions(); + $writer->configmodemappings = array( + array( + 'mode' => cache_store::MODE_APPLICATION, + 'store' => 'default_application', + 'sort' => -1 + ), + array( + 'mode' => cache_store::MODE_SESSION, + 'store' => 'default_session', + 'sort' => -1 + ), + array( + 'mode' => cache_store::MODE_REQUEST, + 'store' => 'default_request', + 'sort' => -1 + ) + ); + $writer->configdefinitionmappings = array( + array( + 'store' => 'default_locking', + 'definition' => 'core/locking', + 'sort' => -1 + ) + ); + $writer->config_save(); + } + + /** + * Updates the definition in the configuration from those found in the cache files. + * + * Calls config_save further down, you should redirect immediately or asap after calling this method. + */ + public static function update_definitions() { + $config = cache_config_writer::instance(); + $config->write_definitions_to_cache(self::locate_definitions()); + } + + /** + * Locates all of the definition files. + * + * @return array + */ + protected static function locate_definitions() { + global $CFG; + + $files = array(); + if (file_exists($CFG->dirroot.'/lib/db/caches.php')) { + $files['core'] = $CFG->dirroot.'/lib/db/caches.php'; + } + + $plugintypes = get_plugin_types(); + foreach ($plugintypes as $type => $location) { + $plugins = get_plugin_list_with_file($type, 'db/caches.php'); + foreach ($plugins as $plugin => $filepath) { + $component = clean_param($type.'_'.$plugin, PARAM_COMPONENT); // standardised plugin name + $files[$component] = $filepath; + } + } + + $definitions = array(); + foreach ($files as $component => $file) { + $filedefs = self::load_caches_file($file); + foreach ($filedefs as $area => $definition) { + $area = clean_param($area, PARAM_AREA); + $id = $component.'/'.$area; + $definition['component'] = $component; + $definition['area'] = $area; + if (array_key_exists($id, $definitions)) { + debugging('Error: duplicate cache definition found with name '.$name, DEBUG_DEVELOPER); + continue; + } + $definitions[$id] = $definition; + } + } + + return $definitions; + } + + /** + * Writes the updated definitions for the config file. + * @param array $definitions + */ + private function write_definitions_to_cache(array $definitions) { + $this->configdefinitions = $definitions; + foreach ($this->configdefinitionmappings as $key => $mapping) { + if (!array_key_exists($mapping['definition'], $definitions)) { + unset($this->configdefinitionmappings[$key]); + } + } + $this->config_save(); + } + + /** + * Loads the caches file if it exists. + * @param string $file Absolute path to the file. + * @return array + */ + private static function load_caches_file($file) { + if (!file_exists($file)) { + return array(); + } + $definitions = array(); + include $file; + return $definitions; + } + + /** + * Sets the mappings for a given definition. + * + * @param string $definition + * @param array $mappings + * @throws coding_exception + */ + public function set_definition_mappings($definition, $mappings) { + if (!array_key_exists($definition, $this->configdefinitions)) { + throw new coding_exception('Invalid definition name passed when updating mappings.'); + } + foreach ($mappings as $store) { + if (!array_key_exists($store, $this->configstores)) { + throw new coding_exception('Invalid store name passed when updating definition mappings.'); + } + } + foreach ($this->configdefinitionmappings as $key => $mapping) { + if ($mapping['definition'] == $definition) { + unset($this->configdefinitionmappings[$key]); + } + } + $sort = count($mappings); + foreach ($mappings as $store) { + $this->configdefinitionmappings[] = array( + 'store' => $store, + 'definition' => $definition, + 'sort' => $sort + ); + $sort--; + } + + $this->config_save(); + } + +} + +/** + * A cache helper for administration tasks + * + * @package core + * @category cache + * @copyright 2012 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class cache_administration_helper extends cache_helper { + + /** + * Returns an array containing all of the information about stores a renderer needs. + * @return array + */ + public static function get_store_summaries() { + $return = array(); + $default = array(); + $instance = cache_config::instance(); + $stores = $instance->get_all_stores(); + foreach ($stores as $name => $details) { + $class = $details['class']; + $store = new $class($details['name'], $details['configuration']); + $record = array( + 'name' => $name, + 'plugin' => $details['plugin'], + 'default' => $details['default'], + 'isready' => $store->is_ready(), + 'requirementsmet' => $store->are_requirements_met(), + 'mappings' => 0, + 'modes' => array( + cache_store::MODE_APPLICATION => + ($store->get_supported_modes($return) & cache_store::MODE_APPLICATION) == cache_store::MODE_APPLICATION, + cache_store::MODE_SESSION => + ($store->get_supported_modes($return) & cache_store::MODE_SESSION) == cache_store::MODE_SESSION, + cache_store::MODE_REQUEST => + ($store->get_supported_modes($return) & cache_store::MODE_REQUEST) == cache_store::MODE_REQUEST, + ), + 'supports' => array( + 'multipleidentifiers' => $store->supports_multiple_indentifiers(), + 'dataguarantee' => $store->supports_data_guarantee(), + 'nativettl' => $store->supports_native_ttl(), + 'nativelocking' => ($store instanceof cache_is_lockable), + 'keyawareness' => ($store instanceof cache_is_key_aware), + ) + ); + if (empty($details['default'])) { + $return[$name] = $record; + } else { + $default[$name] = $record; + } + } + + ksort($return); + ksort($default); + $return = $return + $default; + + foreach ($instance->get_mode_mappings() as $mapping) { + if (!array_key_exists($mapping['store'], $return)) { + continue; + } + $return[$mapping['store']]['mappings']++; + } + foreach ($instance->get_definition_mappings() as $mapping) { + if (!array_key_exists($mapping['store'], $return)) { + continue; + } + $return[$mapping['store']]['mappings']++; + } + + return $return; + } + + /** + * Returns an array of information about plugins, everything a renderer needs. + * @return array + */ + public static function get_plugin_summaries() { + $return = array(); + $plugins = get_plugin_list_with_file('cache', 'lib.php', true); + foreach ($plugins as $plugin => $path) { + $class = 'cache_store_'.$plugin; + $return[$plugin] = array( + 'name' => get_string('pluginname', 'cache_'.$plugin), + 'requirementsmet' => $class::are_requirements_met(), + 'instances' => 0, + 'modes' => array( + cache_store::MODE_APPLICATION => ($class::get_supported_modes() & cache_store::MODE_APPLICATION), + cache_store::MODE_SESSION => ($class::get_supported_modes() & cache_store::MODE_SESSION), + cache_store::MODE_REQUEST => ($class::get_supported_modes() & cache_store::MODE_REQUEST), + ), + 'supports' => array( + 'multipleidentifiers' => ($class::get_supported_features() & cache_store::SUPPORTS_MULTIPLE_IDENTIFIERS), + 'dataguarantee' => ($class::get_supported_features() & cache_store::SUPPORTS_DATA_GUARANTEE), + 'nativettl' => ($class::get_supported_features() & cache_store::SUPPORTS_NATIVE_TTL), + 'nativelocking' => (in_array('cache_is_lockable', class_implements($class))), + 'keyawareness' => (array_key_exists('cache_is_key_aware', class_implements($class))), + ), + 'canaddinstance' => ($class::can_add_instance()) + ); + } + + $instance = cache_config::instance(); + $stores = $instance->get_all_stores(); + foreach ($stores as $store) { + $plugin = $store['plugin']; + if (array_key_exists($plugin, $return)) { + $return[$plugin]['instances']++; + } + } + + return $return; + } + + /** + * Returns an array about the definitions. All the information a renderer needs. + * @return array + */ + public static function get_definition_summaries() { + $instance = cache_config::instance(); + $definitions = $instance->get_definitions(); + + $storenames = array(); + foreach ($instance->get_all_stores() as $key => $store) { + if (!empty($store['default'])) { + $storenames[$key] = new lang_string('store_'.$key, 'cache'); + } + } + + $modemappings = array(); + foreach ($instance->get_mode_mappings() as $mapping) { + $mode = $mapping['mode']; + if (!array_key_exists($mode, $modemappings)) { + $modemappings[$mode] = array(); + } + if (array_key_exists($mapping['store'], $storenames)) { + $modemappings[$mode][] = $storenames[$mapping['store']]; + } else { + $modemappings[$mode][] = $mapping['store']; + } + } + + $definitionmappings = array(); + foreach ($instance->get_definition_mappings() as $mapping) { + $definition = $mapping['definition']; + if (!array_key_exists($definition, $definitionmappings)) { + $definitionmappings[$definition] = array(); + } + if (array_key_exists($mapping['store'], $storenames)) { + $definitionmappings[$definition][] = $storenames[$mapping['store']]; + } else { + $definitionmappings[$definition][] = $mapping['store']; + } + } + + $return = array(); + + foreach ($definitions as $id => $definition) { + + $mappings = array(); + if (array_key_exists($id, $definitionmappings)) { + $mappings = $definitionmappings[$id]; + } else if (empty($definition['mappingsonly'])) { + $mappings = $modemappings[$definition['mode']]; + } + + $return[$id] = array( + 'id' => $id, + 'name' => cache_helper::get_definition_name($definition), + 'mode' => $definition['mode'], + 'component' => $definition['component'], + 'area' => $definition['area'], + 'mappings' => $mappings + ); + } + return $return; + } + + /** + * Returns all of the actions that can be performed on a definition. + * @param context $context + * @return array + */ + public static function get_definition_actions(context $context) { + if (has_capability('moodle/site:config', $context)) { + return array( + array( + 'text' => get_string('editmappings', 'cache'), + 'url' => new moodle_url('/cache/admin.php', array('action' => 'editdefinitionmapping', 'sesskey' => sesskey())) + ) + ); + } + return array(); + } + + /** + * Returns all of the actions that can be performed on a store. + * + * @param string $name The name of the store + * @param array $storedetails + * @return array + */ + public static function get_store_actions($name, array $storedetails) { + $actions = array(); + if (has_capability('moodle/site:config', get_system_context())) { + $baseurl = new moodle_url('/cache/admin.php', array('store' => $name, 'sesskey' => sesskey())); + if (empty($storedetails['default'])) { + $actions[] = array( + 'text' => get_string('editstore', 'cache'), + 'url' => new moodle_url($baseurl, array('action' => 'editstore', 'plugin' => $storedetails['plugin'])) + ); + $actions[] = array( + 'text' => get_string('deletestore', 'cache'), + 'url' => new moodle_url($baseurl, array('action' => 'deletestore')) + ); + } + $actions[] = array( + 'text' => get_string('purge', 'cache'), + 'url' => new moodle_url($baseurl, array('action' => 'purge')) + ); + } + return $actions; + } + + + /** + * Returns all of the actions that can be performed on a plugin. + * + * @param string $name The name of the plugin + * @param array $plugindetails + * @return array + */ + public static function get_plugin_actions($name, array $plugindetails) { + $actions = array(); + if (has_capability('moodle/site:config', get_system_context())) { + if (!empty($plugindetails['canaddinstance'])) { + $url = new moodle_url('/cache/admin.php', array('action' => 'addstore', 'plugin' => $name, 'sesskey' => sesskey())); + $actions[] = array( + 'text' => get_string('addinstance', 'cache'), + 'url' => $url + ); + } + } + return $actions; + } + + /** + * Returns a form that can be used to add a store instance. + * + * @param string $plugin The plugin to add an instance of + * @return cache_store_addinstance_form + * @throws coding_exception + */ + public static function get_add_store_form($plugin) { + global $CFG; // Needed for includes + $plugindir = get_plugin_directory('cache', $plugin); + $class = 'cache_store_addinstance_form'; + if (file_exists($plugindir.'/addinstanceform.php')) { + require_once($plugindir.'/addinstanceform.php'); + if (class_exists('cache_store_'.$plugin.'_addinstance_form')) { + $class = 'cache_store_'.$plugin.'_addinstance_form'; + if (!array_key_exists('cache_store_addinstance_form', class_parents($class))) { + throw new coding_exception('Cache plugin add instance forms must extend cache_store_addinstance_form'); + } + } + } + + $url = new moodle_url('/cache/admin.php', array('action' => 'addstore')); + return new $class($url, array('plugin' => $plugin, 'store' => null)); + } + + /** + * Returns a form that can be used to edit a store instance. + * + * @param string $plugin + * @param string $store + * @return cache_store_addinstance_form + * @throws coding_exception + */ + public static function get_edit_store_form($plugin, $store) { + global $CFG; // Needed for includes + $plugindir = get_plugin_directory('cache', $plugin); + $class = 'cache_store_addinstance_form'; + if (file_exists($plugindir.'/addinstanceform.php')) { + require_once($plugindir.'/addinstanceform.php'); + if (class_exists('cache_store_'.$plugin.'_addinstance_form')) { + $class = 'cache_store_'.$plugin.'_addinstance_form'; + if (!array_key_exists('cache_store_addinstance_form', class_parents($class))) { + throw new coding_exception('Cache plugin add instance forms must extend cache_store_addinstance_form'); + } + } + } + + $url = new moodle_url('/cache/admin.php', array('action' => 'editstore')); + return new $class($url, array('plugin' => $plugin, 'store' => $store)); + } + + /** + * Processes the results of the add/edit instance form data for a plugin returning an array of config information suitable to + * store in configuration. + * + * @param stdClass $data The mform data. + * @return array + * @throws coding_exception + */ + public static function get_store_configuration_from_data(stdClass $data) { + global $CFG; + $file = $CFG->dirroot.'/cache/stores/'.$data->plugin.'/lib.php'; + if (!file_exists($file)) { + throw new coding_exception('Invalid cache plugin provided. '.$file); + } + require_once($file); + $class = 'cache_store_'.$data->plugin; + $method = 'config_get_configuration_array'; + if (!class_exists($class)) { + throw new coding_exception('Invalid cache plugin provided.'); + } + if (method_exists($class, $method)) { + return call_user_func(array($class, $method), $data); + } + return array(); + } + + /** + * Get an array of stores that are suitable to be used for a given definition. + * + * @param string $component + * @param string $area + * @return array Array containing 3 elements + * 1. An array of currently used stores + * 2. An array of suitable stores + * 3. An array of default stores + */ + public static function get_definition_store_options($component, $area) { + $factory = cache_factory::instance(); + $definition = $factory->create_definition($component, $area); + $config = cache_config::instance(); + $currentstores = $config->get_stores_for_definition($definition); + $possiblestores = $config->get_stores($definition->get_mode(), $definition->get_requirements_bin()); + + $defaults = array(); + foreach ($currentstores as $key => $store) { + if (!empty($store['default'])) { + $defaults[] = $key; + unset($currentstores[$key]); + } + } + foreach ($possiblestores as $key => $store) { + if ($key === 'default_locking') { + unset($possiblestores[$key]); + } else if ($store['default']) { + unset($possiblestores[$key]); + $possiblestores[$key] = $store; + } + } + return array($currentstores, $possiblestores, $defaults); + } + + /** + * Get the default stores for all modes. + * + * @return array An array containing sub-arrays, one for each mode. + */ + public static function get_default_mode_stores() { + $instance = cache_config::instance(); + $storenames = array(); + foreach ($instance->get_all_stores() as $key => $store) { + if (!empty($store['default'])) { + $storenames[$key] = new lang_string('store_'.$key, 'cache'); + } + } + $modemappings = array( + cache_store::MODE_APPLICATION => array(), + cache_store::MODE_SESSION => array(), + cache_store::MODE_REQUEST => array(), + ); + foreach ($instance->get_mode_mappings() as $mapping) { + $mode = $mapping['mode']; + if (!array_key_exists($mode, $modemappings)) { + debugging('Unknown mode in cache store mode mappings', DEBUG_DEVELOPER); + continue; + } + if (array_key_exists($mapping['store'], $storenames)) { + $modemappings[$mode][$mapping['store']] = $storenames[$mapping['store']]; + } else { + $modemappings[$mode][$mapping['store']] = $mapping['store']; + } + } + return $modemappings; + } +} \ No newline at end of file diff --git a/cache/renderer.php b/cache/renderer.php new file mode 100644 index 00000000000..bf96d94b021 --- /dev/null +++ b/cache/renderer.php @@ -0,0 +1,285 @@ +. + +/** + * The Cache renderer. + * + * This file is part of Moodle's cache API, affectionately called MUC. + * + * @package core + * @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 renderer (mainly admin interfaces). + * + * @package core + * @category cache + * @copyright 2012 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_cache_renderer extends plugin_renderer_base { + + /** + * Displays store summaries. + * + * @param array $stores + * @param array $plugins + * @return string HTML + */ + public function store_summariers(array $stores, array $plugins) { + $table = new html_table(); + $table->head = array( + get_string('storename', 'cache'), + get_string('plugin', 'cache'), + get_string('storeready', 'cache'), + get_string('mappings', 'cache'), + get_string('modes', 'cache'), + get_string('supports', 'cache'), + get_string('actions', 'cache'), + ); + $table->colclasses = array( + 'storename', + 'plugin', + 'storeready', + 'mappings', + 'modes', + 'supports', + 'actions' + ); + $table->data = array(); + + $defaultstoreactions = get_string('defaultstoreactions', 'cache'); + + foreach ($stores as $name => $store) { + $actions = cache_administration_helper::get_store_actions($name, $store); + $modes = array(); + foreach ($store['modes'] as $mode => $enabled) { + if ($enabled) { + $modes[] = get_string('mode_'.$mode, 'cache'); + } + } + + $supports = array(); + foreach ($store['supports'] as $support => $enabled) { + if ($enabled) { + $supports[] = get_string('supports_'.$support, 'cache'); + } + } + + $info = ''; + if (!empty($store['default'])) { + $info = $this->output->pix_icon('i/info', $defaultstoreactions).' '; + } + $htmlactions = array(); + foreach ($actions as $action) { + $htmlactions[] = $this->output->action_link($action['url'], $action['text']); + } + + $storename = $store['name']; + if (!empty($store['default'])) { + $storename = get_string('store_'.$store['name'], 'cache'); + } + + $row = new html_table_row(array( + $storename, + get_string('pluginname', 'cache_'.$store['plugin']), + ($store['isready'] && $store['requirementsmet']) ? $this->output->pix_icon('i/tick_green_small', '1') : '', + $store['mappings'], + join(', ', $modes), + join(', ', $supports), + $info.join(', ', $htmlactions) + )); + $row->attributes['class'] = 'store-'.$name; + if ($store['default']) { + $row->attributes['class'] .= ' default-store'; + } + $table->data[] = $row; + } + + $html = html_writer::start_tag('div', array('id' => 'core-cache-store-summaries')); + $html .= $this->output->heading(get_string('storesummaries', 'cache'), 3); + $html .= html_writer::table($table); + $html .= html_writer::end_tag('div'); + return $html; + } + + /** + * Displays plugin summaries + * + * @param array $plugins + * @return string HTML + */ + public function plugin_summaries(array $plugins) { + $table = new html_table(); + $table->head = array( + get_string('plugin', 'cache'), + get_string('storeready', 'cache'), + get_string('stores', 'cache'), + get_string('modes', 'cache'), + get_string('supports', 'cache'), + get_string('actions', 'cache'), + ); + $table->colclasses = array( + 'plugin', + 'storeready', + 'stores', + 'modes', + 'supports', + 'actions' + ); + $table->data = array(); + + foreach ($plugins as $name => $plugin) { + $actions = cache_administration_helper::get_plugin_actions($name, $plugin); + + $modes = array(); + foreach ($plugin['modes'] as $mode => $enabled) { + if ($enabled) { + $modes[] = get_string('mode_'.$mode, 'cache'); + } + } + + $supports = array(); + foreach ($plugin['supports'] as $support => $enabled) { + if ($enabled) { + $supports[] = get_string('supports_'.$support, 'cache'); + } + } + + $htmlactions = array(); + foreach ($actions as $action) { + $htmlactions[] = $this->output->action_link($action['url'], $action['text']); + } + + $row = new html_table_row(array( + $plugin['name'], + ($plugin['requirementsmet']) ? $this->output->pix_icon('i/tick_green_small', '1') : '', + $plugin['instances'], + join(', ', $modes), + join(', ', $supports), + join(', ', $htmlactions) + )); + + $row->attributes['class'] = 'plugin-'.$name; + $table->data[] = $row; + } + + $html = html_writer::start_tag('div', array('id' => 'core-cache-plugin-summaries')); + $html .= $this->output->heading(get_string('pluginsummaries', 'cache'), 3); + $html .= html_writer::table($table); + $html .= html_writer::end_tag('div'); + return $html; + } + + /** + * Displays definition summaries + * + * @param array $definitions + * @param array $actions + * @return string HTML + */ + public function definition_summaries(array $definitions, array $actions) { + $table = new html_table(); + $table->head = array( + get_string('definition', 'cache'), + get_string('mode', 'cache'), + get_string('component', 'cache'), + get_string('area', 'cache'), + get_string('mappings', 'cache'), + get_string('actions', 'cache'), + ); + $table->colclasses = array( + 'definition', + 'mode', + 'component', + 'area', + 'mappings', + 'actions' + ); + $table->data = array(); + + foreach ($definitions as $id => $definition) { + $htmlactions = array(); + foreach ($actions as $action) { + $action['url']->param('definition', $id); + $htmlactions[] = $this->output->action_link($action['url'], $action['text']); + } + + $row = new html_table_row(array( + $definition['name'], + get_string('mode_'.$definition['mode'], 'cache'), + $definition['component'], + $definition['area'], + (!empty($definition['mappings'])) ? join(', ', $definition['mappings']) : ''.get_string('none', 'cache').'', + join(', ', $htmlactions) + )); + $row->attributes['class'] = 'definition-'.$definition['component'].'-'.$definition['area']; + $table->data[] = $row; + } + + $html = html_writer::start_tag('div', array('id' => 'core-cache-definition-summaries')); + $html .= $this->output->heading(get_string('definitionsummaries', 'cache'), 3); + $html .= html_writer::table($table); + + $html .= html_writer::tag('div', html_writer::link(new moodle_url('/cache/admin.php', array('action' => 'rescandefinitions', 'sesskey' => sesskey())), get_string('rescandefinitions', 'cache')), array('id' => 'core-cache-rescan-definitions')); + + $html .= html_writer::end_tag('div'); + return $html; + } + + /** + * Displays mode mappings + * + * @param string $applicationstore + * @param string $sessionstore + * @param string $requeststore + * @param moodle_url $editurl + * @return string HTML + */ + public function mode_mappings($applicationstore, $sessionstore, $requeststore, moodle_url $editurl) { + $table = new html_table(); + $table->colclasses = array( + 'mode', + 'mapping', + ); + $table->rowclasses = array( + 'mode_application', + 'mode_session', + 'mode_request' + ); + $table->head = array( + get_string('mode', 'cache'), + get_string('mappings', 'cache'), + ); + $table->data = array( + array(get_string('mode_'.cache_store::MODE_APPLICATION, 'cache'), $applicationstore), + array(get_string('mode_'.cache_store::MODE_SESSION, 'cache'), $sessionstore), + array(get_string('mode_'.cache_store::MODE_REQUEST, 'cache'), $requeststore) + ); + + $html = html_writer::start_tag('div', array('id' => 'core-cache-mode-mappings')); + $html .= $this->output->heading(get_string('defaultmappings', 'cache'), 3); + $html .= html_writer::table($table); + $html .= html_writer::tag('div', html_writer::link($editurl, get_string('editmappings', 'cache')), array('class' => 'edit-link')); + $html .= html_writer::end_tag('div'); + return $html; + } +} \ No newline at end of file diff --git a/cache/testperformance.php b/cache/testperformance.php new file mode 100644 index 00000000000..9b3b2325b4a --- /dev/null +++ b/cache/testperformance.php @@ -0,0 +1,202 @@ +. + +/** + * Store performance test run + output script. + * + * @package core + * @category cache + * @copyright 2012 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once('../config.php'); +require_once($CFG->dirroot.'/lib/adminlib.php'); +require_once($CFG->dirroot.'/cache/locallib.php'); + +$count = optional_param('count', 100, PARAM_INT); +$count = min($count, 100000); +$count = max($count, 0); + +admin_externalpage_setup('cachetestperformance'); + +$applicationtable = new html_table(); +$applicationtable->head = array( + get_string('plugin', 'cache'), + get_string('result', 'cache'), + get_string('set', 'cache'), + get_string('gethit', 'cache'), + get_string('getmiss', 'cache'), + get_string('delete', 'cache'), +); +$applicationtable->data = array(); +$sessiontable = clone($applicationtable); +$requesttable = clone($applicationtable); + + +$application = cache_definition::load_adhoc(cache_store::MODE_APPLICATION, 'cache', 'applicationtest', null, false); +$session = cache_definition::load_adhoc(cache_store::MODE_SESSION, 'cache', 'sessiontest', null, false); +$request = cache_definition::load_adhoc(cache_store::MODE_REQUEST, 'cache', 'requesttest', null, false); + +$strinvalidplugin = new lang_string('invalidplugin', 'cache'); +$strunsupportedmode = new lang_string('unsupportedmode', 'cache'); +$struntestable = new lang_string('untestable', 'cache'); +$strtested = new lang_string('tested', 'cache'); + +foreach (get_plugin_list_with_file('cache', 'lib.php', true) as $plugin => $path) { + + $class = 'cache_store_'.$plugin; + $plugin = get_string('pluginname', 'cache_'.$plugin); + + if (!class_exists($class) || !method_exists($class, 'initialise_test_instance')) { + $applicationtable->data[] = array($plugin, $strinvalidplugin, '-', '-', '-', '-'); + $sessiontable->data[] = array($plugin, $strinvalidplugin, '-', '-', '-', '-'); + $requesttable->data[] = array($plugin, $strinvalidplugin, '-', '-', '-', '-'); + continue; + } + + if (!$class::is_supported_mode(cache_store::MODE_APPLICATION)) { + $applicationtable->data[] = array($plugin, $strunsupportedmode, '-', '-', '-', '-'); + } else { + $store = $class::initialise_test_instance($application); + if ($store === false) { + $applicationtable->data[] = array($plugin, $struntestable, '-', '-', '-', '-'); + } else { + $result = array($plugin, $strtested, 0, 0, 0); + $start = microtime(true); + for ($i = 0; $i < $count; $i++) { + $store->set('key'.$i, 'test data '.$i); + } + $result[2] = sprintf('%01.4f', microtime(true) - $start); + + $start = microtime(true); + for ($i = 0; $i < $count; $i++) { + $store->get('key'.$i); + } + $result[3] = sprintf('%01.4f', microtime(true) - $start); + + $start = microtime(true); + for ($i = 0; $i < $count; $i++) { + $store->get('fake'.$i); + } + $result[4] = sprintf('%01.4f', microtime(true) - $start); + + $start = microtime(true); + for ($i = 0; $i < $count; $i++) { + $store->delete('key'.$i); + } + $result[5] = sprintf('%01.4f', microtime(true) - $start); + $applicationtable->data[] = $result; + $store->cleanup(); + } + } + + if (!$class::is_supported_mode(cache_store::MODE_SESSION)) { + $sessiontable->data[] = array($plugin, $strunsupportedmode, '-', '-', '-', '-'); + } else { + $store = $class::initialise_test_instance($session); + if ($store === false) { + $sessiontable->data[] = array($plugin, $struntestable, '-', '-', '-', '-'); + } else { + $result = array($plugin, $strtested, 0, 0, 0); + $start = microtime(true); + for ($i = 0; $i < $count; $i++) { + $store->set('key'.$i, 'test data '.$i); + } + $result[2] = sprintf('%01.4f', microtime(true) - $start); + + $start = microtime(true); + for ($i = 0; $i < $count; $i++) { + $store->get('key'.$i); + } + $result[3] = sprintf('%01.4f', microtime(true) - $start); + + $start = microtime(true); + for ($i = 0; $i < $count; $i++) { + $store->get('fake'.$i); + } + $result[4] = sprintf('%01.4f', microtime(true) - $start); + + $start = microtime(true); + for ($i = 0; $i < $count; $i++) { + $store->delete('key'.$i); + } + $result[5] = sprintf('%01.4f', microtime(true) - $start); + $sessiontable->data[] = $result; + $store->cleanup(); + } + } + + if (!$class::is_supported_mode(cache_store::MODE_REQUEST)) { + $requesttable->data[] = array($plugin, $strunsupportedmode, '-', '-', '-', '-'); + } else { + $store = $class::initialise_test_instance($request); + if ($store === false) { + $requesttable->data[] = array($plugin, $struntestable, '-', '-', '-', '-'); + } else { + $result = array($plugin, $strtested, 0, 0, 0); + $start = microtime(true); + for ($i = 0; $i < $count; $i++) { + $store->set('key'.$i, 'test data '.$i); + } + $result[2] = sprintf('%01.4f', microtime(true) - $start); + + $start = microtime(true); + for ($i = 0; $i < $count; $i++) { + $store->get('key'.$i); + } + $result[3] = sprintf('%01.4f', microtime(true) - $start); + + $start = microtime(true); + for ($i = 0; $i < $count; $i++) { + $store->get('fake'.$i); + } + $result[4] = sprintf('%01.4f', microtime(true) - $start); + + $start = microtime(true); + for ($i = 0; $i < $count; $i++) { + $store->delete('key'.$i); + } + $result[5] = sprintf('%01.4f', microtime(true) - $start); + $requesttable->data[] = $result; + $store->cleanup(); + } + } + +} + +echo $OUTPUT->header(); +echo $OUTPUT->heading(get_string('storeperformance', 'cache', $count)); + +$possiblecounts = array(1, 10, 100, 500, 1000, 5000, 10000, 50000, 100000); +$links = array(); +foreach ($possiblecounts as $pcount) { + $links[] = html_writer::link(new moodle_url($PAGE->url, array('count' => $pcount)), $pcount); +} +echo $OUTPUT->box_start('generalbox performance-test-counts'); +echo get_string('requestcount', 'cache', join(', ', $links)); +echo $OUTPUT->box_end(); + +echo $OUTPUT->heading(get_string('storeresults_application', 'cache')); +echo html_writer::table($applicationtable); + +echo $OUTPUT->heading(get_string('storeresults_session', 'cache')); +echo html_writer::table($sessiontable); + +echo $OUTPUT->heading(get_string('storeresults_request', 'cache')); +echo html_writer::table($requesttable); + +echo $OUTPUT->footer(); \ No newline at end of file diff --git a/cache/tests/cache_test.php b/cache/tests/cache_test.php new file mode 100644 index 00000000000..44d1cff3162 --- /dev/null +++ b/cache/tests/cache_test.php @@ -0,0 +1,529 @@ +. + +/** + * PHPunit tests for the cache API + * + * This file is part of Moodle's cache API, affectionately called MUC. + * It contains the components that are requried in order to use caching. + * + * @package core + * @category cache + * @copyright 2012 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Include the necessary evils. + */ +global $CFG; +require_once($CFG->dirroot.'/cache/locallib.php'); +require_once($CFG->dirroot.'/cache/tests/fixtures/lib.php'); + +/** + * PHPunit tests for the cache API + * + * @copyright 2012 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class cache_phpunit_tests extends advanced_testcase { + + /** + * Set things back to the default before each test. + */ + public function setUp() { + parent::setUp(); + cache_factory::reset(); + cache_lock::reset(); + cache_config_phpunittest::create_default_configuration(); + } + + /** + * Tests cache configuration + */ + public function test_cache_config() { + $instance = cache_config::instance(); + $this->assertInstanceOf('cache_config_phpunittest', $instance); + + $this->assertTrue(cache_config_phpunittest::config_file_exists()); + + $stores = $instance->get_all_stores(); + $this->assertCount(4, $stores); + foreach ($stores as $name => $store) { + // Check its an array + $this->assertInternalType('array', $store); + // Check the name is the key + $this->assertEquals($name, $store['name']); + // Check that it has been declared default + $this->assertTrue($store['default']); + // Required attributes = name + plugin + configuration + modes + features + $this->assertArrayHasKey('name', $store); + $this->assertArrayHasKey('plugin', $store); + $this->assertArrayHasKey('configuration', $store); + $this->assertArrayHasKey('modes', $store); + $this->assertArrayHasKey('features', $store); + } + + $modemappings = $instance->get_mode_mappings(); + $this->assertCount(3, $modemappings); + $modes = array( + cache_store::MODE_APPLICATION => false, + cache_store::MODE_SESSION => false, + cache_store::MODE_REQUEST => false, + ); + foreach ($modemappings as $mapping) { + // We expect 3 properties + $this->assertCount(3, $mapping); + // Required attributes = mode + store + $this->assertArrayHasKey('mode', $mapping); + $this->assertArrayHasKey('store', $mapping); + // Record the mode + $modes[$mapping['mode']] = true; + } + + // Must have the default 3 modes and no more. + $this->assertCount(3, $mapping); + foreach ($modes as $mode) { + $this->assertTrue($mode); + } + + $definitions = $instance->get_definitions(); + // The default locking definition is required for the cache API and must be there. + $this->assertArrayHasKey('core/locking', $definitions); + // The event invalidation definition is required for the cache API and must be there. + $this->assertArrayHasKey('core/eventinvalidation', $definitions); + + $definitionmappings = $instance->get_definition_mappings(); + // There should be a mapping for default locking to default_locking + $found = false; + foreach ($definitionmappings as $mapping) { + // Required attributes = definition + store + $this->assertArrayHasKey('definition', $mapping); + $this->assertArrayHasKey('store', $mapping); + if ($mapping['store'] == 'default_locking' && $mapping['definition'] == 'core/locking') { + $found = true; + } + } + $this->assertTrue($found, 'The expected mapping for default locking definition to the default locking store was not found.'); + } + + /** + * Tests the default application cache + */ + public function test_default_application_cache() { + $cache = cache::make_from_params(cache_store::MODE_APPLICATION, 'phpunit', 'applicationtest'); + $this->assertInstanceOf('cache_application', $cache); + $this->run_on_cache($cache); + } + + /** + * Tests the default session cache + */ + public function test_default_session_cache() { + $cache = cache::make_from_params(cache_store::MODE_SESSION, 'phpunit', 'applicationtest'); + $this->assertInstanceOf('cache_session', $cache); + $this->run_on_cache($cache); + } + + /** + * Tests the default request cache + */ + public function test_default_request_cache() { + $cache = cache::make_from_params(cache_store::MODE_REQUEST, 'phpunit', 'applicationtest'); + $this->assertInstanceOf('cache_request', $cache); + $this->run_on_cache($cache); + } + + /** + * Tests using a cache system when there are no stores available (who knows what the admin did to achieve this). + */ + public function test_on_cache_without_store() { + $instance = cache_config_phpunittest::instance(true); + $instance->phpunit_add_definition('phpunit/nostoretest1', array( + 'mode' => cache_store::MODE_APPLICATION, + 'component' => 'phpunit', + 'area' => 'nostoretest1', + )); + $instance->phpunit_add_definition('phpunit/nostoretest2', array( + 'mode' => cache_store::MODE_APPLICATION, + 'component' => 'phpunit', + 'area' => 'nostoretest2', + 'persistent' => true + )); + $instance->phpunit_remove_stores(); + + $cache = cache::make('phpunit', 'nostoretest1'); + $this->run_on_cache($cache); + + $cache = cache::make('phpunit', 'nostoretest2'); + $this->run_on_cache($cache); + } + + /** + * Runs a standard series of access and use tests on a cache instance. + * + * This function is great because we can use it to ensure all of the loaders perform exactly the same way. + * + * @param cache_loader $cache + */ + protected function run_on_cache(cache_loader $cache) { + $key = 'testkey'; + $datascalar = 'test data'; + $dataarray = array('test' => 'data', 'part' => 'two'); + $dataobject = (object)$dataarray; + + $this->assertTrue($cache->purge()); + + // Check all read methods + $this->assertFalse($cache->get($key)); + $this->assertFalse($cache->has($key)); + $result = $cache->get_many(array($key)); + $this->assertCount(1, $result); + $this->assertFalse(reset($result)); + $this->assertFalse($cache->has_any(array($key))); + $this->assertFalse($cache->has_all(array($key))); + + // Set the data + $this->assertTrue($cache->set($key, $datascalar)); + // Setting it more than once should be permitted + $this->assertTrue($cache->set($key, $datascalar)); + + // Recheck the read methods + $this->assertEquals($datascalar, $cache->get($key)); + $this->assertTrue($cache->has($key)); + $result = $cache->get_many(array($key)); + $this->assertCount(1, $result); + $this->assertEquals($datascalar, reset($result)); + $this->assertTrue($cache->has_any(array($key))); + $this->assertTrue($cache->has_all(array($key))); + + // Delete it + $this->assertTrue($cache->delete($key)); + + // Check its gone + $this->assertFalse($cache->get($key)); + $this->assertFalse($cache->has($key)); + + // Test arrays + $this->assertTrue($cache->set($key, $dataarray)); + $this->assertEquals($dataarray, $cache->get($key)); + + // Test objects + $this->assertTrue($cache->set($key, $dataobject)); + $this->assertEquals($dataobject, $cache->get($key)); + + $specobject = new cache_phpunit_dummy_object('red', 'blue'); + $this->assertTrue($cache->set($key, $specobject)); + $result = $cache->get($key); + $this->assertInstanceOf('cache_phpunit_dummy_object', $result); + $this->assertEquals('red_ptc_wfc', $result->property1); + $this->assertEquals('blue_ptc_wfc', $result->property2); + + // Test set many + $cache->set_many(array('key1' => 'data1', 'key2' => 'data2')); + $this->assertEquals('data1', $cache->get('key1')); + $this->assertEquals('data2', $cache->get('key2')); + $this->assertTrue($cache->delete('key1')); + $this->assertTrue($cache->delete('key2')); + + // Test delete many + $this->assertTrue($cache->set('key1', 'data1')); + $this->assertTrue($cache->set('key2', 'data2')); + + $this->assertEquals('data1', $cache->get('key1')); + $this->assertEquals('data2', $cache->get('key2')); + + $this->assertEquals(2, $cache->delete_many(array('key1', 'key2'))); + + $this->assertFalse($cache->get('key1')); + $this->assertFalse($cache->get('key2')); + } + + /** + * Tests a definition using a data loader + */ + public function test_definition_data_loader() { + $instance = cache_config_phpunittest::instance(true); + $instance->phpunit_add_definition('phpunit/datasourcetest', array( + 'mode' => cache_store::MODE_APPLICATION, + 'component' => 'phpunit', + 'area' => 'datasourcetest', + 'datasource' => 'cache_phpunit_dummy_datasource' + )); + + $cache = cache::make('phpunit', 'datasourcetest'); + $this->assertInstanceOf('cache_application', $cache); + + // Purge it to be sure + $this->assertTrue($cache->purge()); + // It won't be there yet + $this->assertFalse($cache->has('Test')); + + // It should load it ;) + $this->assertTrue($cache->has('Test', true)); + + // Purge it to be sure + $this->assertTrue($cache->purge()); + $this->assertEquals('Test has no value really.', $cache->get('Test')); + } + + /** + * Tests a definition using an overridden loader + */ + public function test_definition_overridden_loader() { + $instance = cache_config_phpunittest::instance(true); + $instance->phpunit_add_definition('phpunit/overridetest', array( + 'mode' => cache_store::MODE_APPLICATION, + 'component' => 'phpunit', + 'area' => 'overridetest', + 'overrideclass' => 'cache_phpunit_dummy_overrideclass' + )); + $cache = cache::make('phpunit', 'overridetest'); + $this->assertInstanceOf('cache_phpunit_dummy_overrideclass', $cache); + $this->assertInstanceOf('cache_application', $cache); + // Purge it to be sure + $this->assertTrue($cache->purge()); + // It won't be there yet + $this->assertFalse($cache->has('Test')); + // Add it + $this->assertTrue($cache->set('Test', 'Test has no value really.')); + // Check its there + $this->assertEquals('Test has no value really.', $cache->get('Test')); + } + + /** + * Tests manual locking operations on an application cache + */ + public function test_application_manual_locking() { + $instance = cache_config_phpunittest::instance(); + $instance->phpunit_add_definition('phpunit/lockingtest', array( + 'mode' => cache_store::MODE_APPLICATION, + 'component' => 'phpunit', + 'area' => 'lockingtest' + )); + $cache1 = cache::make('phpunit', 'lockingtest'); + $cache2 = clone($cache1); + + $this->assertTrue($cache1->set('testkey', 'test data')); + $this->assertTrue($cache2->set('testkey', 'test data')); + + $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->release_lock('testkey')); + $this->assertFalse($cache2->release_lock('testkey')); + + $this->assertTrue($cache1->set('testkey', 'test data')); + $this->assertTrue($cache2->set('testkey', 'test data')); + } + + /** + * Tests application cache event invalidation + */ + public function test_application_event_invalidation() { + $instance = cache_config_phpunittest::instance(); + $instance->phpunit_add_definition('phpunit/eventinvalidationtest', array( + 'mode' => cache_store::MODE_APPLICATION, + 'component' => 'phpunit', + 'area' => 'eventinvalidationtest', + 'invalidationevents' => array( + 'crazyevent' + ) + )); + $cache = cache::make('phpunit', 'eventinvalidationtest'); + + $this->assertTrue($cache->set('testkey1', 'test data 1')); + $this->assertEquals('test data 1', $cache->get('testkey1')); + $this->assertTrue($cache->set('testkey2', 'test data 2')); + $this->assertEquals('test data 2', $cache->get('testkey2')); + + // Test invalidating a single entry + cache_helper::invalidate_by_event('crazyevent', array('testkey1')); + + $this->assertFalse($cache->get('testkey1')); + $this->assertEquals('test data 2', $cache->get('testkey2')); + + $this->assertTrue($cache->set('testkey1', 'test data 1')); + + // Test invalidating both entries + cache_helper::invalidate_by_event('crazyevent', array('testkey1', 'testkey2')); + + $this->assertFalse($cache->get('testkey1')); + $this->assertFalse($cache->get('testkey2')); + } + + /** + * Tests application cache definition invalidation + */ + public function test_application_definition_invalidation() { + $instance = cache_config_phpunittest::instance(); + $instance->phpunit_add_definition('phpunit/definitioninvalidation', array( + 'mode' => cache_store::MODE_APPLICATION, + 'component' => 'phpunit', + 'area' => 'definitioninvalidation' + )); + $cache = cache::make('phpunit', 'definitioninvalidation'); + $this->assertTrue($cache->set('testkey1', 'test data 1')); + $this->assertEquals('test data 1', $cache->get('testkey1')); + $this->assertTrue($cache->set('testkey2', 'test data 2')); + $this->assertEquals('test data 2', $cache->get('testkey2')); + + cache_helper::invalidate_by_definition('phpunit', 'definitioninvalidation', array(), 'testkey1'); + + $this->assertFalse($cache->get('testkey1')); + $this->assertEquals('test data 2', $cache->get('testkey2')); + + $this->assertTrue($cache->set('testkey1', 'test data 1')); + + cache_helper::invalidate_by_definition('phpunit', 'definitioninvalidation', array(), array('testkey1')); + + $this->assertFalse($cache->get('testkey1')); + $this->assertEquals('test data 2', $cache->get('testkey2')); + + $this->assertTrue($cache->set('testkey1', 'test data 1')); + + cache_helper::invalidate_by_definition('phpunit', 'definitioninvalidation', array(), array('testkey1', 'testkey2')); + + $this->assertFalse($cache->get('testkey1')); + $this->assertFalse($cache->get('testkey2')); + } + + /** + * Tests application cache event invalidation over a distributed setup. + */ + public function test_distributed_application_event_invalidation() { + global $CFG; + // This is going to be an intense wee test. + // We need to add data the to cache, invalidate it by event, manually force it back without MUC knowing to simulate a + // disconnected/distributed setup (think load balanced server using local cache), instantiate the cache again and finally + // check that it is not picked up. + + $instance = cache_config_phpunittest::instance(); + $instance->phpunit_add_definition('phpunit/eventinvalidationtest', array( + 'mode' => cache_store::MODE_APPLICATION, + 'component' => 'phpunit', + 'area' => 'eventinvalidationtest', + 'invalidationevents' => array( + 'crazyevent' + ) + )); + $cache = cache::make('phpunit', 'eventinvalidationtest'); + $this->assertTrue($cache->set('testkey1', 'test data 1')); + $this->assertEquals('test data 1', $cache->get('testkey1')); + + cache_helper::invalidate_by_event('crazyevent', array('testkey1')); + + $this->assertFalse($cache->get('testkey1')); + + // OK data added, data invalidated, and invalidation time has been set. + // Now we need to manually add back the data and adjust the invalidation time. + $timefile = $CFG->dataroot.'/cache/cache_store_file/default_application/phpunit_eventinvalidationtest/494515064.cache'; + $timecont = serialize(cache::now() - 60); // Back 60sec in the past to force it to re-invalidate + file_put_contents($timefile, $timecont); + $this->assertTrue(file_exists($timefile)); + + $datafile = $CFG->dataroot.'/cache/cache_store_file/default_application/phpunit_eventinvalidationtest/3140056538.cache'; + $datacont = serialize("test data 1"); + file_put_contents($datafile, $datacont); + $this->assertTrue(file_exists($datafile)); + + // Test 1: Rebuild without the event and test its there. + cache_factory::reset(); + $instance = cache_config_phpunittest::instance(); + $instance->phpunit_add_definition('phpunit/eventinvalidationtest', array( + 'mode' => cache_store::MODE_APPLICATION, + 'component' => 'phpunit', + 'area' => 'eventinvalidationtest', + )); + $cache = cache::make('phpunit', 'eventinvalidationtest'); + $this->assertEquals('test data 1', $cache->get('testkey1')); + + // Test 2: Rebuild and test the invalidation of the event via the invalidation cache + cache_factory::reset(); + $instance = cache_config_phpunittest::instance(); + $instance->phpunit_add_definition('phpunit/eventinvalidationtest', array( + 'mode' => cache_store::MODE_APPLICATION, + 'component' => 'phpunit', + 'area' => 'eventinvalidationtest', + 'invalidationevents' => array( + 'crazyevent' + ) + )); + $cache = cache::make('phpunit', 'eventinvalidationtest'); + $this->assertFalse($cache->get('testkey1')); + } + + /** + * Tests application cache event purge + */ + public function test_application_event_purge() { + $instance = cache_config_phpunittest::instance(); + $instance->phpunit_add_definition('phpunit/eventpurgetest', array( + 'mode' => cache_store::MODE_APPLICATION, + 'component' => 'phpunit', + 'area' => 'eventpurgetest', + 'invalidationevents' => array( + 'crazyevent' + ) + )); + $cache = cache::make('phpunit', 'eventpurgetest'); + + $this->assertTrue($cache->set('testkey1', 'test data 1')); + $this->assertEquals('test data 1', $cache->get('testkey1')); + $this->assertTrue($cache->set('testkey2', 'test data 2')); + $this->assertEquals('test data 2', $cache->get('testkey2')); + + // Purge the event. + cache_helper::purge_by_event('crazyevent'); + + // Check things have been removed. + $this->assertFalse($cache->get('testkey1')); + $this->assertFalse($cache->get('testkey2')); + } + + /** + * Tests application cache definition purge + */ + public function test_application_definition_purge() { + $instance = cache_config_phpunittest::instance(); + $instance->phpunit_add_definition('phpunit/definitionpurgetest', array( + 'mode' => cache_store::MODE_APPLICATION, + 'component' => 'phpunit', + 'area' => 'definitionpurgetest', + 'invalidationevents' => array( + 'crazyevent' + ) + )); + $cache = cache::make('phpunit', 'definitionpurgetest'); + + $this->assertTrue($cache->set('testkey1', 'test data 1')); + $this->assertEquals('test data 1', $cache->get('testkey1')); + $this->assertTrue($cache->set('testkey2', 'test data 2')); + $this->assertEquals('test data 2', $cache->get('testkey2')); + + // Purge the event. + cache_helper::purge_by_definition('phpunit', 'definitionpurgetest'); + + // Check things have been removed. + $this->assertFalse($cache->get('testkey1')); + $this->assertFalse($cache->get('testkey2')); + } +} \ No newline at end of file diff --git a/cache/tests/fixtures/lib.php b/cache/tests/fixtures/lib.php new file mode 100644 index 00000000000..2fdfe5593fb --- /dev/null +++ b/cache/tests/fixtures/lib.php @@ -0,0 +1,146 @@ +. + +/** + * Support library for the cache PHPUnit tests. + * + * This file is part of Moodle's cache API, affectionately called MUC. + * It contains the components that are requried in order to use caching. + * + * @package core + * @category cache + * @copyright 2012 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Override the default cache configuration for our own maniacle purposes. + * + * @copyright 2012 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class cache_config_phpunittest extends cache_config_writer { + /** + * Adds a definition to the stack + * @param string $area + * @param array $properties + */ + public function phpunit_add_definition($area, array $properties) { + $this->configdefinitions[$area] = $properties; + } + + /** + * Removes the configured stores so that there are none available. + */ + public function phpunit_remove_stores() { + $this->configstores = array(); + } +} + +/** + * Dummy object for testing cacheable object interface and interaction + * + * @copyright 2012 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class cache_phpunit_dummy_object extends stdClass implements cacheable_object { + /** + * Test property 1 + * @var string + */ + public $property1; + /** + * Test property 1 + * @var string + */ + public $property2; + /** + * Constructor + * @param string $property1 + * @param string $property2 + */ + public function __construct($property1, $property2) { + $this->property1 = $property1; + $this->property2 = $property2; + } + /** + * Prepares this object for caching + * @return array + */ + public function prepare_to_cache() { + return array($this->property1.'_ptc', $this->property2.'_ptc'); + } + /** + * Returns this object from the cache + * @param array $data + * @return cache_phpunit_dummy_object + */ + public static function wake_from_cache($data) { + return new cache_phpunit_dummy_object(array_shift($data).'_wfc', array_shift($data).'_wfc'); + } +} + +/** + * Dummy data source object for testing data source interface and implementation + * + * @copyright 2012 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class cache_phpunit_dummy_datasource implements cache_data_source { + /** + * Returns an instance of this object for use with the cache. + * + * @param cache_definition $definition + * @return cache_phpunit_dummy_datasource + */ + public static function get_instance_for_cache(cache_definition $definition) { + return new cache_phpunit_dummy_datasource(); + } + + /** + * Loads a key for the cache. + * + * @param string $key + * @return string + */ + public function load_for_cache($key) { + return $key.' has no value really.'; + } + + /** + * Loads many keys for the cache + * + * @param array $keys + * @return array + */ + public function load_many_for_cache(array $keys) { + $return = array(); + foreach ($keys as $key) { + $return[$key] = $key.' has no value really.'; + } + return $return; + } +} + +/** + * Dummy overridden cache loader class that we can use to test overriding loader functionality. + * + * @copyright 2012 Sam Hemelryk + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class cache_phpunit_dummy_overrideclass extends cache_application {} \ No newline at end of file