diff --git a/cache/README.md b/cache/README.md index 621c09c047e..3868c90fdba 100644 --- a/cache/README.md +++ b/cache/README.md @@ -18,6 +18,7 @@ A definition: 'requiremultipleidentifiers' => false, // Optional 'requirelockingread' => false, // Optional 'requirelockingwrite' => false, // Optional + 'requiresearchable' => false, // Optional 'maxsize' => null, // Optional 'overrideclass' => null, // Optional 'overrideclassfile' => null, // Optional @@ -135,6 +136,7 @@ The following optional settings can also be defined: * requiremultipleidentifiers - If set to true then only stores that support multiple identifiers will be used. * requirelockingread - If set to true a lock will be acquired for reading. Don't use this setting unless you have a REALLY good reason to. * requirelockingwrite - If set to true a lock will be acquired before writing to the cache. Avoid this unless necessary. +* requiresearchable - If set to true only stores that support key searching will be used for this definition. Its not recommended to use this unless absolutely unavoidable. * maxsize - This gives a cache an indication about the maximum items it should store. Cache stores don't have to use this, it is up to them to decide if its required. * overrideclass - If provided this class will be used for the loader. It must extend one of the core loader classes (based upon mode). * overrideclassfile - Included if required when using the overrideclass param. @@ -236,4 +238,4 @@ The following snippet illustates how to configure the three core cache stores th define('TEST_CACHESTORE_MEMCACHE_TESTSERVERS', '127.0.0.1:11211'); define('TEST_CACHESTORE_MEMCACHED_TESTSERVERS', '127.0.0.1:11211'); - define('TEST_CACHESTORE_MONGODB_TESTSERVER', 'mongodb://localhost:27017'); \ No newline at end of file + define('TEST_CACHESTORE_MONGODB_TESTSERVER', 'mongodb://localhost:27017'); diff --git a/cache/classes/definition.php b/cache/classes/definition.php index 8c5a16a7c6c..62732f737ce 100644 --- a/cache/classes/definition.php +++ b/cache/classes/definition.php @@ -183,6 +183,13 @@ class cache_definition { */ protected $requirelockingwrite = false; + /** + * Gets set to true if this definition requires searchable stores. + * @since 2.4.4 + * @var bool + */ + protected $requiresearchable = 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. @@ -307,6 +314,7 @@ class cache_definition { $requiremultipleidentifiers = false; $requirelockingread = false; $requirelockingwrite = false; + $requiresearchable = ($mode === cache_store::MODE_SESSION) ? true : false; $maxsize = null; $overrideclass = null; $overrideclassfile = null; @@ -342,6 +350,10 @@ class cache_definition { } $requirelocking = $requirelockingwrite || $requirelockingread; + if (array_key_exists('requiresearchable', $definition)) { + $requiresearchable = (bool)$definition['requiresearchable']; + } + if (array_key_exists('maxsize', $definition)) { $maxsize = (int)$definition['maxsize']; } @@ -433,6 +445,7 @@ class cache_definition { $cachedefinition->requirelocking = $requirelocking; $cachedefinition->requirelockingread = $requirelockingread; $cachedefinition->requirelockingwrite = $requirelockingwrite; + $cachedefinition->requiresearchable = $requiresearchable; $cachedefinition->maxsize = $maxsize; $cachedefinition->overrideclass = $overrideclass; $cachedefinition->overrideclassfile = $overrideclassfile; @@ -633,6 +646,15 @@ class cache_definition { return $this->requirelockingwrite; } + /** + * Returns true if this definition requires a searchable cache. + * @since 2.4.4 + * @return bool + */ + public function require_searchable() { + return $this->requiresearchable; + } + /** * Returns true if this definition has an associated data source. * @return bool @@ -686,6 +708,9 @@ class cache_definition { if ($this->require_multiple_identifiers()) { $requires += cache_store::SUPPORTS_MULTIPLE_IDENTIFIERS; } + if ($this->require_searchable()) { + $requires += cache_store::IS_SEARCHABLE; + } return $requires; } @@ -694,7 +719,7 @@ class cache_definition { * @return bool */ public function should_be_persistent() { - return $this->persistent; + return $this->persistent || $this->mode === cache_store::MODE_SESSION; } /** diff --git a/cache/classes/factory.php b/cache/classes/factory.php index 9eee85a11c7..e57d6ef077e 100644 --- a/cache/classes/factory.php +++ b/cache/classes/factory.php @@ -207,9 +207,7 @@ class cache_factory { 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, $options); - $config = $this->create_config_instance(); $definition->set_identifiers($identifiers); $cache = $this->create_cache($definition, $identifiers); if ($definition->should_be_persistent()) { diff --git a/cache/classes/helper.php b/cache/classes/helper.php index 075dc416dff..be329f77cde 100644 --- a/cache/classes/helper.php +++ b/cache/classes/helper.php @@ -244,10 +244,9 @@ class cache_helper { 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); + // or it is a persistent cache that also needs to be invalidated now. + if ($definition->get_mode() === cache_store::MODE_APPLICATION || $definition->should_be_persistent()) { + $cache = $factory->create_cache_from_definition($definition->get_component(), $definition->get_area()); $cache->delete_many($keys); } @@ -568,4 +567,64 @@ class cache_helper { global $CFG; return (string)$CFG->version; } + + /** + * Runs cron routines for MUC. + */ + public static function cron() { + self::clean_old_session_data(true); + } + + /** + * Cleans old session data from cache stores used for session based definitions. + * + * @param bool $output If set to true output will be given. + */ + public static function clean_old_session_data($output = false) { + global $CFG; + if ($output) { + mtrace('Cleaning up stale session data from cache stores.'); + } + $factory = cache_factory::instance(); + $config = $factory->create_config_instance(); + $definitions = $config->get_definitions(); + $purgetime = time() - $CFG->sessiontimeout; + foreach ($definitions as $definitionarray) { + // We are only interested in session caches. + if (!($definitionarray['mode'] & cache_store::MODE_SESSION)) { + continue; + } + $definition = $factory->create_definition($definitionarray['component'], $definitionarray['area']); + $stores = $config->get_stores_for_definition($definition); + // Turn them into store instances. + $stores = self::initialise_cachestore_instances($stores, $definition); + // Initialise all of the stores used for that definition. + foreach ($stores as $store) { + // If the store doesn't support searching we can skip it. + if (!($store instanceof cache_is_searchable)) { + debugging('Cache stores used for session definitions should ideally be searchable.', DEBUG_DEVELOPER); + continue; + } + // Get all of the keys. + $keys = $store->find_by_prefix(cache_session::KEY_PREFIX); + $todelete = array(); + foreach ($store->get_many($keys) as $key => $value) { + if (strpos($key, cache_session::KEY_PREFIX) !== 0 || !is_array($value) || !isset($value['lastaccess'])) { + continue; + } + if ((int)$value['lastaccess'] < $purgetime || true) { + $todelete[] = $key; + } + } + if (count($todelete)) { + $outcome = (int)$store->delete_many($todelete); + if ($output) { + $strdef = s($definition->get_id()); + $strstore = s($store->my_name()); + mtrace("- Removed {$outcome} old {$strdef} sessions from the '{$strstore}' cache store."); + } + } + } + } + } } diff --git a/cache/classes/interfaces.php b/cache/classes/interfaces.php index d54834961d0..24996ed4d33 100644 --- a/cache/classes/interfaces.php +++ b/cache/classes/interfaces.php @@ -338,6 +338,30 @@ interface cache_is_key_aware { public function has_all(array $keys); } +/** + * Cache store feature: keys are searchable. + * + * Cache stores can choose to implement this interface. + * In order for a store to be usable as a session cache it must implement this interface. + * + * @since 2.4.4 + */ +interface cache_is_searchable { + /** + * Finds all of the keys being used by the cache store. + * + * @return array. + */ + public function find_all(); + + /** + * Finds all of the keys whose keys start with the given prefix. + * + * @param string $prefix + */ + public function find_by_prefix($prefix); +} + /** * Cache store feature: configurable. * diff --git a/cache/classes/loaders.php b/cache/classes/loaders.php index fd1a3403c23..9f7dc79bcca 100644 --- a/cache/classes/loaders.php +++ b/cache/classes/loaders.php @@ -141,7 +141,7 @@ class cache implements cache_loader { * and having it here helps speed up processing. * @var strubg */ - private $storetype = 'unknown'; + protected $storetype = 'unknown'; /** * Gets set to true if we want to collect performance information about the cache API. @@ -365,6 +365,7 @@ class cache implements cache_loader { */ public function get_many(array $keys, $strictness = IGNORE_MISSING) { + $keysparsed = array(); $parsedkeys = array(); $resultpersist = array(); $resultstore = array(); @@ -374,6 +375,7 @@ class cache implements cache_loader { $isusingpersist = $this->is_using_persist_cache(); foreach ($keys as $key) { $pkey = $this->parse_key($key); + $keysparsed[$key] = $pkey; $parsedkeys[$pkey] = $key; $keystofind[$pkey] = $key; if ($isusingpersist) { @@ -426,9 +428,11 @@ class cache implements cache_loader { $resultmissing = $this->datasource->load_many_for_cache($missingkeys); } foreach ($resultmissing as $key => $value) { - $result[$key] = $value; + $pkey = ($usingloader) ? $key : $keysparsed[$key]; + $realkey = ($usingloader) ? $parsedkeys[$key] : $key; + $result[$pkey] = $value; if ($value !== false) { - $this->set($parsedkeys[$key], $value); + $this->set($realkey, $value); } } unset($resultmissing); @@ -748,7 +752,7 @@ class cache implements cache_loader { public function delete($key, $recurse = true) { $parsedkey = $this->parse_key($key); $this->delete_from_persist_cache($parsedkey); - if ($recurse && !empty($this->loader)) { + if ($recurse && $this->loader !== false) { // Delete from the bottom of the stack first. $this->loader->delete($key, $recurse); } @@ -770,7 +774,7 @@ class cache implements cache_loader { $this->delete_from_persist_cache($parsedkey); } } - if ($recurse && !empty($this->loader)) { + if ($recurse && $this->loader !== false) { // Delete from the bottom of the stack first. $this->loader->delete_many($keys, $recurse); } @@ -848,6 +852,26 @@ class cache implements cache_loader { return $this->store; } + /** + * Returns the loader associated with this instance. + * + * @since 2.4.4 + * @return cache_loader|false + */ + protected function get_loader() { + return $this->loader; + } + + /** + * Returns the data source associated with this cache. + * + * @since 2.4.4 + * @return cache_data_source|false + */ + protected function get_datasource() { + return $this->datasource; + } + /** * Returns true if the store supports key awareness. * @@ -1396,6 +1420,18 @@ class cache_application extends cache implements cache_loader_with_locking { * * This class is used for session caches returned by the cache::make methods. * + * It differs from the application loader in a couple of noteable ways: + * 1. Sessions are always expected to be persistent. + * Because of this we don't ever use the persist cache and instead a session array + * containing all of the data is maintained by this object. + * 2. Session data for a loader instance (store + definition) is consolidate into a + * single array for storage within the store. + * Along with this we embed a lastaccessed time with the data. This way we can + * check sessions for a last access time. + * 3. Session stores are required to support key searching and must + * implement cache_is_searchable. This ensures stores used for the cache can be + * targetted for garbage collection of session data. + * * 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. @@ -1421,6 +1457,24 @@ class cache_session extends cache { * @var int */ protected $currentuserid = null; + + /** + * The session id we are currently using. + * @var array + */ + protected $sessionid = null; + + /** + * The session data for the above session id. + * @var array + */ + protected $session = null; + + /** + * Constant used to prefix keys. + */ + const KEY_PREFIX = 'sess_'; + /** * Override the cache::construct method. * @@ -1494,12 +1548,16 @@ class cache_session extends cache { * This function is called for every operation that uses keys. For this reason we use this function to also check * that the current user is the same as the user who last used this cache. * + * On top of that if prepends the string 'sess_' to the start of all keys. The _ ensures things are easily identifiable. + * * @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) { - $this->check_tracked_user(); - return parent::parse_key($key); + if ($key === 'lastaccess') { + $key = '__lastaccess__'; + } + return 'sess_'.parent::parse_key($key); } /** @@ -1514,11 +1572,13 @@ class cache_session extends cache { $new = 0; } if ($new !== self::$loadeduserid) { - // The current user doesn't match the tracker userid for this request. + // The current user doesn't match the tracked userid for this request. if (!is_null(self::$loadeduserid)) { // Purge the data we have for the old user. // This way we don't bloat the session. $this->purge(); + // Update the session id just in case! + $this->sessionid = session_id(); } self::$loadeduserid = $new; $this->currentuserid = $new; @@ -1526,8 +1586,427 @@ class cache_session extends cache { // The current user matches the loaded user but not the user last used by this cache. $this->purge(); $this->currentuserid = $new; + // Update the session id just in case! + $this->sessionid = session_id(); } } + + /** + * Gets the session data. + * + * @param bool $force If true the session data will be loaded from the store again. + * @return array An array of session data. + */ + protected function get_session_data($force = false) { + if ($this->sessionid === null) { + $this->sessionid = session_id(); + } + if (is_array($this->session) && !$force) { + return $this->session; + } + $session = parent::get($this->sessionid); + if ($session === false) { + $session = array(); + } + // We have to write here to ensure that the lastaccess time is recorded. + // And also in order to ensure the session entry exists as when we save it on __destruct + // $CFG is likely to have already been destroyed. + $this->save_session($session); + return $this->session; + } + + /** + * Saves the session data. + * + * This function also updates the last access time. + * + * @param array $session + * @return bool + */ + protected function save_session(array $session) { + $session['lastaccess'] = time(); + $this->session = $session; + return parent::set($this->sessionid, $this->session); + } + + /** + * 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) { + // Check the tracked user. + $this->check_tracked_user(); + // 2. Parse the key. + $parsedkey = $this->parse_key($key); + // 3. Get it from the store. + $result = false; + $session = $this->get_session_data(); + if (array_key_exists($parsedkey, $session)) { + $result = $session[$parsedkey]; + if ($result instanceof cache_ttl_wrapper) { + if ($result->has_expired()) { + $this->get_store()->delete($parsedkey); + $result = false; + } else { + $result = $result->data; + } + } + if ($result instanceof cache_cached_object) { + $result = $result->restore_object(); + } + } + // 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('**static session**', $this->get_definition()->get_id()); + } + if ($this->get_loader() !== false) { + // We must pass the original (unparsed) key to the next loader in the chain. + // The next loader will parse the key as it sees fit. It may be parsed differently + // depending upon the capabilities of the store associated with the loader. + $result = $this->get_loader()->get($key); + } else if ($this->get_datasource() !== false) { + $result = $this->get_datasource()->load_for_cache($key); + } + $setaftervalidation = ($result !== false); + } else if ($this->perfdebug) { + cache_helper::record_cache_hit('**static session**', $this->get_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); + } + // 7. Make sure we don't pass back anything that could be a reference. + // We don't want people modifying the data in the cache. + if (!is_scalar($result)) { + // If data is an object it will be a reference. + // If data is an array if may contain references. + // We want to break references so that the cache cannot be modified outside of itself. + // Call the function to unreference it (in the best way possible). + $result = $this->unref($result); + } + return $result; + } + + /** + * 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) { + $this->check_tracked_user(); + if ($this->perfdebug) { + cache_helper::record_cache_set('**static session**', $this->get_definition()->get_id()); + } + if (is_object($data) && $data instanceof cacheable_object) { + $data = new cache_cached_object($data); + } else if (!is_scalar($data)) { + // If data is an object it will be a reference. + // If data is an array if may contain references. + // We want to break references so that the cache cannot be modified outside of itself. + // Call the function to unreference it (in the best way possible). + $data = $this->unref($data); + } + // We dont' support native TTL here as we consolidate data for sessions. + if ($this->has_a_ttl()) { + $data = new cache_ttl_wrapper($data, $this->get_definition()->get_ttl()); + } + $session = $this->get_session_data(); + $session[$this->parse_key($key)] = $data; + return $this->save_session($session); + } + + /** + * 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) { + $this->check_tracked_user(); + $parsedkey = $this->parse_key($key); + if ($recurse && $this->get_loader() !== false) { + // Delete from the bottom of the stack first. + $this->get_loader()->delete($key, $recurse); + } + $session = $this->get_session_data(); + unset($session[$parsedkey]); + return $this->save_session($session); + } + + /** + * 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) { + $this->check_tracked_user(); + $return = array(); + foreach ($keys as $key) { + $return[$key] = $this->get($key, $strictness); + } + return $return; + } + + /** + * 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) { + $this->check_tracked_user(); + $parsedkeys = array_map(array($this, 'parse_key'), $keys); + if ($recurse && $this->get_loader() !== false) { + // Delete from the bottom of the stack first. + $this->get_loader()->delete_many($keys, $recurse); + } + $session = $this->get_session_data(); + foreach ($parsedkeys as $parsedkey) { + unset($session[$parsedkey]); + } + $this->save_session($session); + return count($keys); + } + + /** + * 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) { + $this->check_tracked_user(); + $session = $this->get_session_data(); + $simulatettl = $this->has_a_ttl(); + foreach ($keyvaluearray as $key => $value) { + if (is_object($value) && $value instanceof cacheable_object) { + $value = new cache_cached_object($value); + } else if (!is_scalar($value)) { + // If data is an object it will be a reference. + // If data is an array if may contain references. + // We want to break references so that the cache cannot be modified outside of itself. + // Call the function to unreference it (in the best way possible). + $value = $this->unref($value); + } + if ($simulatettl) { + $value = new cache_ttl_wrapper($value, $this->get_definition()->get_ttl()); + } + $parsedkey = $this->parse_key($key); + $session[$parsedkey] = $value; + } + if ($this->perfdebug) { + cache_helper::record_cache_set($this->storetype, $this->get_definition()->get_id()); + } + $this->save_session($session); + return count($keyvaluearray); + } + + /** + * Purges the cache store, and loader if there is one. + * + * @return bool True on success, false otherwise + */ + public function purge() { + // 1. Purge the session object. + $this->session = array(); + // 2. Delete the record for this users session from the store. + $this->get_store()->delete($this->sessionid); + // 3. Optionally purge any stacked loaders in the same way. + if ($this->get_loader()) { + $this->get_loader()->delete($this->sessionid); + } + return true; + } + + /** + * 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) { + $this->check_tracked_user(); + $parsedkey = $this->parse_key($key); + $session = $this->get_session_data(); + $has = false; + if ($this->has_a_ttl()) { + // The data has a TTL and the store doesn't support it natively. + // We must fetch the data and expect a ttl wrapper. + if (array_key_exists($parsedkey, $session)) { + $data = $session[$parsedkey]; + $has = ($data instanceof cache_ttl_wrapper && !$data->has_expired()); + } + } else { + $has = array_key_exists($parsedkey, $session); + } + if (!$has && $tryloadifpossible) { + if ($this->get_loader() !== false) { + $result = $this->get_loader()->get($key); + } else if ($this->get_datasource() !== null) { + $result = $this->get_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) { + $this->check_tracked_user(); + $session = $this->get_session_data(); + foreach ($keys as $key) { + $has = false; + $parsedkey = $this->parse_key($key); + if ($this->has_a_ttl()) { + // The data has a TTL and the store doesn't support it natively. + // We must fetch the data and expect a ttl wrapper. + if (array_key_exists($parsedkey, $session)) { + $data = $session[$parsedkey]; + $has = ($data instanceof cache_ttl_wrapper && !$data->has_expired()); + } + } else { + $has = array_key_exists($parsedkey, $session); + } + if (!$has) { + return false; + } + } + return true; + } + + /** + * 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) { + $this->check_tracked_user(); + $session = $this->get_session_data(); + foreach ($keys as $key) { + $has = false; + $parsedkey = $this->parse_key($key); + if ($this->has_a_ttl()) { + // The data has a TTL and the store doesn't support it natively. + // We must fetch the data and expect a ttl wrapper. + if (array_key_exists($parsedkey, $session)) { + $data = $session[$parsedkey]; + $has = ($data instanceof cache_ttl_wrapper && !$data->has_expired()); + } + } else { + $has = array_key_exists($parsedkey, $session); + } + if ($has) { + return true; + } + } + return false; + } + + /** + * The session loader never uses the persist cache. + * Instead it stores things in the static $session variable. Shared between all session loaders. + * + * @return bool + */ + protected function is_using_persist_cache() { + return false; + } } /** diff --git a/cache/classes/store.php b/cache/classes/store.php index 5bb26e4d4fd..d757157536c 100644 --- a/cache/classes/store.php +++ b/cache/classes/store.php @@ -110,6 +110,11 @@ abstract class cache_store implements cache_store_interface { */ const SUPPORTS_NATIVE_TTL = 4; + /** + * The cache is searchable by key. + */ + const IS_SEARCHABLE = 8; + // Constants for the modes of a cache store /** @@ -307,6 +312,15 @@ abstract class cache_store implements cache_store_interface { return $this::get_supported_features() & self::SUPPORTS_NATIVE_TTL; } + /** + * Returns true if the store instance is searchable. + * + * @return bool + */ + public function is_searchable() { + return in_array('cache_is_searchable', class_implements($this)); + } + /** * Creates a clone of this store instance ready to be initialised. * diff --git a/cache/forms.php b/cache/forms.php index 9708a016657..92aa1ac0a1b 100644 --- a/cache/forms.php +++ b/cache/forms.php @@ -200,7 +200,7 @@ class cache_mode_mappings_form extends moodleform { ); foreach ($stores as $storename => $store) { foreach ($store['modes'] as $mode => $enabled) { - if ($enabled) { + if ($enabled && ($mode !== cache_store::MODE_SESSION || $store['supports']['searchable'])) { if (empty($store['default'])) { $options[$mode][$storename] = $store['name']; } else { diff --git a/cache/locallib.php b/cache/locallib.php index e2b498e19ab..f52570e310b 100644 --- a/cache/locallib.php +++ b/cache/locallib.php @@ -340,32 +340,7 @@ class cache_config_writer extends cache_config { require_once($CFG->dirroot.'/cache/stores/static/lib.php'); $writer = new self; - $writer->configstores = array( - 'default_application' => array( - 'name' => 'default_application', - 'plugin' => 'file', - 'configuration' => array(), - 'features' => cachestore_file::get_supported_features(), - 'modes' => cache_store::MODE_APPLICATION, - 'default' => true, - ), - 'default_session' => array( - 'name' => 'default_session', - 'plugin' => 'session', - 'configuration' => array(), - 'features' => cachestore_session::get_supported_features(), - 'modes' => cache_store::MODE_SESSION, - 'default' => true, - ), - 'default_request' => array( - 'name' => 'default_request', - 'plugin' => 'static', - 'configuration' => array(), - 'features' => cachestore_static::get_supported_features(), - 'modes' => cache_store::MODE_REQUEST, - 'default' => true, - ) - ); + $writer->configstores = self::get_default_stores(); $writer->configdefinitions = self::locate_definitions(); $writer->configmodemappings = array( array( @@ -404,6 +379,52 @@ class cache_config_writer extends cache_config { return true; } + /** + * Returns an array of default stores for use. + * + * @return array + */ + protected static function get_default_stores() { + return array( + 'default_application' => array( + 'name' => 'default_application', + 'plugin' => 'file', + 'configuration' => array(), + 'features' => cachestore_file::get_supported_features(), + 'modes' => cachestore_file::get_supported_modes(), + 'default' => true, + ), + 'default_session' => array( + 'name' => 'default_session', + 'plugin' => 'session', + 'configuration' => array(), + 'features' => cachestore_session::get_supported_features(), + 'modes' => cachestore_session::get_supported_modes(), + 'default' => true, + ), + 'default_request' => array( + 'name' => 'default_request', + 'plugin' => 'static', + 'configuration' => array(), + 'features' => cachestore_static::get_supported_features(), + 'modes' => cachestore_static::get_supported_modes(), + 'default' => true, + ) + ); + } + + /** + * Updates the default stores within the MUC config file. + */ + public static function update_default_config_stores() { + $factory = cache_factory::instance(); + $factory->updating_started(); + $config = $factory->create_config_instance(true); + $config->configstores = array_merge($config->configstores, self::get_default_stores()); + $config->config_save(); + $factory->updating_finished(); + } + /** * Updates the definition in the configuration from those found in the cache files. * @@ -581,6 +602,7 @@ abstract class cache_administration_helper extends cache_helper { 'nativettl' => $store->supports_native_ttl(), 'nativelocking' => ($store instanceof cache_is_lockable), 'keyawareness' => ($store instanceof cache_is_key_aware), + 'searchable' => ($store instanceof cache_is_searchable) ) ); if (empty($details['default'])) { diff --git a/cache/stores/file/lib.php b/cache/stores/file/lib.php index 855a223c8c1..54dfe930c6c 100644 --- a/cache/stores/file/lib.php +++ b/cache/stores/file/lib.php @@ -37,7 +37,7 @@ * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class cachestore_file extends cache_store implements cache_is_key_aware, cache_is_configurable { +class cachestore_file extends cache_store implements cache_is_key_aware, cache_is_configurable, cache_is_searchable { /** * The name of the store. @@ -190,7 +190,8 @@ class cachestore_file extends cache_store implements cache_is_key_aware, cache_i */ public static function get_supported_features(array $configuration = array()) { $supported = self::SUPPORTS_DATA_GUARANTEE + - self::SUPPORTS_NATIVE_TTL; + self::SUPPORTS_NATIVE_TTL + + self::IS_SEARCHABLE; return $supported; } @@ -257,13 +258,15 @@ class cachestore_file extends cache_store implements cache_is_key_aware, cache_i /** * Gets a pattern suitable for use with glob to find all keys in the cache. + * + * @param string $prefix A prefix to use. * @return string The pattern. */ - protected function glob_keys_pattern() { + protected function glob_keys_pattern($prefix = '') { if ($this->singledirectory) { - return $this->path . '/*.cache'; + return $this->path . '/'.$prefix.'*.cache'; } else { - return $this->path . '/*/*.cache'; + return $this->path . '/*/'.$prefix.'*.cache'; } } @@ -365,7 +368,6 @@ class cachestore_file extends cache_store implements cache_is_key_aware, cache_i public function delete($key) { $filename = $key.'.cache'; $file = $this->file_path_for_key($key); - if (@unlink($file)) { unset($this->keys[$filename]); return true; @@ -687,4 +689,42 @@ class cachestore_file extends cache_store implements cache_is_key_aware, cache_i public function my_name() { return $this->name; } + + /** + * Finds all of the keys being used by this cache store instance. + * + * @return array + */ + public function find_all() { + $this->ensure_path_exists(); + $files = glob($this->glob_keys_pattern(), GLOB_MARK | GLOB_NOSORT); + $return = array(); + if ($files === false) { + return $return; + } + foreach ($files as $file) { + $return[] = substr(basename($file), 0, -6); + } + return $return; + } + + /** + * Finds all of the keys whose keys start with the given prefix. + * + * @param string $prefix + */ + public function find_by_prefix($prefix) { + $this->ensure_path_exists(); + $prefix = preg_replace('#(\*|\?|\[)#', '[$1]', $prefix); + $files = glob($this->glob_keys_pattern($prefix), GLOB_MARK | GLOB_NOSORT); + $return = array(); + if ($files === false) { + return $return; + } + foreach ($files as $file) { + // Trim off ".cache" from the end. + $return[] = substr(basename($file), 0, -6); + } + return $return; + } } diff --git a/cache/stores/session/lib.php b/cache/stores/session/lib.php index 441bd70cc2e..39ebbf32ff8 100644 --- a/cache/stores/session/lib.php +++ b/cache/stores/session/lib.php @@ -90,7 +90,7 @@ abstract class session_data_store extends cache_store { * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class cachestore_session extends session_data_store implements cache_is_key_aware { +class cachestore_session extends session_data_store implements cache_is_key_aware, cache_is_searchable { /** * The name of the store @@ -137,7 +137,8 @@ class cachestore_session extends session_data_store implements cache_is_key_awar */ public static function get_supported_features(array $configuration = array()) { return self::SUPPORTS_DATA_GUARANTEE + - self::SUPPORTS_NATIVE_TTL; + self::SUPPORTS_NATIVE_TTL + + self::IS_SEARCHABLE; } /** @@ -403,4 +404,28 @@ class cachestore_session extends session_data_store implements cache_is_key_awar public function my_name() { return $this->name; } + + /** + * Finds all of the keys being stored in the cache store instance. + * + * @return array + */ + public function find_all() { + return array_keys($this->store); + } + + /** + * Finds all of the keys whose keys start with the given prefix. + * + * @param string $prefix + */ + public function find_by_prefix($prefix) { + $return = array(); + foreach ($this->find_all() as $key) { + if (strpos($key, $prefix) === 0) { + $return[] = $key; + } + } + return $return; + } } \ No newline at end of file diff --git a/cache/tests/cache_test.php b/cache/tests/cache_test.php index ba8115f8459..b13a4b570eb 100644 --- a/cache/tests/cache_test.php +++ b/cache/tests/cache_test.php @@ -126,6 +126,18 @@ class cache_phpunit_tests extends advanced_testcase { $cache = cache::make_from_params(cache_store::MODE_APPLICATION, 'phpunit', 'applicationtest'); $this->assertInstanceOf('cache_application', $cache); $this->run_on_cache($cache); + + $instance = cache_config_phpunittest::instance(true); + $instance->phpunit_add_definition('phpunit/test_default_application_cache', array( + 'mode' => cache_store::MODE_APPLICATION, + 'component' => 'phpunit', + 'area' => 'test_default_application_cache', + 'persistent' => true, + 'persistentmaxsize' => 1 + )); + $cache = cache::make('phpunit', 'test_default_application_cache'); + $this->assertInstanceOf('cache_application', $cache); + $this->run_on_cache($cache); } /** @@ -231,7 +243,7 @@ class cache_phpunit_tests extends advanced_testcase { $this->assertEquals('red_ptc_wfc', $result->property1); $this->assertEquals('blue_ptc_wfc', $result->property2); - // Test array of objects + // Test array of objects. $specobject = new cache_phpunit_dummy_object('red', 'blue'); $data = new cacheable_object_array(array( clone($specobject), @@ -255,6 +267,18 @@ class cache_phpunit_tests extends advanced_testcase { $this->assertTrue($cache->delete('key1')); $this->assertTrue($cache->delete('key2')); + $cache->set_many(array( + 'key1' => array(1, 2, 3), + 'key2' => array(3, 2, 1), + )); + $this->assertInternalType('array', $cache->get('key1')); + $this->assertInternalType('array', $cache->get('key2')); + $this->assertCount(3, $cache->get('key1')); + $this->assertCount(3, $cache->get('key2')); + $this->assertInternalType('array', $cache->get_many(array('key1', 'key2'))); + $this->assertCount(2, $cache->get_many(array('key1', 'key2'))); + $this->assertEquals(2, $cache->delete_many(array('key1', 'key2'))); + // Test delete many. $this->assertTrue($cache->set('key1', 'data1')); $this->assertTrue($cache->set('key2', 'data2')); @@ -333,6 +357,27 @@ class cache_phpunit_tests extends advanced_testcase { $this->assertEquals('value', $var2->key); $this->assertTrue($cache->delete('obj')); + + // Test strictness exceptions. + try { + $cache->get('exception', MUST_EXIST); + $this->fail('Exception expected from cache::get using MUST_EXIST'); + } catch (Exception $e) { + $this->assertTrue(true); + } + try { + $cache->get_many(array('exception1', 'exception2'), MUST_EXIST); + $this->fail('Exception expected from cache::get_many using MUST_EXIST'); + } catch (Exception $e) { + $this->assertTrue(true); + } + $cache->set('test', 'test'); + try { + $cache->get_many(array('test', 'exception'), MUST_EXIST); + $this->fail('Exception expected from cache::get_many using MUST_EXIST'); + } catch (Exception $e) { + $this->assertTrue(true); + } } /** @@ -355,13 +400,25 @@ class cache_phpunit_tests extends advanced_testcase { $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')); + + // Test multiple values. + $this->assertTrue($cache->purge()); + $this->assertTrue($cache->set('b', 'B')); + $result = $cache->get_many(array('a', 'b', 'c')); + $this->assertInternalType('array', $result); + $this->assertCount(3, $result); + $this->assertArrayHasKey('a', $result); + $this->assertArrayHasKey('b', $result); + $this->assertArrayHasKey('c', $result); + $this->assertEquals('a has no value really.', $result['a']); + $this->assertEquals('B', $result['b']); + $this->assertEquals('c has no value really.', $result['c']); } /** @@ -433,7 +490,10 @@ class cache_phpunit_tests extends advanced_testcase { $this->assertEquals('test data 2', $cache->get('2')); } - public function test_definition_ttl() { + /** + * Test a negative TTL on an application cache. + */ + public function test_application_ttl_negative() { $instance = cache_config_phpunittest::instance(true); $instance->phpunit_add_definition('phpunit/ttltest', array( 'mode' => cache_store::MODE_APPLICATION, @@ -454,6 +514,127 @@ class cache_phpunit_tests extends advanced_testcase { $this->assertFalse($cache->has('Test')); // Double check by trying to get it. $this->assertFalse($cache->get('Test')); + + // Test with multiple keys. + $this->assertEquals(3, $cache->set_many(array('a' => 'A', 'b' => 'B', 'c' => 'C'))); + $result = $cache->get_many(array('a', 'b', 'c')); + $this->assertInternalType('array', $result); + $this->assertCount(3, $result); + $this->assertArrayHasKey('a', $result); + $this->assertArrayHasKey('b', $result); + $this->assertArrayHasKey('c', $result); + $this->assertFalse($result['a']); + $this->assertFalse($result['b']); + $this->assertFalse($result['c']); + + // Test with multiple keys including missing ones. + $result = $cache->get_many(array('a', 'c', 'e')); + $this->assertInternalType('array', $result); + $this->assertCount(3, $result); + $this->assertArrayHasKey('a', $result); + $this->assertArrayHasKey('c', $result); + $this->assertArrayHasKey('e', $result); + $this->assertFalse($result['a']); + $this->assertFalse($result['c']); + $this->assertFalse($result['e']); + } + + /** + * Test a positive TTL on an application cache. + */ + public function test_application_ttl_positive() { + $instance = cache_config_phpunittest::instance(true); + $instance->phpunit_add_definition('phpunit/ttltest', array( + 'mode' => cache_store::MODE_APPLICATION, + 'component' => 'phpunit', + 'area' => 'ttltest', + 'ttl' => 86400 // Set to a day in the future to be extra sure. + )); + $cache = cache::make('phpunit', 'ttltest'); + $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')); + // Set it now. + $this->assertTrue($cache->set('Test', 'Test')); + // Check its there. + $this->assertTrue($cache->has('Test')); + // Double check by trying to get it. + $this->assertEquals('Test', $cache->get('Test')); + + // Test with multiple keys. + $this->assertEquals(3, $cache->set_many(array('a' => 'A', 'b' => 'B', 'c' => 'C'))); + $result = $cache->get_many(array('a', 'b', 'c')); + $this->assertInternalType('array', $result); + $this->assertCount(3, $result); + $this->assertArrayHasKey('a', $result); + $this->assertArrayHasKey('b', $result); + $this->assertArrayHasKey('c', $result); + $this->assertEquals('A', $result['a']); + $this->assertEquals('B', $result['b']); + $this->assertEquals('C', $result['c']); + + // Test with multiple keys including missing ones. + $result = $cache->get_many(array('a', 'c', 'e')); + $this->assertInternalType('array', $result); + $this->assertCount(3, $result); + $this->assertArrayHasKey('a', $result); + $this->assertArrayHasKey('c', $result); + $this->assertArrayHasKey('e', $result); + $this->assertEquals('A', $result['a']); + $this->assertEquals('C', $result['c']); + $this->assertEquals(false, $result['e']); + } + + /** + * Test a negative TTL on an session cache. + */ + public function test_session_ttl_positive() { + $instance = cache_config_phpunittest::instance(true); + $instance->phpunit_add_definition('phpunit/ttltest', array( + 'mode' => cache_store::MODE_SESSION, + 'component' => 'phpunit', + 'area' => 'ttltest', + 'ttl' => 86400 // Set to a day in the future to be extra sure. + )); + $cache = cache::make('phpunit', 'ttltest'); + $this->assertInstanceOf('cache_session', $cache); + + // Purge it to be sure. + $this->assertTrue($cache->purge()); + // It won't be there yet. + $this->assertFalse($cache->has('Test')); + // Set it now. + $this->assertTrue($cache->set('Test', 'Test')); + // Check its there. + $this->assertTrue($cache->has('Test')); + // Double check by trying to get it. + $this->assertEquals('Test', $cache->get('Test')); + + // Test with multiple keys. + $this->assertEquals(3, $cache->set_many(array('a' => 'A', 'b' => 'B', 'c' => 'C'))); + $result = $cache->get_many(array('a', 'b', 'c')); + $this->assertInternalType('array', $result); + $this->assertCount(3, $result); + $this->assertArrayHasKey('a', $result); + $this->assertArrayHasKey('b', $result); + $this->assertArrayHasKey('c', $result); + $this->assertEquals('A', $result['a']); + $this->assertEquals('B', $result['b']); + $this->assertEquals('C', $result['c']); + + // Test with multiple keys including missing ones. + $result = $cache->get_many(array('a', 'c', 'e')); + $this->assertInternalType('array', $result); + $this->assertCount(3, $result); + $this->assertArrayHasKey('a', $result); + $this->assertArrayHasKey('c', $result); + $this->assertArrayHasKey('e', $result); + $this->assertEquals('A', $result['a']); + $this->assertEquals('C', $result['c']); + $this->assertEquals(false, $result['e']); } /** @@ -520,6 +701,42 @@ class cache_phpunit_tests extends advanced_testcase { $this->assertFalse($cache->get('testkey2')); } + /** + * Tests session cache event invalidation + */ + public function test_session_event_invalidation() { + $instance = cache_config_phpunittest::instance(); + $instance->phpunit_add_definition('phpunit/test_session_event_invalidation', array( + 'mode' => cache_store::MODE_SESSION, + 'component' => 'phpunit', + 'area' => 'test_session_event_invalidation', + 'invalidationevents' => array( + 'crazyevent' + ) + )); + $cache = cache::make('phpunit', 'test_session_event_invalidation'); + $this->assertInstanceOf('cache_session', $cache); + + $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 */ @@ -556,6 +773,45 @@ class cache_phpunit_tests extends advanced_testcase { $this->assertFalse($cache->get('testkey2')); } + /** + * Tests session cache definition invalidation + */ + public function test_session_definition_invalidation() { + $instance = cache_config_phpunittest::instance(); + $instance->phpunit_add_definition('phpunit/test_session_definition_invalidation', array( + 'mode' => cache_store::MODE_SESSION, + 'component' => 'phpunit', + 'area' => 'test_session_definition_invalidation' + )); + $cache = cache::make('phpunit', 'test_session_definition_invalidation'); + $this->assertInstanceOf('cache_session', $cache); + $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', 'test_session_definition_invalidation', 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', 'test_session_definition_invalidation', 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', 'test_session_definition_invalidation', array(), + array('testkey1', 'testkey2')); + + $this->assertFalse($cache->get('testkey1')); + $this->assertFalse($cache->get('testkey2')); + } + /** * Tests application cache event invalidation over a distributed setup. */ @@ -865,6 +1121,34 @@ class cache_phpunit_tests extends advanced_testcase { $this->assertFalse($cache->get('test')); $this->assertTrue($cache->set('test', 'test')); $this->assertEquals('test', $cache->get('test')); + $this->assertTrue($cache->delete('test')); + $this->assertFalse($cache->get('test')); + $this->assertTrue($cache->set('test', 'test')); + $this->assertTrue($cache->purge()); + $this->assertFalse($cache->get('test')); + + // Test the many commands. + $this->assertEquals(3, $cache->set_many(array('a' => 'A', 'b' => 'B', 'c' => 'C'))); + $result = $cache->get_many(array('a', 'b', 'c')); + $this->assertInternalType('array', $result); + $this->assertCount(3, $result); + $this->assertArrayHasKey('a', $result); + $this->assertArrayHasKey('b', $result); + $this->assertArrayHasKey('c', $result); + $this->assertEquals('A', $result['a']); + $this->assertEquals('B', $result['b']); + $this->assertEquals('C', $result['c']); + $this->assertEquals($result, $cache->get_many(array('a', 'b', 'c'))); + $this->assertEquals(2, $cache->delete_many(array('a', 'c'))); + $result = $cache->get_many(array('a', 'b', 'c')); + $this->assertInternalType('array', $result); + $this->assertCount(3, $result); + $this->assertArrayHasKey('a', $result); + $this->assertArrayHasKey('b', $result); + $this->assertArrayHasKey('c', $result); + $this->assertFalse($result['a']); + $this->assertEquals('B', $result['b']); + $this->assertFalse($result['c']); } /** @@ -895,6 +1179,76 @@ class cache_phpunit_tests extends advanced_testcase { $this->assertEquals(false, $cache->get('var')); } + /** + * Test switching users with session caches. + */ + public function test_session_cache_switch_user_application_mapping() { + $this->resetAfterTest(true); + $instance = cache_config_phpunittest::instance(true); + $instance->phpunit_add_file_store('testfilestore'); + $instance->phpunit_add_definition('phpunit/testappsession', array( + 'mode' => cache_store::MODE_SESSION, + 'component' => 'phpunit', + 'area' => 'testappsession' + )); + $instance->phpunit_add_definition_mapping('phpunit/testappsession', 'testfilestore', 3); + $cache = cache::make('phpunit', 'testappsession'); + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + + // Log in as the first user. + $this->setUser($user1); + $sesskey1 = sesskey(); + + // Set a basic value in the cache. + $cache->set('var', 1); + $this->assertTrue($cache->has('var')); + $this->assertEquals(1, $cache->get('var')); + + // Change to the second user. + $this->setUser($user2); + $sesskey2 = sesskey(); + + // Make sure the cache doesn't give us the data for the last user. + $this->assertNotEquals($sesskey1, $sesskey2); + $this->assertFalse($cache->has('var')); + $this->assertEquals(false, $cache->get('var')); + } + + /** + * Test two session caches being used at once to confirm collisions don't occur. + */ + public function test_dual_session_caches() { + $instance = cache_config_phpunittest::instance(true); + $instance->phpunit_add_definition('phpunit/testsess1', array( + 'mode' => cache_store::MODE_SESSION, + 'component' => 'phpunit', + 'area' => 'testsess1' + )); + $instance->phpunit_add_definition('phpunit/testsess2', array( + 'mode' => cache_store::MODE_SESSION, + 'component' => 'phpunit', + 'area' => 'testsess2' + )); + $cache1 = cache::make('phpunit', 'testsess1'); + $cache2 = cache::make('phpunit', 'testsess2'); + + $this->assertFalse($cache1->has('test')); + $this->assertFalse($cache2->has('test')); + + $this->assertTrue($cache1->set('test', '1')); + + $this->assertTrue($cache1->has('test')); + $this->assertFalse($cache2->has('test')); + + $this->assertTrue($cache2->set('test', '2')); + + $this->assertEquals(1, $cache1->get('test')); + $this->assertEquals(2, $cache2->get('test')); + + $this->assertTrue($cache1->delete('test')); + } + /** * Test multiple session caches when switching user. */ @@ -925,4 +1279,30 @@ class cache_phpunit_tests extends advanced_testcase { $this->assertEquals(false, $cache1->get('var')); $this->assertEquals(false, $cache2->get('var')); } + + /** + * Test application locking. + */ + public function test_application_locking() { + $instance = cache_config_phpunittest::instance(true); + $instance->phpunit_add_definition('phpunit/test_application_locking', array( + 'mode' => cache_store::MODE_APPLICATION, + 'component' => 'phpunit', + 'area' => 'test_application_locking', + 'persistent' => true, + 'persistentmaxsize' => 1, + 'requirelockingread' => true, + 'requirelockingwrite' => true + )); + $cache = cache::make('phpunit', 'test_application_locking'); + $this->assertInstanceOf('cache_application', $cache); + + $this->assertTrue($cache->set('a', 'A')); + $this->assertTrue($cache->set('b', 'B')); + $this->assertTrue($cache->set('c', 'C')); + $this->assertEquals('A', $cache->get('a')); + $this->assertEquals(array('b' => 'B', 'c' => 'C'), $cache->get_many(array('b', 'c'))); + $this->assertTrue($cache->delete('a')); + $this->assertFalse($cache->has('a')); + } } diff --git a/cache/tests/fixtures/lib.php b/cache/tests/fixtures/lib.php index 42ce92b13d4..b4de9d05da2 100644 --- a/cache/tests/fixtures/lib.php +++ b/cache/tests/fixtures/lib.php @@ -35,6 +35,7 @@ defined('MOODLE_INTERNAL') || die(); * @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 diff --git a/lang/en/cache.php b/lang/en/cache.php index 689e6aebad4..cc49dabf808 100644 --- a/lang/en/cache.php +++ b/lang/en/cache.php @@ -135,6 +135,7 @@ $string['supports_dataguarantee'] = 'data guarantee'; $string['supports_nativettl'] = 'ttl'; $string['supports_nativelocking'] = 'locking'; $string['supports_keyawareness'] = 'key awareness'; +$string['supports_searchable'] = 'searching by key'; $string['tested'] = 'Tested'; $string['testperformance'] = 'Test performance'; $string['unsupportedmode'] = 'Unsupported mode'; diff --git a/lib/cronlib.php b/lib/cronlib.php index 0f33af88596..8762e442417 100644 --- a/lib/cronlib.php +++ b/lib/cronlib.php @@ -453,6 +453,9 @@ function cron_run() { mtrace('done.'); } + mtrace('Running cache cron routines'); + cache_helper::cron(); + mtrace('done.'); // Run automated backups if required - these may take a long time to execute require_once($CFG->dirroot.'/backup/util/includes/backup_includes.php'); diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index b568bff1fe5..e0eb87f2137 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -2117,5 +2117,13 @@ function xmldb_main_upgrade($oldversion) { upgrade_main_savepoint(true, 2013041601.01); } + if ($oldversion < 2013041900.00) { + require_once($CFG->dirroot . '/cache/locallib.php'); + // The features bin needs updating. + cache_config_writer::update_default_config_stores(); + // Main savepoint reached. + upgrade_main_savepoint(true, 2013041900.00); + } + return true; }